Commit 102c1870 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'ac-releases-api' into 'master'

Add first-class releases API

See merge request gitlab-org/gitlab-ce!23795
parents 18601175 54df1a57
......@@ -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
......@@ -43,9 +43,22 @@ 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?
release_params = {
tag: params[:tag_name],
name: params[:tag_name],
description: params[:release_description]
}
Releases::CreateService
.new(@project, current_user, release_params)
.execute
end
@tag = result[:tag]
redirect_to project_tag_path(@project, @tag.name)
......
# frozen_string_literal: true
class ReleasesFinder
def initialize(project, current_user = nil)
@project = project
@current_user = current_user
end
def execute
return Release.none unless Ability.allowed?(@current_user, :read_release, @project)
@project.releases.sorted
end
end
......@@ -2,11 +2,39 @@
class Release < ActiveRecord::Base
include CacheMarkdownField
include Gitlab::Utils::StrongMemoize
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
scope :sorted, -> { order(created_at: :desc) }
delegate :repository, to: :project
def commit
strong_memoize(:commit) do
repository.commit(actual_sha)
end
end
def tag_missing?
actual_tag.nil?
end
private
def actual_sha
sha || actual_tag&.dereferenced_target
end
def actual_tag
strong_memoize(:actual_tag) do
repository.find_tag(tag)
end
end
end
......@@ -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 :destroy_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
......
# frozen_string_literal: true
class ReleasePolicy < BasePolicy
delegate { @subject.project }
end
......@@ -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]
......
# frozen_string_literal: true
class CreateReleaseService < BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name, release_description)
repository = project.repository
existing_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 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
)
success(release)
end
else
error('Tag does not exist', 404)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def success(release)
super().merge(release: release)
end
end
# frozen_string_literal: true
module Releases
module Concerns
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
included do
def tag_name
params[:tag]
end
def ref
params[:ref]
end
def name
params[:name]
end
def description
params[:description]
end
def release
strong_memoize(:release) do
project.releases.find_by_tag(tag_name)
end
end
def existing_tag
strong_memoize(:existing_tag) do
repository.find_tag(tag_name)
end
end
def tag_exist?
existing_tag.present?
end
def repository
strong_memoize(:repository) do
project.repository
end
end
end
end
end
# frozen_string_literal: true
module Releases
class CreateService < BaseService
include Releases::Concerns
def execute
return error('Access Denied', 403) unless allowed?
return error('Release already exists', 409) if release
tag = ensure_tag
return tag unless tag.is_a?(Gitlab::Git::Tag)
create_release(tag)
end
private
def ensure_tag
existing_tag || create_tag
end
def create_tag
return error('Ref is not specified', 422) unless ref
result = Tags::CreateService
.new(project, current_user)
.execute(tag_name, ref, nil)
return result unless result[:status] == :success
result[:tag]
end
def allowed?
Ability.allowed?(current_user, :create_release, project)
end
def create_release(tag)
release = project.releases.create!(
name: name,
description: description,
author: current_user,
tag: tag.name,
sha: tag.dereferenced_target.sha
)
success(tag: tag, release: release)
rescue ActiveRecord::RecordInvalid => e
error(e.message, 400)
end
end
end
# frozen_string_literal: true
module Releases
class DestroyService < BaseService
include Releases::Concerns
def execute
return error('Tag does not exist', 404) unless existing_tag
return error('Release does not exist', 404) unless release
return error('Access Denied', 403) unless allowed?
if release.destroy
success(tag: existing_tag, release: release)
else
error(release.errors.messages || '400 Bad request', 400)
end
end
private
def allowed?
Ability.allowed?(current_user, :destroy_release, release)
end
end
end
# frozen_string_literal: true
module Releases
class UpdateService < BaseService
include Releases::Concerns
def execute
return error('Tag does not exist', 404) unless existing_tag
return error('Release does not exist', 404) unless release
return error('Access Denied', 403) unless allowed?
return error('params is empty', 400) if empty_params?
if release.update(params)
success(tag: existing_tag, release: release)
else
error(release.errors.messages || '400 Bad request', 400)
end
end
private
def allowed?
Ability.allowed?(current_user, :update_release, release)
end
# rubocop: disable CodeReuse/ActiveRecord
def empty_params?
params.except(:tag).empty?
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
......@@ -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
......
......@@ -2,7 +2,6 @@
module Tags
class DestroyService < BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name)
repository = project.repository
tag = repository.find_tag(tag_name)
......@@ -12,8 +11,12 @@ module Tags
end
if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name)
release&.destroy
##
# When a tag in a repository is destroyed,
# release assets will be destroyed too.
Releases::DestroyService
.new(project, current_user, tag: tag_name)
.execute
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
......@@ -27,7 +30,6 @@ module Tags
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
# rubocop: enable CodeReuse/ActiveRecord
def error(message, return_code = 400)
super(message).merge(return_code: return_code)
......
# frozen_string_literal: true
class UpdateReleaseService < BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name, release_description)
repository = project.repository
existing_tag = repository.find_tag(tag_name)
if existing_tag
release = project.releases.find_by(tag: tag_name)
if release
release.update(description: release_description)
success(release)
else
error('Release does not exist', 404)
end
else
error('Tag does not exist', 404)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def success(release)
super().merge(release: release)
end
end
......@@ -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')
......
---
title: Releases API
merge_request: 23795
author:
type: added
......@@ -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
......
......@@ -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
......
# 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_releases! }
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 = ::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', as: :tag
end
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_read_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 :tag_name, type: String, desc: 'The name of the tag', as: :tag
requires :name, type: String, desc: 'The name of the release'
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 = ::Releases::CreateService
.new(user_project, current_user, declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
else
render_api_error!(result[:message], result[:http_status])
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', as: :tag
optional :name, type: String, desc: 'The name of the release'
optional :description, type: String, desc: 'Release notes with markdown support'
end
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_update_release!
result = ::Releases::UpdateService
.new(user_project, current_user, declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
else
render_api_error!(result[:message], result[:http_status])
end
end
desc 'Delete 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', as: :tag
end
delete ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_destroy_release!
result = ::Releases::DestroyService
.new(user_project, current_user, declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
else
render_api_error!(result[:message], result[:http_status])
end
end
end
helpers do
def authorize_create_release!
authorize! :create_release, user_project
end
def authorize_read_releases!
authorize! :read_release, user_project
end
def authorize_read_release!
authorize! :read_release, release
end
def authorize_update_release!
authorize! :update_release, release
end
def authorize_destroy_release!
authorize! :destroy_release, release
end
def release
@release ||= user_project.releases.find_by_tag(params[:tag])
end
end
end
end
......@@ -42,21 +42,35 @@ 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?
release_create_params = {
tag: params[:tag_name],
name: params[:tag_name], # Name can be specified in new API
description: params[:release_description]
}
::Releases::CreateService
.new(user_project, current_user, release_create_params)
.execute
end
present result[:tag],
with: Entities::Tag,
project: user_project
......@@ -88,44 +102,72 @@ 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 :tag_name, type: String, desc: 'The name of the tag', as: :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!
##
# Legacy API does not support tag auto creation.
not_found!('Tag') unless user_project.repository.find_tag(params[:tag])
result = CreateReleaseService.new(user_project, current_user)
.execute(params[:tag_name], params[:description])
release_create_params = {
tag: params[:tag],
name: params[:tag], # Name can be specified in new API
description: params[:description]
}
result = ::Releases::CreateService
.new(user_project, current_user, release_create_params)
.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
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 :tag_name, type: String, desc: 'The name of the tag', as: :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 = ::Releases::UpdateService
.new(user_project, current_user, declared_params(include_missing: false))
.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
end
end
helpers do
def authorize_create_release!
authorize! :create_release, user_project
end
def authorize_update_release!
authorize! :update_release, release
end
def release
@release ||= user_project.releases.find_by_tag(params[:tag])
end
end
end
end
FactoryBot.define do
factory :release do
tag "v1.1.0"
sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
name { tag }
description "Awesome release"
project
author
trait :legacy do
sha nil
author nil
end
end
end
# 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
v1_0_0.update_attribute(:created_at, 2.days.ago)
v1_1_0.update_attribute(:created_at, 1.day.ago)
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
{
"type": "object",
"required": ["name", "tag_name"],
"properties": {
"name": { "type": "string" },
"tag_name": { "type": "string" },
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
"commit": {
"oneOf": [{ "type": "null" }, { "$ref": "public_api/v4/commit/basic.json" }]
},
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "public_api/v4/user/basic.json" }]
}
},
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "release.json" }
}
......@@ -138,7 +138,7 @@ describe Gitlab::LegacyGithubImport::Importer do
let(:release2) do
double(
tag_name: 'v2.0.0',
tag_name: 'v1.1.0',
name: 'Second release',
body: nil,
draft: false,
......
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 }
......
......@@ -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 destroy_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,
:destroy_release
]
expect_disallowed(*repository_permissions)
......
require 'spec_helper'
describe API::Releases do
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
let(:non_project_member) { create(:user) }
let(:commit) { create(:commit, project: project) }
before do
project.add_maintainer(maintainer)
project.add_reporter(reporter)
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
end
describe 'GET /projects/:id/releases' do
context 'when there are two releases' do
let!(:release_1) do
create(:release,
project: project,
tag: 'v0.1',
author: maintainer,
created_at: 2.days.ago)
end
let!(:release_2) do
create(:release,
project: project,
tag: 'v0.2',
author: maintainer,
created_at: 1.day.ago)
end
it 'returns 200 HTTP status' do
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns releases ordered by created_at' do
get api("/projects/#{project.id}/releases", maintainer)
expect(json_response.count).to eq(2)
expect(json_response.first['tag_name']).to eq(release_2.tag)
expect(json_response.second['tag_name']).to eq(release_1.tag)
end
it 'matches response schema' do
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to match_response_schema('releases')
end
end
context 'when tag does not exist in git repository' do
let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
it 'returns the tag' do
get api("/projects/#{project.id}/releases", maintainer)
expect(json_response.count).to eq(1)
expect(json_response.first['tag_name']).to eq('v1.1.5')
expect(release).to be_tag_missing
end
end
context 'when user is not a project member' do
it 'cannot find the project' do
get api("/projects/#{project.id}/releases", non_project_member)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'allows the request' do
get api("/projects/#{project.id}/releases", non_project_member)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(releases_page: false)
end
it 'cannot find the API' do
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET /projects/:id/releases/:tag_name' do
context 'when there is a release' do
let!(:release) do
create(:release,
project: project,
tag: 'v0.1',
sha: commit.id,
author: maintainer,
description: 'This is v0.1')
end
it 'returns 200 HTTP status' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns a release entry' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(json_response['tag_name']).to eq(release.tag)
expect(json_response['description']).to eq('This is v0.1')
expect(json_response['author']['name']).to eq(maintainer.name)
expect(json_response['commit']['id']).to eq(commit.id)
end
it 'matches response schema' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to match_response_schema('release')
end
end
context 'when specified tag is not found in the project' do
it 'cannot find the release entry' do
get api("/projects/#{project.id}/releases/non_exist_tag", maintainer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
let!(:release) { create(:release, tag: 'v0.1', project: project) }
it 'cannot find the project' do
get api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'allows the request' do
get api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(releases_page: false)
end
it 'cannot find the API' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'POST /projects/:id/releases' do
let(:params) do
{
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release'
}
end
it 'accepts the request' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to have_gitlab_http_status(:created)
end
it 'creates a new release' do
expect do
post api("/projects/#{project.id}/releases", maintainer), params: params
end.to change { Release.count }.by(1)
expect(project.releases.last.name).to eq('New release')
expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.description).to eq('Super nice release')
end
context 'when description is empty' do
let(:params) do
{
name: 'New release',
tag_name: 'v0.1',
description: ''
}
end
it 'returns an error as validation failure' do
expect do
post api("/projects/#{project.id}/releases", maintainer), params: params
end.not_to change { Release.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message'])
.to eq("Validation failed: Description can't be blank")
end
end
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to match_response_schema('release')
end
it 'does not create a new tag' do
expect do
post api("/projects/#{project.id}/releases", maintainer), params: params
end.not_to change { Project.find_by_id(project.id).repository.tag_count }
end
context 'when user is a reporter' do
it 'forbids the request' do
post api("/projects/#{project.id}/releases", reporter), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'forbids the request' do
post api("/projects/#{project.id}/releases", non_project_member),
params: params
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'forbids the request' do
post api("/projects/#{project.id}/releases", non_project_member),
params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when tag does not exist in git repository' do
let(:params) do
{
name: 'Android ~ Ice Cream Sandwich ~',
tag_name: tag_name,
description: 'Android 4.0–4.0.4 "Ice Cream Sandwich" is the ninth' \
'version of the Android mobile operating system developed' \
'by Google.',
ref: 'master'
}
end
let(:tag_name) { 'v4.0' }
it 'creates a new tag' do
expect do
post api("/projects/#{project.id}/releases", maintainer), params: params
end.to change { Project.find_by_id(project.id).repository.tag_count }.by(1)
expect(project.repository.find_tag('v4.0').dereferenced_target.id)
.to eq(project.repository.commit('master').id)
end
it 'creates a new release' do
expect do
post api("/projects/#{project.id}/releases", maintainer), params: params
end.to change { Release.count }.by(1)
expect(project.releases.last.name).to eq('Android ~ Ice Cream Sandwich ~')
expect(project.releases.last.tag).to eq('v4.0')
expect(project.releases.last.description).to eq(
'Android 4.0–4.0.4 "Ice Cream Sandwich" is the ninth' \
'version of the Android mobile operating system developed' \
'by Google.')
end
context 'when tag name is HEAD' do
let(:tag_name) { 'HEAD' }
it 'returns an error as failure on tag creation' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response['message']).to eq('Tag name invalid')
end
end
context 'when tag name is empty' do
let(:tag_name) { '' }
it 'returns an error as failure on tag creation' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response['message']).to eq('Tag name invalid')
end
end
end
context 'when release already exists' do
before do
create(:release, project: project, tag: 'v0.1', name: 'New release')
end
it 'returns an error as conflicted request' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to have_gitlab_http_status(:conflict)
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(releases_page: false)
end
it 'cannot find the API' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'PUT /projects/:id/releases/:tag_name' do
let(:params) { { description: 'Best release ever!' } }
let!(:release) do
create(:release,
project: project,
tag: 'v0.1',
name: 'New release',
description: 'Super nice release')
end
it 'accepts the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'updates the description' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(project.releases.last.description).to eq('Best release ever!')
end
it 'does not change other attributes' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(project.releases.last.tag).to eq('v0.1')
expect(project.releases.last.name).to eq('New release')
end
it 'matches response schema' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(response).to match_response_schema('release')
end
context 'when user tries to update sha' do
let(:params) { { sha: 'xxx' } }
it 'does not allow the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when params is empty' do
let(:params) { {} }
it 'does not allow the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when there are no corresponding releases' do
let!(:release) { }
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is a reporter' do
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", reporter), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", non_project_member),
params: params
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'forbids the request' do
put api("/projects/#{project.id}/releases/v0.1", non_project_member),
params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(releases_page: false)
end
it 'cannot find the API' do
put api("/projects/#{project.id}/releases/v0.1", non_project_member),
params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'DELETE /projects/:id/releases/:tag_name' do
let!(:release) do
create(:release,
project: project,
tag: 'v0.1',
name: 'New release',
description: 'Super nice release')
end
it 'accepts the request' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:ok)
end
it 'destroys the release' do
expect do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
end.to change { Release.count }.by(-1)
end
it 'does not remove a tag in repository' do
expect do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
end.not_to change { Project.find_by_id(project.id).repository.tag_count }
end
it 'matches response schema' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to match_response_schema('release')
end
context 'when there are no corresponding releases' do
let!(:release) { }
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is a reporter' do
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", reporter)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when user is not a project member' do
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is public' do
let(:project) { create(:project, :repository, :public) }
it 'forbids the request' do
delete api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(releases_page: false)
end
it 'cannot find the API' do
delete api("/projects/#{project.id}/releases/v0.1", non_project_member)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......@@ -107,9 +107,12 @@ describe API::Tags do
context 'with releases' do
let(:description) { 'Awesome release!' }
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
release.update(description: description)
let!(:release) do
create(:release,
:legacy,
project: project,
tag: tag_name,
description: description)
end
it 'returns an array of project tags with release info' do
......@@ -373,7 +376,7 @@ describe API::Tags do
it_behaves_like '404 response' do
let(:request) { post api(route, current_user), params: { description: description } }
let(:message) { 'Tag does not exist' }
let(:message) { '404 Tag Not Found' }
end
end
......@@ -398,10 +401,7 @@ describe API::Tags do
end
context 'on tag with existing release' do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
release.update(description: description)
end
let!(:release) { create(:release, :legacy, project: project, tag: tag_name, description: description) }
it 'returns 409 if there is already a release' do
post api(route, user), params: { description: description }
......@@ -420,9 +420,12 @@ describe API::Tags do
shared_examples_for 'repository update release' do
context 'on tag with existing release' do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
release.update(description: description)
let!(:release) do
create(:release,
:legacy,
project: project,
tag: tag_name,
description: description)
end
it 'updates the release description' do
......@@ -437,9 +440,9 @@ describe API::Tags do
context 'when tag does not exist' do
let(:tag_name) { 'unknown' }
it_behaves_like '404 response' do
it_behaves_like '403 response' do
let(:request) { put api(route, current_user), params: { description: new_description } }
let(:message) { 'Tag does not exist' }
let(:message) { '403 Forbidden' }
end
end
......@@ -464,9 +467,9 @@ describe API::Tags do
end
context 'when release does not exist' do
it_behaves_like '404 response' do
it_behaves_like '403 response' do
let(:request) { put api(route, current_user), params: { description: new_description } }
let(:message) { 'Release does not exist' }
let(:message) { '403 Forbidden' }
end
end
end
......
require 'spec_helper'
describe CreateReleaseService do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
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 }
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)
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 'there already exists a release on a tag' do
before do
service.execute(tag_name, description)
end
it 'raises an error and does not update the release' do
result = service.execute(tag_name, 'The best release!')
expect(result[:status]).to eq(:error)
expect(project.releases.find_by(tag: tag_name).description).to eq(description)
end
end
end
require 'spec_helper'
describe Releases::CreateService do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
let(:tag_sha) { project.repository.find_tag(tag_name).dereferenced_target.sha }
let(:name) { 'Bionic Beaver' }
let(:description) { 'Awesome release!' }
let(:params) { { tag: tag_name, name: name, description: description, ref: ref } }
let(:ref) { nil }
let(:service) { described_class.new(project, user, params) }
before do
project.add_maintainer(user)
end
describe '#execute' do
shared_examples 'a successful release creation' do
it 'creates a new release' do
result = service.execute
expect(result[:status]).to eq(:success)
expect(result[:tag]).not_to be_nil
expect(result[:release]).not_to be_nil
expect(result[:release].description).to eq(description)
expect(result[:release].name).to eq(name)
expect(result[:release].author).to eq(user)
expect(result[:release].sha).to eq(tag_sha)
end
end
it_behaves_like 'a successful release creation'
context 'when the tag does not exist' do
let(:tag_name) { 'non-exist-tag' }
it 'raises an error' do
result = service.execute
expect(result[:status]).to eq(:error)
end
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
expect(result[:status]).to eq(:success)
expect(result[:tag]).not_to be_nil
expect(result[:release]).not_to be_nil
end
end
context 'there already exists a release on a tag' do
let!(:release) do
create(:release, project: project, tag: tag_name, description: description)
end
it 'raises an error and does not update the release' do
result = service.execute
expect(result[:status]).to eq(:error)
expect(project.releases.find_by(tag: tag_name).description).to eq(description)
end
end
end
end
require 'spec_helper'
describe Releases::DestroyService do
let(:project) { create(:project, :repository) }
let(:mainatiner) { create(:user) }
let(:repoter) { create(:user) }
let(:tag) { 'v1.1.0' }
let!(:release) { create(:release, project: project, tag: tag) }
let(:service) { described_class.new(project, user, params) }
let(:params) { { tag: tag } }
let(:user) { mainatiner }
before do
project.add_maintainer(mainatiner)
project.add_reporter(repoter)
end
describe '#execute' do
subject { service.execute }
context 'when there is a release' do
it 'removes the release' do
expect { subject }.to change { project.releases.count }.by(-1)
end
it 'returns the destroyed object' do
is_expected.to include(status: :success, release: release)
end
end
context 'when tag is not found' do
let(:tag) { 'v1.1.1' }
it 'returns an error' do
is_expected.to include(status: :error,
message: 'Tag does not exist',
http_status: 404)
end
end
context 'when release is not found' do
let!(:release) { }
it 'returns an error' do
is_expected.to include(status: :error,
message: 'Release does not exist',
http_status: 404)
end
end
context 'when user does not have permission' do
let(:user) { repoter }
it 'returns an error' do
is_expected.to include(status: :error,
message: 'Access Denied',
http_status: 403)
end
end
end
end
require 'spec_helper'
describe Releases::UpdateService do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:new_name) { 'A new name' }
let(:new_description) { 'The best release!' }
let(:params) { { name: new_name, description: new_description, tag: tag_name } }
let(:service) { described_class.new(project, user, params) }
let!(:release) { create(:release, project: project, author: user, tag: tag_name) }
let(:tag_name) { 'v1.1.0' }
before do
project.add_developer(user)
end
describe '#execute' do
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)
expect(result[:release].name).to eq(new_name)
expect(result[: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
let!(:release) { }
it_behaves_like 'a failed update'
end
context 'with an invalid update' do
let(:new_description) { '' }
it_behaves_like 'a failed update'
end
end
end
require 'spec_helper'
describe UpdateReleaseService do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
let(:description) { 'Awesome release!' }
let(:new_description) { 'The best release!' }
let(:service) { described_class.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
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
end
it 'raises an error if the tag does not exist' do
result = service.execute("foobar", description)
expect(result[:status]).to eq(:error)
end
it 'raises an error if the release does not exist' do
result = service.execute(tag_name, description)
expect(result[:status]).to eq(:error)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment