diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 58d5ea4762f..62bdc84b41a 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -3,7 +3,7 @@ class Projects::ReleasesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project - before_action :authorize_download_code! + before_action :authorize_read_release! before_action :check_releases_page_feature_flag def index @@ -12,8 +12,8 @@ class Projects::ReleasesController < Projects::ApplicationController private def check_releases_page_feature_flag - return render_404 unless Feature.enabled?(:releases_page) + return render_404 unless Feature.enabled?(:releases_page, @project) - push_frontend_feature_flag(:releases_page) + push_frontend_feature_flag(:releases_page, @project) end end diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb index 334e1847cc8..3725df010b0 100644 --- a/app/controllers/projects/tags/releases_controller.rb +++ b/app/controllers/projects/tags/releases_controller.rb @@ -4,7 +4,7 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_download_code! - before_action :authorize_push_code! + before_action :authorize_update_release! before_action :tag before_action :release diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index a50a1475eb2..35d798c0fda 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -43,9 +43,15 @@ class Projects::TagsController < Projects::ApplicationController def create result = ::Tags::CreateService.new(@project, current_user) - .execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) + .execute(params[:tag_name], params[:ref], params[:message]) if result[:status] == :success + # Release creation with Tags was deprecated in GitLab 11.7 + if params[:release_description].present? + CreateReleaseService.new(@project, current_user) + .execute(params[:tag_name], params[:release_description]) + end + @tag = result[:tag] redirect_to project_tag_path(@project, @tag.name) diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb new file mode 100644 index 00000000000..c185f77afa2 --- /dev/null +++ b/app/finders/releases_finder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ReleasesFinder + def initialize(project, current_user = nil) + @project = project + @current_user = current_user + end + + # rubocop: disable CodeReuse/ActiveRecord + def execute + return [] unless Ability.allowed?(@current_user, :read_release, @project) + + @project.releases.order('created_at DESC') + end + # rubocop: enable CodeReuse/ActiveRecord +end diff --git a/app/models/release.rb b/app/models/release.rb index 7a09ee459a6..a4272dd63aa 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -6,7 +6,15 @@ class Release < ActiveRecord::Base cache_markdown_field :description belongs_to :project + # releases prior to 11.7 have no author belongs_to :author, class_name: 'User' validates :description, :project, :tag, presence: true + + delegate :repository, to: :project + + def commit + git_tag = repository.find_tag(tag) + repository.commit(git_tag.dereferenced_target) + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index bcbd9676f2e..88cab816fe4 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -23,6 +23,7 @@ class ProjectPolicy < BasePolicy container_image pages cluster + release ].freeze desc "User is a project owner" @@ -173,6 +174,7 @@ class ProjectPolicy < BasePolicy enable :read_cycle_analytics enable :award_emoji enable :read_pages_content + enable :read_release end # These abilities are not allowed to admins that are not members of the project, @@ -239,6 +241,8 @@ class ProjectPolicy < BasePolicy enable :update_container_image enable :create_environment enable :create_deployment + enable :create_release + enable :update_release end rule { can?(:maintainer_access) }.policy do @@ -266,6 +270,7 @@ class ProjectPolicy < BasePolicy enable :update_cluster enable :admin_cluster enable :create_environment_terminal + enable :admin_release end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror @@ -325,6 +330,7 @@ class ProjectPolicy < BasePolicy prevent :download_code prevent :fork_project prevent :read_commit_status + prevent(*create_read_update_admin_destroy(:release)) end rule { container_registry_disabled }.policy do @@ -354,6 +360,7 @@ class ProjectPolicy < BasePolicy enable :read_commit_status enable :read_container_image enable :download_code + enable :read_release enable :download_wiki_code enable :read_cycle_analytics enable :read_pages_content diff --git a/app/services/commits/tag_service.rb b/app/services/commits/tag_service.rb index 7961ba4d3c4..bb8cfb63f98 100644 --- a/app/services/commits/tag_service.rb +++ b/app/services/commits/tag_service.rb @@ -9,11 +9,10 @@ module Commits tag_name = params[:tag_name] message = params[:tag_message] - release_description = nil result = Tags::CreateService .new(commit.project, current_user) - .execute(tag_name, commit.sha, message, release_description) + .execute(tag_name, commit.sha, message) if result[:status] == :success tag = result[:tag] diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb index ab2dc5337aa..223670a935d 100644 --- a/app/services/create_release_service.rb +++ b/app/services/create_release_service.rb @@ -1,35 +1,50 @@ # frozen_string_literal: true class CreateReleaseService < BaseService - # rubocop: disable CodeReuse/ActiveRecord - def execute(tag_name, release_description) + def execute(tag_name, release_description, name: nil, ref: nil) repository = project.repository - existing_tag = repository.find_tag(tag_name) + tag = repository.find_tag(tag_name) - # Only create a release if the tag exists - if existing_tag - release = project.releases.find_by(tag: tag_name) + if tag.blank? && ref.present? + result = create_tag(tag_name, ref) + return result unless result[:status] == :success - if release - error('Release already exists', 409) - else - release = project.releases.create!( - tag: tag_name, - name: tag_name, - sha: existing_tag.dereferenced_target.sha, - author: current_user, - description: release_description - ) + tag = result[:tag] + end - success(release) - end + if tag.present? + create_release(tag, name, release_description) else error('Tag does not exist', 404) end end - # rubocop: enable CodeReuse/ActiveRecord def success(release) super().merge(release: release) end + + private + + def create_release(tag, name, description) + release = project.releases.find_by(tag: tag.name) # rubocop: disable CodeReuse/ActiveRecord + + if release + error('Release already exists', 409) + else + release = project.releases.create!( + tag: tag.name, + name: name || tag.name, + sha: tag.dereferenced_target.sha, + author: current_user, + description: description + ) + + success(release) + end + end + + def create_tag(tag_name, ref) + Tags::CreateService.new(project, current_user) + .execute(tag_name, ref, nil) + end end diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 6bb9bb3988e..4de6b2d2774 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -2,7 +2,7 @@ module Tags class CreateService < BaseService - def execute(tag_name, target, message, release_description = nil) + def execute(tag_name, target, message) valid_tag = Gitlab::GitRefValidator.validate(tag_name) return error('Tag name invalid') unless valid_tag @@ -20,10 +20,7 @@ module Tags end if new_tag - if release_description.present? - CreateReleaseService.new(@project, @current_user) - .execute(tag_name, release_description) - end + repository.expire_tags_cache success.merge(tag: new_tag) else diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb index e2228ca026c..b51e72c7bfc 100644 --- a/app/services/update_release_service.rb +++ b/app/services/update_release_service.rb @@ -1,18 +1,28 @@ # frozen_string_literal: true class UpdateReleaseService < BaseService + attr_accessor :tag_name + + def initialize(project, user, tag_name, params) + super(project, user, params) + + @tag_name = tag_name + end + # rubocop: disable CodeReuse/ActiveRecord - def execute(tag_name, release_description) + def execute repository = project.repository - existing_tag = repository.find_tag(tag_name) + existing_tag = repository.find_tag(@tag_name) if existing_tag - release = project.releases.find_by(tag: tag_name) + release = project.releases.find_by(tag: @tag_name) if release - release.update(description: release_description) - - success(release) + if release.update(params) + success(release) + else + error(release.errors.messages || '400 Bad request', 400) + end else error('Release does not exist', 404) end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 59557c70904..d8017742c90 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -29,7 +29,7 @@ = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do %span= _('Activity') - - if project_nav_tab?(:releases) && Feature.enabled?(:releases_page) + - if project_nav_tab?(:releases) && Feature.enabled?(:releases_page, @project) = nav_link(controller: :releases) do = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do %span= _('Releases') diff --git a/changelogs/unreleased/ac-releases-api.yml b/changelogs/unreleased/ac-releases-api.yml new file mode 100644 index 00000000000..87217cce371 --- /dev/null +++ b/changelogs/unreleased/ac-releases-api.yml @@ -0,0 +1,5 @@ +--- +title: Releases API +merge_request: 23795 +author: +type: added diff --git a/lib/api/api.rb b/lib/api/api.rb index f1448da7403..19da0b2c434 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -139,6 +139,7 @@ module API mount ::API::ProjectTemplates mount ::API::ProtectedBranches mount ::API::ProtectedTags + mount ::API::Releases mount ::API::Repositories mount ::API::Runner mount ::API::Runners diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 22403664c21..7116ab2882b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1087,11 +1087,20 @@ module API expose :password_authentication_enabled_for_web, as: :signin_enabled end - class Release < Grape::Entity + # deprecated old Release representation + class TagRelease < Grape::Entity expose :tag, as: :tag_name expose :description end + class Release < TagRelease + expose :name + expose :description_html + expose :created_at + expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } + expose :commit, using: Entities::Commit + end + class Tag < Grape::Entity expose :name, :message, :target @@ -1100,7 +1109,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord - expose :release, using: Entities::Release do |repo_tag, options| + expose :release, using: Entities::TagRelease do |repo_tag, options| options[:project].releases.find_by(tag: repo_tag.name) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index c3eca713712..742d3760533 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -255,6 +255,18 @@ module API authorize! :update_build, user_project end + def authorize_create_release! + authorize! :create_release, user_project + end + + def authorize_read_release! + authorize! :read_release, user_project + end + + def authorize_update_release! + authorize! :update_release, user_project + end + def require_gitlab_workhorse! unless env['HTTP_GITLAB_WORKHORSE'].present? forbidden!('Request should be executed via GitLab Workhorse') diff --git a/lib/api/releases.rb b/lib/api/releases.rb new file mode 100644 index 00000000000..c5c2e3d9594 --- /dev/null +++ b/lib/api/releases.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module API + class Releases < Grape::API + include PaginationParams + + RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX) + + before { error!('404 Not Found', 404) unless Feature.enabled?(:releases_page, user_project) } + before { authorize_read_release! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get a project releases' do + detail 'This feature was introduced in GitLab 11.7.' + success Entities::Release + end + params do + use :pagination + end + get ':id/releases' do + releases = ::Kaminari.paginate_array(::ReleasesFinder.new(user_project, current_user).execute) + + present paginate(releases), with: Entities::Release + end + + desc 'Get a single project release' do + detail 'This feature was introduced in GitLab 11.7.' + success Entities::Release + end + params do + requires :tag_name, type: String, desc: 'The name of the tag' + end + get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do + release = user_project.releases.find_by_tag(params[:tag_name]) + not_found!('Release') unless release + + present release, with: Entities::Release + end + + desc 'Create a new release' do + detail 'This feature was introduced in GitLab 11.7.' + success Entities::Release + end + params do + requires :name, type: String, desc: 'The name of the release' + requires :tag_name, type: String, desc: 'The name of the tag' + requires :description, type: String, desc: 'The release notes' + optional :ref, type: String, desc: 'The commit sha or branch name' + end + post ':id/releases' do + authorize_create_release! + + result = ::CreateReleaseService.new(user_project, current_user) + .execute(params[:tag_name], params[:description], params[:name], params[:ref]) + + if result[:status] == :success + present result[:release], with: Entities::Release + else + render_api_error!(result[:message], 400) + end + end + + desc 'Update a release' do + detail 'This feature was introduced in GitLab 11.7.' + success Entities::Release + end + params do + requires :tag_name, type: String, desc: 'The name of the tag' + requires :name, type: String, desc: 'The name of the release' + requires :description, type: String, desc: 'Release notes with markdown support' + end + put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do + authorize_update_release! + + attributes = declared(params) + tag = attributes.delete(:tag_name) + result = UpdateReleaseService.new(user_project, current_user, tag, attributes).execute + + if result[:status] == :success + present result[:release], with: Entities::Release + else + render_api_error!(result[:message], result[:http_status]) + end + end + end + end +end diff --git a/lib/api/tags.rb b/lib/api/tags.rb index b18eec7d796..50ad184ba99 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -42,21 +42,28 @@ module API end desc 'Create a new repository tag' do + detail 'This optional release_description parameter was deprecated in GitLab 11.7.' success Entities::Tag end params do requires :tag_name, type: String, desc: 'The name of the tag' requires :ref, type: String, desc: 'The commit sha or branch name' optional :message, type: String, desc: 'Specifying a message creates an annotated tag' - optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database' + optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database (deprecated in GitLab 11.7)' end post ':id/repository/tags' do authorize_push_project result = ::Tags::CreateService.new(user_project, current_user) - .execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) + .execute(params[:tag_name], params[:ref], params[:message]) if result[:status] == :success + # Release creation with Tags API was deprecated in GitLab 11.7 + if params[:release_description].present? + CreateReleaseService.new(user_project, current_user) + .execute(params[:tag_name], params[:release_description]) + end + present result[:tag], with: Entities::Tag, project: user_project @@ -88,40 +95,46 @@ module API end desc 'Add a release note to a tag' do - success Entities::Release + detail 'This feature was deprecated in GitLab 11.7.' + success Entities::TagRelease end params do requires :tag_name, type: String, desc: 'The name of the tag' requires :description, type: String, desc: 'Release notes with markdown support' end post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do - authorize_push_project + authorize_create_release! result = CreateReleaseService.new(user_project, current_user) .execute(params[:tag_name], params[:description]) if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::TagRelease else render_api_error!(result[:message], result[:http_status]) end end desc "Update a tag's release note" do - success Entities::Release + detail 'This feature was deprecated in GitLab 11.7.' + success Entities::TagRelease end params do requires :tag_name, type: String, desc: 'The name of the tag' requires :description, type: String, desc: 'Release notes with markdown support' end put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do - authorize_push_project + authorize_update_release! - result = UpdateReleaseService.new(user_project, current_user) - .execute(params[:tag_name], params[:description]) + result = UpdateReleaseService.new( + user_project, + current_user, + params[:tag_name], + description: params[:description] + ).execute if result[:status] == :success - present result[:release], with: Entities::Release + present result[:release], with: Entities::TagRelease else render_api_error!(result[:message], result[:http_status]) end diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 18047c74a5d..eaf3999e9ff 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -4,5 +4,6 @@ FactoryBot.define do name { tag } description "Awesome release" project + author end end diff --git a/spec/finders/releases_finder_spec.rb b/spec/finders/releases_finder_spec.rb new file mode 100644 index 00000000000..aa7163021cb --- /dev/null +++ b/spec/finders/releases_finder_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ReleasesFinder do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:v1_0_0) { create(:release, project: project, tag: 'v1.0.0')} + let(:v1_1_0) { create(:release, project: project, tag: 'v1.1.0')} + + subject { described_class.new(project, user)} + + before do + now = Time.now + v1_0_0.update_attribute(:created_at, now - 2.days) + v1_1_0.update_attribute(:created_at, now - 1.day) + end + + describe '#execute' do + context 'when the user is not part of the project' do + it 'returns no releases' do + releases = subject.execute + + expect(releases).to be_empty + end + end + + context 'when the user is a project developer' do + before do + project.add_developer(user) + end + + it 'sorts by creation date' do + releases = subject.execute + + expect(releases).to be_present + expect(releases.size).to eq(2) + expect(releases).to eq([v1_1_0, v1_0_0]) + end + end + end +end diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 51725eeacac..92ba2d82f58 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -1,7 +1,9 @@ require 'rails_helper' RSpec.describe Release do - let(:release) { create(:release) } + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:release) { create(:release, project: project, author: user) } it { expect(release).to be_valid } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index fa47b95899a..ca339dd8ef5 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -15,7 +15,7 @@ describe ProjectPolicy do read_project_for_iids read_issue_iid read_merge_request_iid read_label read_milestone read_project_snippet read_project_member read_note create_project create_issue create_note upload_file create_merge_request_in - award_emoji + award_emoji read_release ] end @@ -38,7 +38,7 @@ describe ProjectPolicy do update_commit_status create_build update_build create_pipeline update_pipeline create_merge_request_from create_wiki push_code resolve_note create_container_image update_container_image - create_environment create_deployment + create_environment create_deployment create_release update_release ] end @@ -48,7 +48,7 @@ describe ProjectPolicy do update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project admin_commit_status admin_build admin_container_image - admin_pipeline admin_environment admin_deployment add_cluster + admin_pipeline admin_environment admin_deployment admin_release add_cluster ] end @@ -56,7 +56,7 @@ describe ProjectPolicy do %i[ download_code fork_project read_commit_status read_pipeline read_container_image build_download_code build_read_container_image - download_wiki_code + download_wiki_code read_release ] end @@ -183,7 +183,8 @@ describe ProjectPolicy do :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, :create_cluster, :read_cluster, :update_cluster, :admin_cluster, - :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment + :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment, + :admin_release ] expect_disallowed(*repository_permissions) diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb index 1a2dd0b39ee..d7ec6d4815a 100644 --- a/spec/services/create_release_service_spec.rb +++ b/spec/services/create_release_service_spec.rb @@ -4,27 +4,57 @@ describe CreateReleaseService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } + let(:name) { 'Bionic Beaver'} let(:description) { 'Awesome release!' } let(:service) { described_class.new(project, user) } - let(:tag) { project.repository.find_tag(tag_name) } - let(:sha) { tag.dereferenced_target.sha } + let(:ref) { nil } - it 'creates a new release' do - result = service.execute(tag_name, description) - expect(result[:status]).to eq(:success) - release = project.releases.find_by(tag: tag_name) - expect(release).not_to be_nil - expect(release.description).to eq(description) - expect(release.name).to eq(tag_name) - expect(release.sha).to eq(sha) - expect(release.author).to eq(user) + shared_examples 'a successful release creation' do + it 'creates a new release' do + result = service.execute(tag_name, description, name: name, ref: ref) + expect(result[:status]).to eq(:success) + release = project.releases.find_by(tag: tag_name) + expect(release).not_to be_nil + expect(release.description).to eq(description) + expect(release.name).to eq(name) + expect(release.author).to eq(user) + end end + it_behaves_like 'a successful release creation' + it 'raises an error if the tag does not exist' do result = service.execute("foobar", description) expect(result[:status]).to eq(:error) end + it 'keeps track of the commit sha' do + tag = project.repository.find_tag(tag_name) + sha = tag.dereferenced_target.sha + result = service.execute(tag_name, description, name: name) + + expect(result[:status]).to eq(:success) + expect(project.releases.find_by(tag: tag_name).sha).to eq(sha) + end + + context 'when ref is provided' do + let(:ref) { 'master' } + let(:tag_name) { 'foobar' } + + it_behaves_like 'a successful release creation' + + it 'creates a tag if the tag does not exist' do + expect(project.repository.ref_exists?("refs/tags/#{tag_name}")).to be_falsey + + result = service.execute(tag_name, description, name: name, ref: ref) + expect(result[:status]).to eq(:success) + expect(project.repository.ref_exists?("refs/tags/#{tag_name}")).to be_truthy + + release = project.releases.find_by(tag: tag_name) + expect(release).not_to be_nil + end + end + context 'there already exists a release on a tag' do before do service.execute(tag_name, description) diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb index dc2d0e2d47a..a24dcabfc2e 100644 --- a/spec/services/update_release_service_spec.rb +++ b/spec/services/update_release_service_spec.rb @@ -5,30 +5,49 @@ describe UpdateReleaseService do let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } let(:description) { 'Awesome release!' } + let(:new_name) { 'A new name' } let(:new_description) { 'The best release!' } - let(:service) { described_class.new(project, user) } + let(:params) { { name: new_name, description: new_description } } + let(:service) { described_class.new(project, user, tag_name, params) } + let(:create_service) { CreateReleaseService.new(project, user) } - context 'with an existing release' do - let(:create_service) { CreateReleaseService.new(project, user) } + before do + create_service.execute(tag_name, description) + end + shared_examples 'a failed update' do + it 'raises an error' do + result = service.execute + expect(result[:status]).to eq(:error) + end + end + + it 'successfully updates an existing release' do + result = service.execute + expect(result[:status]).to eq(:success) + + release = project.releases.find_by(tag: tag_name) + expect(release.name).to eq(new_name) + expect(release.description).to eq(new_description) + end + + context 'when the tag does not exists' do + let(:tag_name) { 'foobar' } + + it_behaves_like 'a failed update' + end + + context 'when the release does not exist' do before do - create_service.execute(tag_name, description) + project.releases.delete_all end - it 'successfully updates an existing release' do - result = service.execute(tag_name, new_description) - expect(result[:status]).to eq(:success) - expect(project.releases.find_by(tag: tag_name).description).to eq(new_description) - end + it_behaves_like 'a failed update' end - it 'raises an error if the tag does not exist' do - result = service.execute("foobar", description) - expect(result[:status]).to eq(:error) - end + context 'with an invalid update' do + let(:new_description) { '' } - it 'raises an error if the release does not exist' do - result = service.execute(tag_name, description) - expect(result[:status]).to eq(:error) + it_behaves_like 'a failed update' end end