Commit 66755c9e authored by Shinya Maeda's avatar Shinya Maeda

Support CURD operation for release asset links

- Add Releases::Links model
- Expose it in release API
- Add integration tests
parent b4f4edd4
......@@ -10,6 +10,10 @@ class Release < ActiveRecord::Base
# releases prior to 11.7 have no author
belongs_to :author, class_name: 'User'
has_many :links, class_name: 'Releases::Link'
accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true
scope :sorted, -> { order(created_at: :desc) }
......@@ -26,6 +30,16 @@ class Release < ActiveRecord::Base
actual_tag.nil?
end
def assets_count
links.size + sources.size
end
def sources
strong_memoize(:sources) do
Releases::Source.all(project, tag)
end
end
private
def actual_sha
......
# frozen_string_literal: true
module Releases
class Link < ActiveRecord::Base
self.table_name = 'release_links'
belongs_to :release
validates :url, presence: true, url: true
validates :name, presence: true, uniqueness: { scope: :release }
scope :sorted, -> { order(created_at: :desc) }
def internal?
url.start_with?(release.project.web_url)
end
def external?
!internal?
end
end
end
# frozen_string_literal: true
module Releases
class Source
include ActiveModel::Model
attr_accessor :project, :tag_name, :format
FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
class << self
def all(project, tag_name)
Releases::Source::FORMATS.map do |format|
Releases::Source.new(project: project,
tag_name: tag_name,
format: format)
end
end
end
def url
Gitlab::Routing
.url_helpers
.project_archive_url(project,
id: File.join(tag_name, archive_path),
format: format)
end
private
def archive_path
"#{project.path}-#{tag_name.tr('/', '-')}"
end
end
end
......@@ -43,7 +43,8 @@ module Releases
description: description,
author: current_user,
tag: tag.name,
sha: tag.dereferenced_target.sha
sha: tag.dereferenced_target.sha,
links_attributes: params[:links_attributes] || []
)
success(tag: tag, release: release)
......
---
title: Support CURD operation for Links as one of the Release assets
merge_request: 24056
author:
type: changed
# frozen_string_literal: true
class CreateReleasesLinkTable < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :release_links do |t|
t.references :release, null: false, index: false, foreign_key: { on_delete: :cascade }
t.string :url, null: false
t.string :name, null: false
t.timestamps_with_timezone null: false
t.index [:release_id, :name], unique: true
end
end
end
......@@ -1798,6 +1798,15 @@ ActiveRecord::Schema.define(version: 20190103140724) do
t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
end
create_table "release_links", force: :cascade do |t|
t.integer "release_id", null: false
t.string "url", null: false
t.string "name", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.index ["release_id", "name"], name: "index_release_links_on_release_id_and_name", unique: true, using: :btree
end
create_table "releases", force: :cascade do |t|
t.string "tag"
t.text "description"
......@@ -2439,6 +2448,7 @@ ActiveRecord::Schema.define(version: 20190103140724) do
add_foreign_key "protected_tag_create_access_levels", "users"
add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "release_links", "releases", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "releases", "users", column: "author_id", name: "fk_8e4456f90f", on_delete: :nullify
add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
......
......@@ -1093,12 +1093,34 @@ module API
expose :description
end
module Releases
class Link < Grape::Entity
expose :id
expose :name
expose :url
expose :external?, as: :external
end
class Source < Grape::Entity
expose :format
expose :url
end
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
expose :assets do
expose :assets_count, as: :count
expose :sources, using: Entities::Releases::Source
expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted
end
end
end
class Tag < Grape::Entity
......
......@@ -49,6 +49,10 @@ module API
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'
optional :links_attributes, type: Array do
requires :name, type: String
requires :url, type: String
end
end
post ':id/releases' do
authorize_create_release!
......@@ -72,6 +76,13 @@ module API
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'
optional :links_attributes, type: Array do
optional :id, type: Integer
optional :name, type: String
optional :url, type: String
optional :_destroy, type: Integer
at_least_one_of :name, :url, :_destroy
end
end
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
authorize_update_release!
......
......@@ -28,7 +28,8 @@ project_tree:
- notes:
:author
- releases:
:author
- :author
- :links
- project_members:
- :user
- merge_requests:
......
......@@ -23,7 +23,8 @@ module Gitlab
custom_attributes: 'ProjectCustomAttribute',
project_badges: 'Badge',
metrics: 'MergeRequest::Metrics',
ci_cd_settings: 'ProjectCiCdSetting' }.freeze
ci_cd_settings: 'ProjectCiCdSetting',
links: 'Releases::Link' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
......
FactoryBot.define do
factory :release_link, class: ::Releases::Link do
release
name "release-18.04.dmg"
url 'https://my-external-hosting.example.com/scrambled-url/app.zip'
end
end
......@@ -12,6 +12,24 @@
},
"author": {
"oneOf": [{ "type": "null" }, { "$ref": "public_api/v4/user/basic.json" }]
},
"assets": {
"count": { "type": "integer" },
"links": {
"type": "array",
"items": {
"name": "string",
"url": "string",
"external": "boolean"
}
},
"sources": {
"type": "array",
"items": {
"format": "zip",
"url": "string"
}
}
}
},
"additionalProperties": false
......
......@@ -66,6 +66,9 @@ snippets:
releases:
- author
- project
- links
links:
- release
project_members:
- created_by
- user
......
......@@ -121,6 +121,7 @@ describe API::Releases do
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)
expect(json_response['assets']['count']).to eq(4)
end
it 'matches response schema' do
......@@ -128,6 +129,52 @@ describe API::Releases do
expect(response).to match_response_schema('release')
end
it 'contains source information as assets' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(json_response['assets']['sources'].map { |h| h['format'] })
.to match_array(release.sources.map(&:format))
expect(json_response['assets']['sources'].map { |h| h['url'] })
.to match_array(release.sources.map(&:url))
end
context 'when release has link asset' do
let!(:link) do
create(:release_link,
release: release,
name: 'release-18.04.dmg',
url: url)
end
let(:url) { 'https://my-external-hosting.example.com/scrambled-url/app.zip' }
it 'contains link information as assets' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(json_response['assets']['links'].count).to eq(1)
expect(json_response['assets']['links'].first['name'])
.to eq('release-18.04.dmg')
expect(json_response['assets']['links'].first['url'])
.to eq('https://my-external-hosting.example.com/scrambled-url/app.zip')
expect(json_response['assets']['links'].first['external'])
.to be_truthy
end
context 'when link is internal' do
let(:url) do
"#{project.web_url}/-/jobs/artifacts/v11.6.0-rc4/download?" \
"job=rspec-mysql+41%2F50"
end
it 'has external false' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
expect(json_response['assets']['links'].first['external'])
.to be_falsy
end
end
end
end
context 'when specified tag is not found in the project' do
......@@ -254,6 +301,76 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when create assets altogether' do
context 'when create one asset' do
let(:params) do
{
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release',
links_attributes: [
{
name: 'beta',
url: 'https://dosuken.example.com/inspection.exe'
}
]
}
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 an asset with specified parameters' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(json_response['assets']['links'].count).to eq(1)
expect(json_response['assets']['links'].first['name']).to eq('beta')
expect(json_response['assets']['links'].first['url'])
.to eq('https://dosuken.example.com/inspection.exe')
end
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(response).to match_response_schema('release')
end
end
context 'when create two assets' do
let(:params) do
{
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release',
links_attributes: [
{
name: 'alpha',
url: 'https://dosuken.example.com/alpha.exe'
},
{
name: 'beta',
url: 'https://dosuken.example.com/beta.exe'
}
]
}
end
it 'creates two assets with specified parameters' do
post api("/projects/#{project.id}/releases", maintainer), params: params
expect(json_response['assets']['links'].count).to eq(2)
expect(json_response['assets']['links'].map { |h| h['name'] })
.to match_array(%w[alpha beta])
expect(json_response['assets']['links'].map { |h| h['url'] })
.to match_array(%w[https://dosuken.example.com/alpha.exe
https://dosuken.example.com/beta.exe])
end
end
end
end
context 'when tag does not exist in git repository' do
......@@ -434,6 +551,90 @@ describe API::Releases do
end
end
context 'when links_attributes param is specified' do
context 'when the release does not have any link assets' do
let(:params) do
{ links_attributes: [{ name: 'Beta release',
url: 'http://dosuken.com/win.exe' }] }
end
it 'creates an asset' do
put api("/projects/#{project.id}/releases/v0.1", maintainer),
params: params
expect(json_response['assets']['links'].count).to eq(1)
expect(json_response['assets']['links'].first['name'])
.to eq('Beta release')
expect(json_response['assets']['links'].first['url'])
.to eq('http://dosuken.com/win.exe')
end
context 'when url is invalid' do
let(:params) do
{ links_attributes: [{ name: 'Beta release',
url: 'SELECT 1 from ci_builds;' }] }
end
it 'returns an error' do
put api("/projects/#{project.id}/releases/v0.1", maintainer),
params: params
expect(json_response['message']['links.url'].first)
.to include('Only allowed protocols are http, https')
end
end
end
context 'when the release has asset links' do
let!(:release_link_1) do
create(:release_link,
name: 'gcc',
url: 'http://dosuken.com/executable-gcc',
release: release,
created_at: 1.day.ago)
end
let!(:release_link_2) do
create(:release_link,
name: 'llvm',
url: 'http://dosuken.com/executable-llvm',
release: release,
created_at: 2.days.ago)
end
context 'when updates link names' do
let(:params) do
{ links_attributes: [{ id: release_link_1.id, name: 'bin-gcc' },
{ id: release_link_2.id, name: 'bin-llvm' }] }
end
it 'updates the asset' do
put api("/projects/#{project.id}/releases/v0.1", maintainer),
params: params
expect(json_response['assets']['links'].first['name'])
.to eq('bin-gcc')
expect(json_response['assets']['links'].second['name'])
.to eq('bin-llvm')
end
end
context 'when destroys an asset' do
let(:params) do
{ links_attributes: [{ id: release_link_1.id, _destroy: '1' }] }
end
it 'updates the asset' do
put api("/projects/#{project.id}/releases/v0.1", maintainer),
params: params
expect(json_response['assets']['links'].count).to eq(1)
expect(json_response['assets']['links'].first['name']).to eq('llvm')
end
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(releases_page: false)
......
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