Commit 8f1e96c8 authored by Shinya Maeda's avatar Shinya Maeda

Add spec for Release API

Add spec for all release API - GET, POST, PUT, DELETE.
Also, fixes some minior bugs.
parent dc8a8c7d
......@@ -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_update_release!
before_action :authorize_push_code!
before_action :tag
before_action :release
......
......@@ -48,8 +48,15 @@ class Projects::TagsController < Projects::ApplicationController
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], description: params[:release_description] }
CreateReleaseService.new(@project, current_user, release_params).execute
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]
......
......@@ -2,6 +2,7 @@
class Release < ActiveRecord::Base
include CacheMarkdownField
include Gitlab::Utils::StrongMemoize
cache_markdown_field :description
......@@ -15,25 +16,25 @@ class Release < ActiveRecord::Base
delegate :repository, to: :project
def self.by_tag(project, tag)
self.find_by(project: project, tag: tag)
end
def commit
git_tag = repository.find_tag(tag)
repository.commit(git_tag.dereferenced_target)
strong_memoize(:commit) do
repository.commit(actual_sha)
end
end
def sources_formats
@sources_formats ||= %w(zip tar.gz tar.bz2 tar).freeze
def tag_missing?
actual_tag.nil?
end
# TODO: placeholder for frontend API compatibility
def links
[]
private
def actual_sha
sha || actual_tag&.dereferenced_target
end
def assets_count
links.size + sources_formats.size
def actual_tag
strong_memoize(:actual_tag) do
repository.find_tag(tag)
end
end
end
......@@ -270,7 +270,7 @@ class ProjectPolicy < BasePolicy
enable :update_cluster
enable :admin_cluster
enable :create_environment_terminal
enable :admin_release
enable :destroy_release
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
......
# frozen_string_literal: true
class ReleasePolicy < BasePolicy
delegate { @subject.project }
end
# frozen_string_literal: true
class CreateReleaseService < BaseService
def execute(ref = nil)
return error('Unauthorized', 401) unless Ability.allowed?(current_user, :create_release, project)
tag_result = find_or_create_tag(ref)
return tag_result if tag_result[:status] != :success
create_release(tag_result[:tag])
end
private
def find_or_create_tag(ref)
tag = repository.find_tag(params[:tag])
return success(tag: tag) if tag
return error('Tag does not exist', 404) if ref.blank?
Tags::CreateService.new(project, current_user).execute(params[:tag], ref, nil)
end
def create_release(tag)
release = Release.by_tag(project, tag.name)
if release
error('Release already exists', 409)
else
create_params = {
author: current_user,
name: tag.name,
sha: tag.dereferenced_target.sha
}.merge(params)
release = project.releases.create!(create_params)
success(tag: tag, release: release)
end
end
end
# frozen_string_literal: true
class DeleteReleaseService < BaseService
include Gitlab::Utils::StrongMemoize
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.destory
success(release: release)
else
error(release.errors.messages || '400 Bad request', 400)
end
end
private
def allowed?
Ability.allowed?(current_user, :admin_release, release)
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 repository
strong_memoize(:repository) do
project.repository
end
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
new_tag = nil
unless tag_exist?
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
new_tag = result[:tag]
end
create_release(existing_tag || new_tag)
end
private
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,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
def execute
return error('Unauthorized', 401) unless Ability.allowed?(current_user, :update_release, project)
tag_name = params[:tag]
release = Release.by_tag(project, tag_name)
return error('Release does not exist', 404) if release.blank?
if release.update(params)
success(release: release)
else
error(release.errors.messages || '400 Bad request', 400)
end
end
end
......@@ -1099,20 +1099,6 @@ module API
expose :created_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit
expose :assets do
expose :assets_count, as: :count
expose :links
expose :sources do |release, _opts|
archive_path = "#{release.project.path}-#{release.tag.tr('/', '-')}"
release.sources_formats.map do |format|
{
format: format,
url: Gitlab::Routing.url_helpers.project_archive_url(release.project, id: File.join(release.tag, archive_path), format: format)
}
end
end
end
end
class Tag < Grape::Entity
......
......@@ -255,18 +255,6 @@ 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')
......
......@@ -4,10 +4,11 @@ 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)
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! }
before { authorize_read_releases! }
params do
requires :id, type: String, desc: 'The ID of a project'
......@@ -31,11 +32,10 @@ module API
success Entities::Release
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
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
authorize_read_release!
present release, with: Entities::Release
end
......@@ -45,25 +45,22 @@ module API
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', as: :tag
requires :description, type: String, desc: 'The release notes'
optional :ref, type: String, desc: 'The commit sha or branch name'
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!
attributes = declared(params)
ref = attributes.delete(:ref)
attributes.delete(:id)
result = ::CreateReleaseService.new(user_project, current_user, attributes)
.execute(ref)
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], 400)
render_api_error!(result[:message], result[:http_status])
end
end
......@@ -73,15 +70,15 @@ module API
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: 'Release notes with markdown support'
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!
attributes = declared(params)
attributes.delete(:id)
result = UpdateReleaseService.new(user_project, current_user, attributes).execute
result = ::Releases::UpdateService
.new(user_project, current_user, declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
......@@ -98,11 +95,11 @@ module API
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_update_release!
authorize_destroy_release!
attributes = declared(params)
attributes.delete(:id)
result = DeleteReleaseService.new(user_project, current_user, attributes).execute
result = ::Releases::DestroyService
.new(user_project, current_user, declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:release], with: Entities::Release
......@@ -111,5 +108,31 @@ module API
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
......@@ -60,10 +60,15 @@ module API
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,
tag: params[:tag_name], description: params[:release_description]
).execute
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],
......@@ -107,9 +112,18 @@ module API
post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do
authorize_create_release!
attributes = declared(params)
attributes.delete(:id)
result = CreateReleaseService.new(user_project, current_user, attributes)
##
# Legacy API does not support tag auto creation.
not_found!('Tag') unless user_project.repository.find_tag(params[:tag])
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
......@@ -124,18 +138,15 @@ module API
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_update_release!
result = UpdateReleaseService.new(
user_project,
current_user,
tag: params[:tag_name],
description: params[:description]
).execute
result = ::Releases::UpdateService
.new(user_project, current_user, declared_params(include_missing: false))
.execute
if result[:status] == :success
present result[:release], with: Entities::TagRelease
......@@ -144,5 +155,19 @@ module API
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
{
"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,
......
......@@ -16,18 +16,4 @@ RSpec.describe Release do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) }
end
describe '.by_tag' do
let(:tag) { release.tag }
subject { described_class.by_tag(project, tag) }
it { is_expected.to eq(release) }
context 'when no releases exists' do
let(:tag) { 'not-existing' }
it { is_expected.to be_nil }
end
end
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 admin_release add_cluster
admin_pipeline admin_environment admin_deployment destroy_release add_cluster