Commit 549bb96f authored by Nathan Friend's avatar Nathan Friend

Add GraphQL mutation to create a release

This commit adds a new GraphQL mutation - `releaseCreate` that creates a
new release.
parent 1aa7f24b
# frozen_string_literal: true
module Mutations
module Releases
class Base < BaseMutation
include ResolvesProject
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path of the project the release is associated with'
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
# frozen_string_literal: true
module Mutations
module Releases
class Create < Base
graphql_name 'ReleaseCreate'
field :release,
Types::ReleaseType,
null: true,
description: 'The release after mutation'
argument :tag_name, GraphQL::STRING_TYPE,
required: true, as: :tag,
description: 'Name of the tag to associate with the release'
argument :ref, GraphQL::STRING_TYPE,
required: false,
description: 'The commit SHA or branch name to use if creating a new tag'
argument :name, GraphQL::STRING_TYPE,
required: false,
description: 'Name of the release'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'Description (also known as "release notes") of the release'
argument :released_at, Types::TimeType,
required: false,
description: 'The date when the release will be/was ready. Defaults to the current time.'
argument :milestones, [GraphQL::STRING_TYPE],
required: false,
description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.'
argument :assets, Types::ReleaseAssetsInputType,
required: false,
description: 'Assets associated to the release'
authorize :create_release
def resolve(project_path:, milestones: nil, assets: nil, **scalars)
project = authorized_find!(full_path: project_path)
params = {
**scalars,
milestones: milestones.presence || [],
assets: assets.to_h
}.with_indifferent_access
result = ::Releases::CreateService.new(project, current_user, params).execute
if result[:status] == :success
{
release: result[:release],
errors: []
}
else
{
release: nil,
errors: [result[:message]]
}
end
end
end
end
end
......@@ -63,6 +63,7 @@ module Types
'destroyed during the update, and no Note will be returned'
mount_mutation Mutations::Notes::RepositionImageDiffNote
mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Releases::Create
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class ReleaseAssetLinkInputType < BaseInputObject
graphql_name 'ReleaseAssetLinkInput'
description 'Fields that are available when modifying a release asset link'
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the asset link'
argument :url, GraphQL::STRING_TYPE,
required: true,
description: 'URL of the asset link'
argument :direct_asset_path, GraphQL::STRING_TYPE,
required: false, as: :filepath,
description: 'Relative path for a direct asset link'
argument :link_type, Types::ReleaseAssetLinkTypeEnum,
required: false, default_value: 'other',
description: 'The type of the asset link'
end
end
......@@ -3,7 +3,7 @@
module Types
class ReleaseAssetLinkTypeEnum < BaseEnum
graphql_name 'ReleaseAssetLinkType'
description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`'
description 'Type of the link: `other`, `runbook`, `image`, `package`'
::Releases::Link.link_types.keys.each do |link_type|
value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type"
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class ReleaseAssetsInputType < BaseInputObject
graphql_name 'ReleaseAssetsInput'
description 'Fields that are available when modifying release assets'
argument :links, [Types::ReleaseAssetLinkInputType],
required: false,
description: 'A list of asset links to associate to the release'
end
end
---
title: Add releaseCreate mutation to GraphQL endpoint
merge_request: 46263
author:
type: added
......@@ -13301,6 +13301,7 @@ type Mutation {
prometheusIntegrationResetToken(input: PrometheusIntegrationResetTokenInput!): PrometheusIntegrationResetTokenPayload
prometheusIntegrationUpdate(input: PrometheusIntegrationUpdateInput!): PrometheusIntegrationUpdatePayload
promoteToEpic(input: PromoteToEpicInput!): PromoteToEpicPayload
releaseCreate(input: ReleaseCreateInput!): ReleaseCreatePayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
......@@ -17549,7 +17550,32 @@ type ReleaseAssetLinkEdge {
}
"""
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
Fields that are available when modifying a release asset link
"""
input ReleaseAssetLinkInput {
"""
Relative path for a direct asset link
"""
directAssetPath: String
"""
The type of the asset link
"""
linkType: ReleaseAssetLinkType = OTHER
"""
Name of the asset link
"""
name: String!
"""
URL of the asset link
"""
url: String!
}
"""
Type of the link: `other`, `runbook`, `image`, `package`
"""
enum ReleaseAssetLinkType {
"""
......@@ -17633,6 +17659,16 @@ type ReleaseAssets {
): ReleaseSourceConnection
}
"""
Fields that are available when modifying release assets
"""
input ReleaseAssetsInput {
"""
A list of asset links to associate to the release
"""
links: [ReleaseAssetLinkInput!]
}
"""
The connection type for Release.
"""
......@@ -17658,6 +17694,76 @@ type ReleaseConnection {
pageInfo: PageInfo!
}
"""
Autogenerated input type of ReleaseCreate
"""
input ReleaseCreateInput {
"""
Assets associated to the release
"""
assets: ReleaseAssetsInput
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Description (also known as "release notes") of the release
"""
description: String
"""
The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.
"""
milestones: [String!]
"""
Name of the release
"""
name: String
"""
Full path of the project the release is associated with
"""
projectPath: ID!
"""
The commit SHA or branch name to use if creating a new tag
"""
ref: String
"""
The date when the release will be/was ready. Defaults to the current time.
"""
releasedAt: Time
"""
Name of the tag to associate with the release
"""
tagName: String!
}
"""
Autogenerated return type of ReleaseCreate
"""
type ReleaseCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The release after mutation
"""
release: Release
}
"""
An edge in a connection.
"""
......
......@@ -38655,6 +38655,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "ReleaseCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ReleaseCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "removeAwardEmoji",
"description": null,
......@@ -50694,9 +50721,68 @@
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ReleaseAssetLinkInput",
"description": "Fields that are available when modifying a release asset link",
"fields": null,
"inputFields": [
{
"name": "name",
"description": "Name of the asset link",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "url",
"description": "URL of the asset link",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "directAssetPath",
"description": "Relative path for a direct asset link",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "linkType",
"description": "The type of the asset link",
"type": {
"kind": "ENUM",
"name": "ReleaseAssetLinkType",
"description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`",
"ofType": null
},
"defaultValue": "OTHER"
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ReleaseAssetLinkType",
"description": "Type of the link: `other`, `runbook`, `image`, `package`",
"fields": null,
"inputFields": null,
"interfaces": null,
......@@ -50861,6 +50947,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ReleaseAssetsInput",
"description": "Fields that are available when modifying release assets",
"fields": null,
"inputFields": [
{
"name": "links",
"description": "A list of asset links to associate to the release",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "ReleaseAssetLinkInput",
"ofType": null
}
}
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseConnection",
......@@ -50946,6 +51061,190 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ReleaseCreateInput",
"description": "Autogenerated input type of ReleaseCreate",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "Full path of the project the release is associated with",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "tagName",
"description": "Name of the tag to associate with the release",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "ref",
"description": "The commit SHA or branch name to use if creating a new tag",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "name",
"description": "Name of the release",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "description",
"description": "Description (also known as \"release notes\") of the release",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "releasedAt",
"description": "The date when the release will be/was ready. Defaults to the current time.",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "milestones",
"description": "The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "assets",
"description": "Assets associated to the release",
"type": {
"kind": "INPUT_OBJECT",
"name": "ReleaseAssetsInput",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseCreatePayload",
"description": "Autogenerated return type of ReleaseCreate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "release",
"description": "The release after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Release",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseEdge",
......@@ -2462,6 +2462,16 @@ A container for all assets associated with a release.
| `links` | ReleaseAssetLinkConnection | Asset links of the release |
| `sources` | ReleaseSourceConnection | Sources of the release |
### ReleaseCreatePayload
Autogenerated return type of ReleaseCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `release` | Release | The release after mutation |
### ReleaseEvidence
Evidence for a release.
......@@ -4063,7 +4073,7 @@ State of a Geo registry.
### ReleaseAssetLinkType
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`.
Type of the link: `other`, `runbook`, `image`, `package`.
| Value | Description |
| ----- | ----------- |
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Creation of a new release' do
include GraphqlHelpers
include Presentable
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, :repository, group: group) }
let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
let_it_be(:group_milestone) { create(:milestone, group: group, title: '13.1') }
let_it_be(:developer) { create(:user) }
let(:mutation_name) { :release_create }
let(:tag_name) { 'v7.12.5'}
let(:ref) { 'master'}
let(:milestones) { [milestone_12_3.title, milestone_12_4.title, group_milestone.title] }
let(:mutation_arguments) do
{
projectPath: project.full_path,
tagName: tag_name,
ref: ref,
milestones: milestones
}
end
let(:mutation) do
graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
release {
milestones {
nodes {
title
}
}
}
errors
FIELDS
end
let(:create_release) { post_graphql_mutation(mutation, current_user: developer) }
let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
before do
project.add_developer(developer)
end
context 'when the provided milestones include a group milestone' do
context 'when the group milestone association feature is licensed' do
before do
stub_licensed_features(group_milestone_project_releases: true)
end
it 'returns no errors' do
create_release
expect(graphql_errors).not_to be_present
end
it 'creates a release with both project and group milestone associations' do
create_release
returned_milestone_titles = mutation_response.dig(:release, :milestones, :nodes)
.map { |m| m[:title] }
# Right now the milestones are returned in a non-deterministic order.
# This `milestones` test should be moved up into the expect(release)
# above (and `.to include` updated to `.to eq`) once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(returned_milestone_titles).to match_array([
milestone_12_3.title,
milestone_12_4.title,
group_milestone.title
])
end
end
context 'when the group milestone association feature is not licensed' do
before do
stub_licensed_features(group_milestone_project_releases: false)
end
it 'returns an error-as-data field with a message about an invalid license' do
create_release
expect(mutation_response[:release]).to be_nil
expect(mutation_response[:errors].count).to eq(1)
expect(mutation_response[:errors].first).to match('Validation failed: Milestone releases is invalid, Milestone releases None of the group milestones have the same project as the release,,')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Releases::Create do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
let(:tag) { 'v1.1.0'}
let(:ref) { 'master'}
let(:name) { 'Version 1.0'}
let(:description) { 'The first release :rocket:' }
let(:released_at) { Time.parse('2018-12-10') }
let(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
let(:assets) do
{
links: [
{
name: 'An asset link',
url: 'https://gitlab.example.com/link',
filepath: '/permanent/link',
link_type: 'other'
}
]
}
end
let(:mutation_arguments) do
{
project_path: project.full_path,
tag: tag,
ref: ref,
name: name,
description: description,
released_at: released_at,
milestones: milestones,
assets: assets
}
end
around do |example|
freeze_time { example.run }
end
before do
project.add_reporter(reporter)
project.add_developer(developer)
end
describe '#resolve' do
subject(:resolve) do
mutation.resolve(**mutation_arguments)
end
let(:new_release) { subject[:release] }
context 'when the current user has access to create releases' do
let(:current_user) { developer }
it 'returns no errors' do
expect(resolve).to include(errors: [])
end
it 'creates the release with the correct tag' do
expect(new_release.tag).to eq(tag)
end
it 'creates the release with the correct name' do
expect(new_release.name).to eq(name)
end
it 'creates the release with the correct description' do
expect(new_release.description).to eq(description)
end
it 'creates the release with the correct released_at' do
expect(new_release.released_at).to eq(released_at)
end
it 'creates the release with the correct created_at' do
expect(new_release.created_at).to eq(Time.current)
end
it 'creates the release with the correct milestone associations' do
expected_milestone_titles = [milestone_12_3.title, milestone_12_4.title]
actual_milestone_titles = new_release.milestones.map { |m| m.title }
# Right now the milestones are returned in a non-deterministic order.
# `match_array` should be updated to `eq` once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(actual_milestone_titles).to match_array(expected_milestone_titles)
end
describe 'asset links' do
let(:expected_link) { assets[:links].first }
let(:new_link) { new_release.links.first }
it 'creates a single asset link' do
expect(new_release.links.size).to eq(1)
end
it 'creates the link with the correct name' do
expect(new_link.name).to eq(expected_link[:name])
end
it 'creates the link with the correct url' do
expect(new_link.url).to eq(expected_link[:url])
end
it 'creates the link with the correct link type' do
expect(new_link.link_type).to eq(expected_link[:link_type])
end
it 'creates the link with the correct direct filepath' do
expect(new_link.filepath).to eq(expected_link[:filepath])
end
end
end
context "when the current user doesn't have access to create releases" do
let(:current_user) { reporter }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::ReleaseAssetLinkInputType do
specify { expect(described_class.graphql_name).to eq('ReleaseAssetLinkInput') }
it 'has the correct arguments' do
expect(described_class.arguments.keys).to match_array(%w[name url directAssetPath linkType])
end
it 'sets the type of link_type argument to ReleaseAssetLinkTypeEnum' do
expect(described_class.arguments['linkType'].type).to eq(Types::ReleaseAssetLinkTypeEnum)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::ReleaseAssetsInputType do
specify { expect(described_class.graphql_name).to eq('ReleaseAssetsInput') }
it 'has the correct arguments' do
expect(described_class.arguments.keys).to match_array(%w[links])
end
it 'sets the type of links argument to ReleaseAssetLinkInputType' do
expect(described_class.arguments['links'].type.of_type.of_type).to eq(Types::ReleaseAssetLinkInputType)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Creation of a new release' do
include GraphqlHelpers
include Presentable
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
let_it_be(:public_user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let(:mutation_name) { :release_create }
let(:tag_name) { 'v7.12.5'}
let(:ref) { 'master'}
let(:name) { 'Version 7.12.5'}
let(:description) { 'Release 7.12.5 :rocket:' }
let(:released_at) { '2018-12-10' }
let(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
let(:asset_link) { { name: 'An asset link', url: 'https://gitlab.example.com/link', directAssetPath: '/permanent/link', linkType: 'OTHER' } }
let(:assets) { { links: [asset_link] } }
let(:mutation_arguments) do
{
projectPath: project.full_path,
tagName: tag_name,
ref: ref,
name: name,
description: description,
releasedAt: released_at,
milestones: milestones,
assets: assets
}
end
let(:mutation) do
graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
release {
tagName
name
description
releasedAt
createdAt
milestones {
nodes {
title
}
}
assets {
links {
nodes {
name
url
linkType
external
directAssetUrl
}
}
}
}
errors
FIELDS
end
let(:create_release) { post_graphql_mutation(mutation, current_user: current_user) }
let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
around do |example|
freeze_time { example.run }
end
before do
project.add_guest(guest)
project.add_reporter(reporter)
project.add_developer(developer)
stub_default_url_options(host: 'www.example.com')
end
shared_examples 'no errors' do
it 'returns no errors' do
create_release
expect(graphql_errors).not_to be_present
end
end
shared_examples 'top-level error with message' do |error_message|
it 'returns a top-level error with message' do
create_release
expect(mutation_response).to be_nil
expect(graphql_errors.count).to eq(1)
expect(graphql_errors.first['message']).to eq(error_message)
end
end
shared_examples 'errors-as-data with message' do |error_message|
it 'returns an error-as-data with message' do
create_release
expect(mutation_response[:release]).to be_nil
expect(mutation_response[:errors].count).to eq(1)
expect(mutation_response[:errors].first).to match(error_message)
end
end
context 'when the current user has access to create releases' do
let(:current_user) { developer }
context 'when all available mutation arguments are provided' do
it_behaves_like 'no errors'
# rubocop: disable CodeReuse/ActiveRecord
it 'returns the new release data' do
create_release
release = mutation_response[:release]
expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << asset_link[:directAssetPath]
expected_attributes = {
tagName: tag_name,
name: name,
description: description,
releasedAt: Time.parse(released_at).utc.iso8601,
createdAt: Time.current.utc.iso8601,
assets: {
links: {
nodes: [{
name: asset_link[:name],
url: asset_link[:url],
linkType: asset_link[:linkType],
external: true,
directAssetUrl: expected_direct_asset_url
}]
}
}
}
expect(release).to include(expected_attributes)
# Right now the milestones are returned in a non-deterministic order.
# This `milestones` test should be moved up into the expect(release)
# above (and `.to include` updated to `.to eq`) once
# https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
expect(release['milestones']['nodes']).to match_array([
{ 'title' => '12.4' },
{ 'title' => '12.3' }
])
end
# rubocop: enable CodeReuse/ActiveRecord
end
context 'when only the required mutation arguments are provided' do
let(:mutation_arguments) { super().slice(:projectPath, :tagName, :ref) }
it_behaves_like 'no errors'
it 'returns the new release data' do
create_release
expected_response = {
tagName: tag_name,
name: tag_name,
description: nil,
releasedAt: Time.current.utc.iso8601,
createdAt: Time.current.utc.iso8601,
milestones: {
nodes: []
},
assets: {
links: {
nodes: []
}
}
}.with_indifferent_access
expect(mutation_response[:release]).to eq(expected_response)
end
end
context 'when the provided tag already exists' do
let(:tag_name) { 'v1.1.0' }
it_behaves_like 'no errors'
it 'does not create a new tag' do
expect { create_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
end
end
context 'when the provided tag does not already exist' do
let(:tag_name) { 'v7.12.5-alpha' }
it_behaves_like 'no errors'
it 'creates a new tag' do
expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1)
end
end
context 'when a local timezone is provided for releasedAt' do
let(:released_at) { Time.parse(super()).in_time_zone('Hawaii').iso8601 }
it_behaves_like 'no errors'
it 'returns the correct releasedAt date in UTC' do
create_release
expect(mutation_response[:release]).to include({ releasedAt: Time.parse(released_at).utc.iso8601 })
end
end
context 'when no releasedAt is provided' do
let(:mutation_arguments) { super().except(:releasedAt) }
it_behaves_like 'no errors'
it 'sets releasedAt to the current time' do
create_release
expect(mutation_response[:release]).to include({ releasedAt: Time.current.utc.iso8601 })
end
end
context "when a release asset doesn't include an explicit linkType" do
let(:asset_link) { super().except(:linkType) }
it_behaves_like 'no errors'
it 'defaults the linkType to OTHER' do
create_release
returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :linkType)
expect(returned_asset_link_type).to eq('OTHER')
end
end
context "when a release asset doesn't include a directAssetPath" do
let(:asset_link) { super().except(:directAssetPath) }
it_behaves_like 'no errors'
it 'returns the provided url as the directAssetUrl' do
create_release
returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :directAssetUrl)
expect(returned_asset_link_type).to eq(asset_link[:url])
end
end
context 'empty milestones' do
shared_examples 'no associated milestones' do
it_behaves_like 'no errors'
it 'creates a release with no associated milestones' do
create_release
returned_milestones = mutation_response.dig(:release, :milestones, :nodes)
expect(returned_milestones.count).to eq(0)
end
end
context 'when the milestones parameter is not provided' do
let(:mutation_arguments) { super().except(:milestones) }
it_behaves_like 'no associated milestones'
end
context 'when the milestones parameter is null' do
let(:milestones) { nil }
it_behaves_like 'no associated milestones'
end
context 'when the milestones parameter is an empty array' do
let(:milestones) { [] }
it_behaves_like 'no associated milestones'
end
end
context 'validation' do
context 'when a release is already associated to the specified tag' do
before do
create(:release, project: project, tag: tag_name)
end
it_behaves_like 'errors-as-data with message', 'Release already exists'
end
context "when a provided milestone doesn\'t exist" do
let(:milestones) { ['a fake milestone'] }
it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: a fake milestone'
end
context "when a provided milestone belongs to a different project than the release" do
let(:milestone_in_different_project) { create(:milestone, title: 'different milestone') }
let(:milestones) { [milestone_in_different_project.title] }
it_behaves_like 'errors-as-data with message', "Milestone(s) not found: different milestone"
end
context 'when two release assets share the same name' do
let(:asset_link_1) { { name: 'My link', url: 'https://example.com/1' } }
let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } }
let(:assets) { { links: [asset_link_1, asset_link_2] } }
# Right now the raw Postgres error message is sent to the user as the validation message.
# We should catch this validation error and return a nicer message:
# https://gitlab.com/gitlab-org/gitlab/-/issues/277087
it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
end
context 'when two release assets share the same URL' do
let(:asset_link_1) { { name: 'My first link', url: 'https://example.com' } }
let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } }
let(:assets) { { links: [asset_link_1, asset_link_2] } }
# Same note as above about the ugly error message
it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
end
context 'when the provided tag name is HEAD' do
let(:tag_name) { 'HEAD' }
it_behaves_like 'errors-as-data with message', 'Tag name invalid'
end
context 'when the provided tag name is empty' do
let(:tag_name) { '' }
it_behaves_like 'errors-as-data with message', 'Tag name invalid'
end
context "when the provided tag doesn't already exist, and no ref parameter was provided" do
let(:ref) { nil }
let(:tag_name) { 'v7.12.5-beta' }
it_behaves_like 'errors-as-data with message', 'Ref is not specified'
end
end
end
context "when the current user doesn't have access to create releases" do
expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
context 'when the current user is a Reporter' do
let(:current_user) { reporter }
it_behaves_like 'top-level error with message', expected_error_message
end
context 'when the current user is a Guest' do
let(:current_user) { guest }
it_behaves_like 'top-level error with message', expected_error_message
end
context 'when the current user is a public user' do
let(:current_user) { public_user }
it_behaves_like 'top-level error with message', expected_error_message
end
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