Commit a524777a authored by Nathan Friend's avatar Nathan Friend Committed by Peter Leitzen

Add related links to Release GraphQL endpoint

This commit adds related links to the Release portion of our GraphQL
endpoint.
parent 678c9c1b
# frozen_string_literal: true
module Types
class ReleaseLinksType < BaseObject
graphql_name 'ReleaseLinks'
authorize :download_code
alias_method :release, :object
present_using ReleasePresenter
field :self_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the release'
field :merge_requests_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the merge request page filtered by this release'
field :issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page filtered by this release'
field :edit_url, GraphQL::STRING_TYPE, null: true,
description: "HTTP URL of the release's edit page",
authorize: :update_release
end
end
...@@ -28,6 +28,8 @@ module Types ...@@ -28,6 +28,8 @@ module Types
description: 'Timestamp of when the release was released' description: 'Timestamp of when the release was released'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself, field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release' description: 'Assets of the release'
field :links, Types::ReleaseLinksType, null: true, method: :itself,
description: 'Links of the release'
field :milestones, Types::MilestoneType.connection_type, null: true, field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release' description: 'Milestones associated to the release'
field :evidences, Types::EvidenceType.connection_type, null: true, field :evidences, Types::EvidenceType.connection_type, null: true,
......
...@@ -10161,6 +10161,11 @@ type Release { ...@@ -10161,6 +10161,11 @@ type Release {
last: Int last: Int
): ReleaseEvidenceConnection ): ReleaseEvidenceConnection
"""
Links of the release
"""
links: ReleaseLinks
""" """
Milestones associated to the release Milestones associated to the release
""" """
...@@ -10452,6 +10457,28 @@ type ReleaseEvidenceEdge { ...@@ -10452,6 +10457,28 @@ type ReleaseEvidenceEdge {
node: ReleaseEvidence node: ReleaseEvidence
} }
type ReleaseLinks {
"""
HTTP URL of the release's edit page
"""
editUrl: String
"""
HTTP URL of the issues page filtered by this release
"""
issuesUrl: String
"""
HTTP URL of the merge request page filtered by this release
"""
mergeRequestsUrl: String
"""
HTTP URL of the release
"""
selfUrl: String
}
""" """
Represents the source code attached to a release in a particular format Represents the source code attached to a release in a particular format
""" """
......
...@@ -29727,6 +29727,20 @@ ...@@ -29727,6 +29727,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "links",
"description": "Links of the release",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ReleaseLinks",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "milestones", "name": "milestones",
"description": "Milestones associated to the release", "description": "Milestones associated to the release",
...@@ -30508,6 +30522,75 @@ ...@@ -30508,6 +30522,75 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ReleaseLinks",
"description": null,
"fields": [
{
"name": "editUrl",
"description": "HTTP URL of the release's edit page",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issuesUrl",
"description": "HTTP URL of the issues page filtered by this release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequestsUrl",
"description": "HTTP URL of the merge request page filtered by this release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "selfUrl",
"description": "HTTP URL of the release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ReleaseSource", "name": "ReleaseSource",
...@@ -1410,6 +1410,7 @@ Represents a release ...@@ -1410,6 +1410,7 @@ Represents a release
| `createdAt` | Time | Timestamp of when the release was created | | `createdAt` | Time | Timestamp of when the release was created |
| `description` | String | Description (also known as "release notes") of the release | | `description` | String | Description (also known as "release notes") of the release |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `links` | ReleaseLinks | Links of the release |
| `name` | String | Name of the release | | `name` | String | Name of the release |
| `releasedAt` | Time | Timestamp of when the release was released | | `releasedAt` | Time | Timestamp of when the release was released |
| `tagName` | String | Name of the tag associated with the release | | `tagName` | String | Name of the tag associated with the release |
...@@ -1446,6 +1447,15 @@ Evidence for a release ...@@ -1446,6 +1447,15 @@ Evidence for a release
| `id` | ID! | ID of the evidence | | `id` | ID! | ID of the evidence |
| `sha` | String | SHA1 ID of the evidence hash | | `sha` | String | SHA1 ID of the evidence hash |
## ReleaseLinks
| Name | Type | Description |
| --- | ---- | ---------- |
| `editUrl` | String | HTTP URL of the release's edit page |
| `issuesUrl` | String | HTTP URL of the issues page filtered by this release |
| `mergeRequestsUrl` | String | HTTP URL of the merge request page filtered by this release |
| `selfUrl` | String | HTTP URL of the release |
## ReleaseSource ## ReleaseSource
Represents the source code attached to a release in a particular format Represents the source code attached to a release in a particular format
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['ReleaseLinks'] do
it { expect(described_class).to require_graphql_authorizations(:download_code) }
it 'has the expected fields' do
expected_fields = %w[
selfUrl
mergeRequestsUrl
issuesUrl
editUrl
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
...@@ -9,7 +9,8 @@ describe GitlabSchema.types['Release'] do ...@@ -9,7 +9,8 @@ describe GitlabSchema.types['Release'] do
expected_fields = %w[ expected_fields = %w[
tag_name tag_path tag_name tag_path
description description_html description description_html
name assets milestones evidences author commit name milestones evidences author commit
assets links
created_at released_at created_at released_at
] ]
...@@ -22,6 +23,12 @@ describe GitlabSchema.types['Release'] do ...@@ -22,6 +23,12 @@ describe GitlabSchema.types['Release'] do
it { is_expected.to have_graphql_type(Types::ReleaseAssetsType) } it { is_expected.to have_graphql_type(Types::ReleaseAssetsType) }
end end
describe 'links field' do
subject { described_class.fields['links'] }
it { is_expected.to have_graphql_type(Types::ReleaseLinksType) }
end
describe 'milestones field' do describe 'milestones field' do
subject { described_class.fields['milestones'] } subject { described_class.fields['milestones'] }
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
require 'pp'
describe 'Query.project(fullPath).release(tagName)' do describe 'Query.project(fullPath).release(tagName)' do
include GraphqlHelpers include GraphqlHelpers
...@@ -12,15 +11,19 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -12,15 +11,19 @@ describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:stranger) { create(:user) } let_it_be(:stranger) { create(:user) }
let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
let(:post_query) { post_graphql(query, current_user: current_user) }
let(:path_prefix) { %w[project release] }
let(:data) { graphql_data.dig(*path) }
def query(rq = release_fields) def query(rq = release_fields)
graphql_query_for(:project, { fullPath: project.full_path }, graphql_query_for(:project, { fullPath: project.full_path },
query_graphql_field(:release, { tagName: release.tag }, rq)) query_graphql_field(:release, { tagName: release.tag }, rq))
end end
let(:post_query) { post_graphql(query, current_user: current_user) } before do
let(:path_prefix) { %w[project release] } stub_default_url_options(host: 'www.example.com')
end
let(:data) { graphql_data.dig(*path) }
shared_examples 'full access to the release field' do shared_examples 'full access to the release field' do
describe 'scalar fields' do describe 'scalar fields' do
...@@ -83,10 +86,10 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -83,10 +86,10 @@ describe 'Query.project(fullPath).release(tagName)' do
it 'finds the author of the release' do it 'finds the author of the release' do
post_query post_query
expect(data).to eq({ expect(data).to eq(
'id' => global_id_of(release.author), 'id' => global_id_of(release.author),
'username' => release.author.username 'username' => release.author.username
}) )
end end
end end
...@@ -100,7 +103,7 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -100,7 +103,7 @@ describe 'Query.project(fullPath).release(tagName)' do
it 'finds the commit associated with the release' do it 'finds the commit associated with the release' do
post_query post_query
expect(data).to eq({ 'sha' => release.commit.sha }) expect(data).to eq('sha' => release.commit.sha)
end end
end end
...@@ -115,7 +118,7 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -115,7 +118,7 @@ describe 'Query.project(fullPath).release(tagName)' do
it 'returns the number of assets associated to the release' do it 'returns the number of assets associated to the release' do
post_query post_query
expect(data).to eq({ 'count' => release.sources.size + release.links.size }) expect(data).to eq('count' => release.sources.size + release.links.size)
end end
end end
...@@ -166,6 +169,28 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -166,6 +169,28 @@ describe 'Query.project(fullPath).release(tagName)' do
end end
end end
describe 'links' do
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
query_graphql_field(:links, nil, %{
selfUrl
mergeRequestsUrl
issuesUrl
})
end
it 'finds all release links' do
post_query
expect(data).to eq(
'selfUrl' => project_release_url(project, release),
'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
)
end
end
describe 'evidences' do describe 'evidences' do
let(:path) { path_prefix + %w[evidences] } let(:path) { path_prefix + %w[evidences] }
...@@ -177,14 +202,13 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -177,14 +202,13 @@ describe 'Query.project(fullPath).release(tagName)' do
post_query post_query
evidence = release.evidences.first.present evidence = release.evidences.first.present
expected = {
expect(data["nodes"].first).to eq(
'id' => global_id_of(evidence), 'id' => global_id_of(evidence),
'sha' => evidence.sha, 'sha' => evidence.sha,
'filepath' => evidence.filepath, 'filepath' => evidence.filepath,
'collectedAt' => evidence.collected_at.utc.iso8601 'collectedAt' => evidence.collected_at.utc.iso8601
} )
expect(data["nodes"].first).to eq(expected)
end end
end end
end end
...@@ -207,6 +231,38 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -207,6 +231,38 @@ describe 'Query.project(fullPath).release(tagName)' do
end end
end end
shared_examples 'access to editUrl' do
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
query_graphql_field(:links, nil, 'editUrl')
end
before do
post_query
end
it 'returns editUrl' do
expect(data).to eq('editUrl' => edit_project_release_url(project, release))
end
end
shared_examples 'no access to editUrl' do
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
query_graphql_field(:links, nil, 'editUrl')
end
before do
post_query
end
it 'does not return editUrl' do
expect(data).to eq('editUrl' => nil)
end
end
describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do
context 'when the project is private' do context 'when the project is private' do
let_it_be(:project) { create(:project, :repository, :private) } let_it_be(:project) { create(:project, :repository, :private) }
...@@ -238,12 +294,14 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -238,12 +294,14 @@ describe 'Query.project(fullPath).release(tagName)' do
let(:current_user) { reporter } let(:current_user) { reporter }
it_behaves_like 'full access to the release field' it_behaves_like 'full access to the release field'
it_behaves_like 'no access to editUrl'
end end
context 'when the user has Developer permissions' do context 'when the user has Developer permissions' do
let(:current_user) { developer } let(:current_user) { developer }
it_behaves_like 'full access to the release field' it_behaves_like 'full access to the release field'
it_behaves_like 'access to editUrl'
end end
end end
...@@ -265,12 +323,21 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -265,12 +323,21 @@ describe 'Query.project(fullPath).release(tagName)' do
let(:current_user) { stranger } let(:current_user) { stranger }
it_behaves_like 'full access to the release field' it_behaves_like 'full access to the release field'
it_behaves_like 'no access to editUrl'
end end
context 'when the user has Guest permissions' do context 'when the user has Guest permissions' do
let(:current_user) { guest } let(:current_user) { guest }
it_behaves_like 'full access to the release field' it_behaves_like 'full access to the release field'
it_behaves_like 'no access to editUrl'
end
context 'when the user has Reporter permissions' do
let(:current_user) { reporter }
it_behaves_like 'full access to the release field'
it_behaves_like 'no access to editUrl'
end end
context 'when the user has Reporter permissions' do context 'when the user has Reporter permissions' do
...@@ -283,6 +350,7 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -283,6 +350,7 @@ describe 'Query.project(fullPath).release(tagName)' do
let(:current_user) { developer } let(:current_user) { developer }
it_behaves_like 'full access to the release field' it_behaves_like 'full access to the release field'
it_behaves_like 'access to editUrl'
end end
end end
end end
......
...@@ -5,9 +5,10 @@ require 'spec_helper' ...@@ -5,9 +5,10 @@ require 'spec_helper'
describe 'Query.project(fullPath).releases()' do describe 'Query.project(fullPath).releases()' do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:stranger) { create(:user) }
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:stranger) { create(:user) } let_it_be(:developer) { create(:user) }
let(:query) do let(:query) do
graphql_query_for(:project, { fullPath: project.full_path }, graphql_query_for(:project, { fullPath: project.full_path },
...@@ -33,15 +34,25 @@ describe 'Query.project(fullPath).releases()' do ...@@ -33,15 +34,25 @@ describe 'Query.project(fullPath).releases()' do
sha sha
} }
} }
links {
selfUrl
mergeRequestsUrl
issuesUrl
}
} }
} }
}) })
end end
let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
let(:post_query) { post_graphql(query, current_user: current_user) } let(:post_query) { post_graphql(query, current_user: current_user) }
let(:data) { graphql_data.dig('project', 'releases', 'nodes', 0) } let(:data) { graphql_data.dig('project', 'releases', 'nodes', 0) }
before do
stub_default_url_options(host: 'www.example.com')
end
shared_examples 'full access to all repository-related fields' do shared_examples 'full access to all repository-related fields' do
describe 'repository-related fields' do describe 'repository-related fields' do
before do before do
...@@ -57,7 +68,7 @@ describe 'Query.project(fullPath).releases()' do ...@@ -57,7 +68,7 @@ describe 'Query.project(fullPath).releases()' do
{ 'sha' => e.sha } { 'sha' => e.sha }
end end
expect(data).to eq({ expect(data).to eq(
'tagName' => release.tag, 'tagName' => release.tag,
'tagPath' => project_tag_path(project, release.tag), 'tagPath' => project_tag_path(project, release.tag),
'name' => release.name, 'name' => release.name,
...@@ -72,8 +83,13 @@ describe 'Query.project(fullPath).releases()' do ...@@ -72,8 +83,13 @@ describe 'Query.project(fullPath).releases()' do
}, },
'evidences' => { 'evidences' => {
'nodes' => expected_evidences 'nodes' => expected_evidences
},
'links' => {
'selfUrl' => project_release_url(project, release),
'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
} }
}) )
end end
end end
end end
...@@ -85,7 +101,7 @@ describe 'Query.project(fullPath).releases()' do ...@@ -85,7 +101,7 @@ describe 'Query.project(fullPath).releases()' do
end end
it 'does not return data for fields that expose repository information' do it 'does not return data for fields that expose repository information' do
expect(data).to eq({ expect(data).to eq(
'tagName' => nil, 'tagName' => nil,
'tagPath' => nil, 'tagPath' => nil,
'name' => "Release-#{release.id}", 'name' => "Release-#{release.id}",
...@@ -98,9 +114,76 @@ describe 'Query.project(fullPath).releases()' do ...@@ -98,9 +114,76 @@ describe 'Query.project(fullPath).releases()' do
}, },
'evidences' => { 'evidences' => {
'nodes' => [] 'nodes' => []
},
'links' => nil
)
end
end
end
# editUrl is tested separately becuase its permissions
# are slightly different than other release fields
shared_examples 'access to editUrl' do
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
%{
releases {
nodes {
links {
editUrl
}
}
} }
}) })
end end
before do
post_query
end
it 'returns editUrl' do
expect(data).to eq(
'links' => {
'editUrl' => edit_project_release_url(project, release)
}
)
end
end
shared_examples 'no access to editUrl' do
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
%{
releases {
nodes {
links {
editUrl
}
}
}
})
end
before do
post_query
end
it 'does not return editUrl' do
expect(data).to eq(
'links' => {
'editUrl' => nil
}
)
end
end
shared_examples 'no access to any release data' do
before do
post_query
end
it 'returns nil' do
expect(data).to eq(nil)
end end
end end
...@@ -112,6 +195,13 @@ describe 'Query.project(fullPath).releases()' do ...@@ -112,6 +195,13 @@ describe 'Query.project(fullPath).releases()' do
before_all do before_all do
project.add_guest(guest) project.add_guest(guest)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_developer(developer)
end
context 'when the user is not logged in' do
let(:current_user) { stranger }
it_behaves_like 'no access to any release data'
end end
context 'when the user has Guest permissions' do context 'when the user has Guest permissions' do
...@@ -124,6 +214,14 @@ describe 'Query.project(fullPath).releases()' do ...@@ -124,6 +214,14 @@ describe 'Query.project(fullPath).releases()' do
let(:current_user) { reporter } let(:current_user) { reporter }
it_behaves_like 'full access to all repository-related fields' it_behaves_like 'full access to all repository-related fields'
it_behaves_like 'no access to editUrl'
end
context 'when the user has Developer permissions' do
let(:current_user) { developer }
it_behaves_like 'full access to all repository-related fields'
it_behaves_like 'access to editUrl'
end end
end end
...@@ -134,18 +232,35 @@ describe 'Query.project(fullPath).releases()' do ...@@ -134,18 +232,35 @@ describe 'Query.project(fullPath).releases()' do
before_all do before_all do
project.add_guest(guest) project.add_guest(guest)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_developer(developer)
end end
context 'when the user is not logged in' do context 'when the user is not logged in' do
let(:current_user) { stranger } let(:current_user) { stranger }
it_behaves_like 'full access to all repository-related fields' it_behaves_like 'full access to all repository-related fields'
it_behaves_like 'no access to editUrl'
end end
context 'when the user has Guest permissions' do context 'when the user has Guest permissions' do
let(:current_user) { guest } let(:current_user) { guest }
it_behaves_like 'full access to all repository-related fields' it_behaves_like 'full access to all repository-related fields'
it_behaves_like 'no access to editUrl'
end
context 'when the user has Reporter permissions' do
let(:current_user) { reporter }
it_behaves_like 'full access to all repository-related fields'
it_behaves_like 'no access to editUrl'
end
context 'when the user has Developer permissions' do
let(:current_user) { developer }
it_behaves_like 'full access to all repository-related fields'
it_behaves_like 'access to editUrl'
end end
end 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