diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /spec/requests/api/graphql/project | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) | |
download | gitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/requests/api/graphql/project')
10 files changed, 1291 insertions, 107 deletions
diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb new file mode 100644 index 00000000000..ffd328429ef --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'getting Alert Management Alert counts by status' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) } + let_it_be(:alert_2) { create(:alert_management_alert, project: project) } + let_it_be(:other_project_alert) { create(:alert_management_alert) } + let(:params) { {} } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('AlertManagementAlertStatusCountsType'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementAlertStatusCounts', params, fields) + ) + end + + context 'with alert data' do + let(:alert_counts) { graphql_data.dig('project', 'alertManagementAlertStatusCounts') } + + context 'without project permissions' do + let(:user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + it { expect(alert_counts).to be nil } + end + + context 'with project permissions' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + it 'returns the correct counts for each status' do + expect(alert_counts).to eq( + 'open' => 1, + 'all' => 2, + 'triggered' => 1, + 'acknowledged' => 0, + 'resolved' => 1, + 'ignored' => 0 + ) + end + end + end +end diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb new file mode 100644 index 00000000000..c226e659364 --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'getting Alert Management Alerts' do + include GraphqlHelpers + + let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) } + let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) } + let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) } + let(:params) { {} } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('AlertManagementAlert'.classify)} + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementAlerts', params, fields) + ) + end + + context 'with alert data' do + let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') } + + context 'without project permissions' do + let(:user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it { expect(alerts).to be nil } + end + + context 'with project permissions' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + let(:first_alert) { alerts.first } + let(:second_alert) { alerts.second } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(2) } + + it 'returns the correct properties of the alerts' do + expect(first_alert).to include( + 'iid' => triggered_alert.iid.to_s, + 'issueIid' => triggered_alert.issue_iid.to_s, + 'title' => triggered_alert.title, + 'description' => triggered_alert.description, + 'severity' => triggered_alert.severity.upcase, + 'status' => 'TRIGGERED', + 'monitoringTool' => triggered_alert.monitoring_tool, + 'service' => triggered_alert.service, + 'hosts' => triggered_alert.hosts, + 'eventCount' => triggered_alert.events, + 'startedAt' => triggered_alert.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'endedAt' => nil, + 'details' => { 'custom.alert' => 'payload' }, + 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') + ) + + expect(second_alert).to include( + 'iid' => resolved_alert.iid.to_s, + 'issueIid' => nil, + 'status' => 'RESOLVED', + 'endedAt' => resolved_alert.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ') + ) + end + + context 'with iid given' do + let(:params) { { iid: resolved_alert.iid.to_s } } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(1) } + it { expect(first_alert['iid']).to eq(resolved_alert.iid.to_s) } + end + + context 'with statuses given' do + let(:params) { 'statuses: [TRIGGERED, ACKNOWLEDGED]' } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(1) } + it { expect(first_alert['iid']).to eq(triggered_alert.iid.to_s) } + end + + context 'sorting data given' do + let(:params) { 'sort: SEVERITY_DESC' } + let(:iids) { alerts.map { |a| a['iid'] } } + + it_behaves_like 'a working graphql query' + + it 'sorts in the correct order' do + expect(iids).to eq [resolved_alert.iid.to_s, triggered_alert.iid.to_s] + end + + context 'ascending order' do + let(:params) { 'sort: SEVERITY_ASC' } + + it 'sorts in the correct order' do + expect(iids).to eq [triggered_alert.iid.to_s, resolved_alert.iid.to_s] + end + end + end + + context 'searching' do + let(:params) { { search: resolved_alert.title } } + + it_behaves_like 'a working graphql query' + + it { expect(alerts.size).to eq(1) } + it { expect(first_alert['iid']).to eq(resolved_alert.iid.to_s) } + + context 'unknown criteria' do + let(:params) { { search: 'something random' } } + + it { expect(alerts.size).to eq(0) } + end + end + end + end +end diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb index e7155934b3a..c9bc6c1a68e 100644 --- a/spec/requests/api/graphql/project/grafana_integration_spec.rb +++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb @@ -35,7 +35,7 @@ describe 'Getting Grafana Integration' do it_behaves_like 'a working graphql query' - it { expect(integration_data).to be nil } + specify { expect(integration_data).to be nil } end context 'with project admin permissions' do @@ -45,16 +45,16 @@ describe 'Getting Grafana Integration' do it_behaves_like 'a working graphql query' - it { expect(integration_data['token']).to eql grafana_integration.masked_token } - it { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url } + specify { expect(integration_data['token']).to eql grafana_integration.masked_token } + specify { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url } - it do + specify do expect( integration_data['createdAt'] ).to eql grafana_integration.created_at.strftime('%Y-%m-%dT%H:%M:%SZ') end - it do + specify do expect( integration_data['updatedAt'] ).to eql grafana_integration.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb new file mode 100644 index 00000000000..04f445b4318 --- /dev/null +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let_it_be(:stranger) { create(:user) } + let_it_be(:old_version) do + create(:design_version, issue: issue, + created_designs: create_list(:design, 3, issue: issue)) + end + let_it_be(:version) do + create(:design_version, issue: issue, + modified_designs: old_version.designs, + created_designs: create_list(:design, 2, issue: issue)) + end + + let(:current_user) { developer } + + def query(vq = version_fields) + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field(:issue, { iid: issue.iid.to_s }, + query_graphql_field(:design_collection, nil, + query_graphql_field(:version, { sha: version.sha }, vq)))) + end + + let(:post_query) { post_graphql(query, current_user: current_user) } + let(:path_prefix) { %w[project issue designCollection version] } + + let(:data) { graphql_data.dig(*path) } + + before do + enable_design_management + project.add_developer(developer) + end + + describe 'scalar fields' do + let(:path) { path_prefix } + let(:version_fields) { query_graphql_field(:sha) } + + before do + post_query + end + + { id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value| + describe ".#{field}" do + let(:version_fields) { query_graphql_field(field) } + + it "retrieves the #{field}" do + expect(data).to match(a_hash_including(field.to_s => value[version])) + end + end + end + end + + describe 'design_at_version' do + let(:path) { path_prefix + %w[designAtVersion] } + let(:design) { issue.designs.visible_at_version(version).to_a.sample } + let(:design_at_version) { build(:design_at_version, design: design, version: version) } + + let(:version_fields) do + query_graphql_field(:design_at_version, dav_params, 'id filename') + end + + shared_examples :finds_dav do + it 'finds all the designs as of the given version' do + post_query + + expect(data).to match( + a_hash_including( + 'id' => global_id_of(design_at_version), + 'filename' => design.filename + )) + end + + context 'when the current_user is not authorized' do + let(:current_user) { stranger } + + it 'returns nil' do + post_query + + expect(data).to be_nil + end + end + end + + context 'by ID' do + let(:dav_params) { { id: global_id_of(design_at_version) } } + + include_examples :finds_dav + end + + context 'by filename' do + let(:dav_params) { { filename: design.filename } } + + include_examples :finds_dav + end + + context 'by design_id' do + let(:dav_params) { { design_id: global_id_of(design) } } + + include_examples :finds_dav + end + end + + describe 'designs_at_version' do + let(:path) { path_prefix + %w[designsAtVersion edges] } + let(:version_fields) do + query_graphql_field(:designs_at_version, dav_params, 'edges { node { id filename } }') + end + + let(:dav_params) { nil } + + let(:results) do + issue.designs.visible_at_version(version).map do |d| + dav = build(:design_at_version, design: d, version: version) + { 'id' => global_id_of(dav), 'filename' => d.filename } + end + end + + it 'finds all the designs as of the given version' do + post_query + + expect(data.pluck('node')).to match_array(results) + end + + describe 'filtering' do + let(:designs) { issue.designs.sample(3) } + let(:filenames) { designs.map(&:filename) } + let(:ids) do + designs.map { |d| global_id_of(build(:design_at_version, design: d, version: version)) } + end + + before do + post_query + end + + describe 'by filename' do + let(:dav_params) { { filenames: filenames } } + + it 'finds the designs by filename' do + expect(data.map { |e| e.dig('node', 'id') }).to match_array(ids) + end + end + + describe 'by design-id' do + let(:dav_params) { { ids: designs.map { |d| global_id_of(d) } } } + + it 'finds the designs by id' do + expect(data.map { |e| e.dig('node', 'filename') }).to match_array(filenames) + end + end + end + + describe 'pagination' do + let(:end_cursor) { graphql_data_at(*path_prefix, :designs_at_version, :page_info, :end_cursor) } + + let(:ids) do + ::DesignManagement::Design.visible_at_version(version).order(:id).map do |d| + global_id_of(build(:design_at_version, design: d, version: version)) + end + end + + let(:version_fields) do + query_graphql_field(:designs_at_version, { first: 2 }, fields) + end + + let(:cursored_query) do + frag = query_graphql_field(:designs_at_version, { after: end_cursor }, fields) + query(frag) + end + + let(:fields) { ['pageInfo { endCursor }', 'edges { node { id } }'] } + + def response_values(data = graphql_data) + data.dig(*path).map { |e| e.dig('node', 'id') } + end + + it 'sorts designs for reliable pagination' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_values(new_data)).to match_array(ids.drop(2)) + end + end + end + + describe 'designs' do + let(:path) { path_prefix + %w[designs edges] } + let(:version_fields) do + query_graphql_field(:designs, nil, 'edges { node { id filename } }') + end + + let(:results) do + version.designs.map do |design| + { 'id' => global_id_of(design), 'filename' => design.filename } + end + end + + it 'finds all the designs as of the given version' do + post_query + + expect(data.pluck('node')).to match_array(results) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb new file mode 100644 index 00000000000..18787bf925d --- /dev/null +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting versions related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + + let_it_be(:version_a) do + create(:design_version, issue: issue) + end + let_it_be(:version_b) do + create(:design_version, issue: issue) + end + let_it_be(:version_c) do + create(:design_version, issue: issue) + end + let_it_be(:version_d) do + create(:design_version, issue: issue) + end + + let_it_be(:owner) { issue.project.owner } + + def version_query(params = version_params) + query_graphql_field(:versions, params, version_query_fields) + end + + let(:version_params) { nil } + + let(:version_query_fields) { ['edges { node { sha } }'] } + + let(:project) { issue.project } + let(:current_user) { owner } + + let(:query) { make_query } + + def make_query(vq = version_query) + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field(:issue, { iid: issue.iid.to_s }, + query_graphql_field(:design_collection, {}, vq))) + end + + let(:design_collection) do + graphql_data_at(:project, :issue, :design_collection) + end + + def response_values(data = graphql_data, key = 'sha') + path = %w[project issue designCollection versions edges] + data.dig(*path).map { |e| e.dig('node', key) } + end + + before do + enable_design_management + end + + it 'returns the design filename' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha)) + end + + describe 'filter by sha' do + let(:sha) { version_b.sha } + + let(:version_params) { { earlier_or_equal_to_sha: sha } } + + it 'finds only those versions at or before the given cut-off' do + post_graphql(query, current_user: current_user) + + expect(response_values).to contain_exactly(version_a.sha, version_b.sha) + end + end + + describe 'filter by id' do + let(:id) { global_id_of(version_c) } + + let(:version_params) { { earlier_or_equal_to_id: id } } + + it 'finds only those versions at or before the given cut-off' do + post_graphql(query, current_user: current_user) + + expect(response_values).to contain_exactly(version_a.sha, version_b.sha, version_c.sha) + end + end + + describe 'pagination' do + let(:end_cursor) { design_collection.dig('versions', 'pageInfo', 'endCursor') } + + let(:ids) { issue.design_collection.versions.ordered.map(&:sha) } + + let(:query) { make_query(version_query(first: 2)) } + + let(:cursored_query) do + make_query(version_query(after: end_cursor)) + end + + let(:version_query_fields) { ['pageInfo { endCursor }', 'edges { node { sha } }'] } + + it 'sorts designs for reliable pagination' do + post_graphql(query, current_user: current_user) + + expect(response_values).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_values(new_data)).to match_array(ids.drop(2)) + end + end +end diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb new file mode 100644 index 00000000000..b6fd0d91bda --- /dev/null +++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb @@ -0,0 +1,388 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting designs related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:design) { create(:design, :with_smaller_image_versions, versions_count: 1) } + let_it_be(:current_user) { design.project.owner } + let(:design_query) do + <<~NODE + designs { + edges { + node { + id + filename + fullPath + event + image + imageV432x230 + } + } + } + NODE + end + let(:issue) { design.issue } + let(:project) { issue.project } + let(:query) { make_query } + let(:design_collection) do + graphql_data_at(:project, :issue, :design_collection) + end + let(:design_response) do + design_collection.dig('designs', 'edges').first['node'] + end + + def make_query(dq = design_query) + designs_field = query_graphql_field(:design_collection, {}, dq) + issue_field = query_graphql_field(:issue, { iid: issue.iid.to_s }, designs_field) + + graphql_query_for(:project, { fullPath: project.full_path }, issue_field) + end + + def design_image_url(design, ref: nil, size: nil) + Gitlab::UrlBuilder.build(design, ref: ref, size: size) + end + + context 'when the feature is available' do + before do + enable_design_management + end + + it 'returns the design properties correctly' do + version_sha = design.versions.first.sha + + post_graphql(query, current_user: current_user) + + expect(design_response).to eq( + 'id' => design.to_global_id.to_s, + 'event' => 'CREATION', + 'fullPath' => design.full_path, + 'filename' => design.filename, + 'image' => design_image_url(design, ref: version_sha), + 'imageV432x230' => design_image_url(design, ref: version_sha, size: :v432x230) + ) + end + + context 'when the v432x230-sized design image has not been processed' do + before do + allow_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader| + allow(uploader).to receive(:file).and_return(nil) + end + end + + it 'returns nil for the v432x230-sized design image' do + post_graphql(query, current_user: current_user) + + expect(design_response['imageV432x230']).to be_nil + end + end + + describe 'pagination' do + before do + create_list(:design, 5, :with_file, issue: issue) + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + let(:issue) { create(:issue) } + + let(:end_cursor) { design_collection.dig('designs', 'pageInfo', 'endCursor') } + + let(:ids) { issue.designs.order(:id).map { |d| global_id_of(d) } } + + let(:query) { make_query(designs_fragment(first: 2)) } + + let(:design_query_fields) { 'pageInfo { endCursor } edges { node { id } }' } + + let(:cursored_query) do + make_query(designs_fragment(after: end_cursor)) + end + + def designs_fragment(params) + query_graphql_field(:designs, params, design_query_fields) + end + + def response_ids(data = graphql_data) + path = %w[project issue designCollection designs edges] + data.dig(*path).map { |e| e.dig('node', 'id') } + end + + it 'sorts designs for reliable pagination' do + expect(response_ids).to match_array(ids.take(2)) + + post_graphql(cursored_query, current_user: current_user) + + new_data = Gitlab::Json.parse(response.body).fetch('data') + + expect(response_ids(new_data)).to match_array(ids.drop(2)) + end + end + + context 'with versions' do + let_it_be(:version) { design.versions.take } + let(:design_query) do + <<~NODE + designs { + edges { + node { + filename + versions { + edges { + node { + id + sha + } + } + } + } + } + } + NODE + end + + it 'includes the version id' do + post_graphql(query, current_user: current_user) + + version_id = design_response['versions']['edges'].first['node']['id'] + + expect(version_id).to eq(version.to_global_id.to_s) + end + + it 'includes the version sha' do + post_graphql(query, current_user: current_user) + + version_sha = design_response['versions']['edges'].first['node']['sha'] + + expect(version_sha).to eq(version.sha) + end + end + + describe 'viewing a design board at a particular version' do + let_it_be(:issue) { design.issue } + let_it_be(:second_design, reload: true) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 1) } + let_it_be(:deleted_design) { create(:design, :with_versions, issue: issue, deleted: true, versions_count: 1) } + let(:all_versions) { issue.design_versions.ordered.reverse } + let(:design_query) do + <<~NODE + designs(atVersion: "#{version.to_global_id}") { + edges { + node { + id + image + imageV432x230 + event + versions { + edges { + node { + id + } + } + } + } + } + } + NODE + end + let(:design_response) do + design_collection['designs']['edges'] + end + + def global_id(object) + object.to_global_id.to_s + end + + # Filters just design nodes from the larger `design_response` + def design_nodes + design_response.map do |response| + response['node'] + end + end + + # Filters just version nodes from the larger `design_response` + def version_nodes + design_response.map do |response| + response.dig('node', 'versions', 'edges') + end + end + + context 'viewing the original version, when one design was created' do + let(:version) { all_versions.first } + + before do + post_graphql(query, current_user: current_user) + end + + it 'only returns the first design' do + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)) + ) + end + + it 'returns the correct full-sized design image' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design image' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct event for the design in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'CREATION') + ) + end + + it 'only returns one version record for the design (the original version)' do + expect(version_nodes).to eq([ + [{ 'node' => { 'id' => global_id(version) } }] + ]) + end + end + + context 'viewing the second version, when one design was created' do + let(:version) { all_versions.second } + + before do + post_graphql(query, current_user: current_user) + end + + it 'only returns the first two designs' do + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)), + a_hash_including('id' => global_id(second_design)) + ) + end + + it 'returns the correct full-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)), + a_hash_including('image' => design_image_url(second_design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)), + a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct events for the designs in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'NONE'), + a_hash_including('event' => 'CREATION') + ) + end + + it 'returns the correct versions records for both designs' do + expect(version_nodes).to eq([ + [{ 'node' => { 'id' => global_id(design.versions.first) } }], + [{ 'node' => { 'id' => global_id(second_design.versions.first) } }] + ]) + end + end + + context 'viewing the last version, when one design was deleted and one was updated' do + let(:version) { all_versions.last } + let!(:second_design_update) do + create(:design_action, :with_image_v432x230, design: second_design, version: version, event: 'modification') + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'does not include the deleted design' do + # The design does exist in the version + expect(version.designs).to include(deleted_design) + + # But the GraphQL API does not include it in these results + expect(design_nodes).to contain_exactly( + a_hash_including('id' => global_id(design)), + a_hash_including('id' => global_id(second_design)) + ) + end + + it 'returns the correct full-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('image' => design_image_url(design, ref: version.sha)), + a_hash_including('image' => design_image_url(second_design, ref: version.sha)) + ) + end + + it 'returns the correct v432x230-sized design images' do + expect(design_nodes).to contain_exactly( + a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)), + a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230)) + ) + end + + it 'returns the correct events for the designs in this version' do + expect(design_nodes).to contain_exactly( + a_hash_including('event' => 'NONE'), + a_hash_including('event' => 'MODIFICATION') + ) + end + + it 'returns all versions records for the designs' do + expect(version_nodes).to eq([ + [ + { 'node' => { 'id' => global_id(design.versions.first) } } + ], + [ + { 'node' => { 'id' => global_id(second_design.versions.second) } }, + { 'node' => { 'id' => global_id(second_design.versions.first) } } + ] + ]) + end + end + end + + describe 'a design with note annotations' do + let_it_be(:note) { create(:diff_note_on_design, noteable: design) } + + let(:design_query) do + <<~NODE + designs { + edges { + node { + notesCount + notes { + edges { + node { + id + } + } + } + } + } + } + NODE + end + + let(:design_response) do + design_collection['designs']['edges'].first['node'] + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'returns the notes for the design' do + expect(design_response.dig('notes', 'edges')).to eq( + ['node' => { 'id' => note.to_global_id.to_s }] + ) + end + + it 'returns a note_count for the design' do + expect(design_response['notesCount']).to eq(1) + end + end + end +end diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb new file mode 100644 index 00000000000..0207bb9123a --- /dev/null +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Getting designs related to an issue' do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:design) { create(:design, :with_file, versions_count: 1, issue: issue) } + let_it_be(:current_user) { project.owner } + let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project) } + + before do + enable_design_management + + note + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'is not too deep for anonymous users' do + note_fields = <<~FIELDS + id + author { name } + FIELDS + + post_graphql(query(note_fields), current_user: nil) + + designs_data = graphql_data['project']['issue']['designs']['designs'] + design_data = designs_data['edges'].first['node'] + note_data = design_data['notes']['edges'].first['node'] + + expect(note_data['id']).to eq(note.to_global_id.to_s) + end + + def query(note_fields = all_graphql_fields_for(Note)) + design_node = <<~NODE + designs { + edges { + node { + notes { + edges { + node { + #{note_fields} + } + } + } + } + } + } + NODE + graphql_query_for( + 'project', + { 'fullPath' => design.project.full_path }, + query_graphql_field( + 'issue', + { iid: design.issue.iid.to_s }, + query_graphql_field( + 'designs', {}, design_node + ) + ) + ) + end +end diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb new file mode 100644 index 00000000000..92d2f9d0d31 --- /dev/null +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Query.project(fullPath).issue(iid)' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue_b) { create(:issue, project: project) } + let_it_be(:developer) { create(:user) } + let(:current_user) { developer } + + let_it_be(:project_params) { { 'fullPath' => project.full_path } } + let_it_be(:issue_params) { { 'iid' => issue.iid.to_s } } + let_it_be(:issue_fields) { 'title' } + + let(:query) do + graphql_query_for('project', project_params, project_fields) + end + + let(:project_fields) do + query_graphql_field(:issue, issue_params, issue_fields) + end + + shared_examples 'being able to fetch a design-like object by ID' do + let(:design) { design_a } + let(:path) { %w[project issue designCollection] + [GraphqlHelpers.fieldnamerize(object_field_name)] } + + let(:design_fields) do + [ + query_graphql_field(:filename), + query_graphql_field(:project, nil, query_graphql_field(:id)) + ] + end + + let(:design_collection_fields) do + query_graphql_field(object_field_name, object_params, object_fields) + end + + let(:object_fields) { design_fields } + + context 'the ID is passed' do + let(:object_params) { { id: global_id_of(object) } } + let(:result_fields) { {} } + + let(:expected_fields) do + result_fields.merge({ 'filename' => design.filename, 'project' => id_hash(project) }) + end + + it 'retrieves the object' do + post_query + + data = graphql_data.dig(*path) + + expect(data).to match(a_hash_including(expected_fields)) + end + + context 'the user is unauthorized' do + let(:current_user) { create(:user) } + + it_behaves_like 'a failure to find anything' + end + end + + context 'without parameters' do + let(:object_params) { nil } + + it 'raises an error' do + post_query + + expect(graphql_errors).to include(no_argument_error) + end + end + + context 'attempting to retrieve an object from a different issue' do + let(:object_params) { { id: global_id_of(object_on_other_issue) } } + + it_behaves_like 'a failure to find anything' + end + end + + before do + project.add_developer(developer) + end + + let(:post_query) { post_graphql(query, current_user: current_user) } + + describe '.designCollection' do + include DesignManagementTestHelpers + + let_it_be(:design_a) { create(:design, issue: issue) } + let_it_be(:version_a) { create(:design_version, issue: issue, created_designs: [design_a]) } + + let(:issue_fields) do + query_graphql_field(:design_collection, dc_params, design_collection_fields) + end + + let(:dc_params) { nil } + let(:design_collection_fields) { nil } + + before do + enable_design_management + end + + describe '.design' do + let(:object) { design } + let(:object_field_name) { :design } + + let(:no_argument_error) do + custom_graphql_error(path, a_string_matching(%r/id or filename/)) + end + + let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) } + + it_behaves_like 'being able to fetch a design-like object by ID' + + it_behaves_like 'being able to fetch a design-like object by ID' do + let(:object_params) { { filename: design.filename } } + end + end + + describe '.version' do + let(:version) { version_a } + let(:path) { %w[project issue designCollection version] } + + let(:design_collection_fields) do + query_graphql_field(:version, version_params, 'id sha') + end + + context 'no parameters' do + let(:version_params) { nil } + + it 'raises an error' do + post_query + + expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/))) + end + end + + shared_examples 'a successful query for a version' do + it 'finds the version' do + post_query + + data = graphql_data.dig(*path) + + expect(data).to match( + a_hash_including('id' => global_id_of(version), + 'sha' => version.sha) + ) + end + end + + context '(sha: STRING_TYPE)' do + let(:version_params) { { sha: version.sha } } + + it_behaves_like 'a successful query for a version' + end + + context '(id: ID_TYPE)' do + let(:version_params) { { id: global_id_of(version) } } + + it_behaves_like 'a successful query for a version' + end + end + + describe '.designAtVersion' do + it_behaves_like 'being able to fetch a design-like object by ID' do + let(:object) { build(:design_at_version, design: design, version: version) } + let(:object_field_name) { :design_at_version } + + let(:version) { version_a } + + let(:result_fields) { { 'version' => id_hash(version) } } + let(:object_fields) do + design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))] + end + + let(:no_argument_error) { missing_required_argument(path, :id) } + + let(:object_on_other_issue) { build(:design_at_version, issue: issue_b) } + end + end + end + + def id_hash(object) + a_hash_including('id' => global_id_of(object)) + end +end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 4ce7a3912a3..91fce3eed92 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -45,8 +45,8 @@ describe 'getting an issue list for a project' do it 'includes discussion locked' do post_graphql(query, current_user: current_user) - expect(issues_data[0]['node']['discussionLocked']).to eq false - expect(issues_data[1]['node']['discussionLocked']).to eq true + expect(issues_data[0]['node']['discussionLocked']).to eq(false) + expect(issues_data[1]['node']['discussionLocked']).to eq(true) end context 'when limiting the number of results' do @@ -79,7 +79,7 @@ describe 'getting an issue list for a project' do post_graphql(query) - expect(issues_data).to eq [] + expect(issues_data).to eq([]) end end @@ -118,131 +118,138 @@ describe 'getting an issue list for a project' do end describe 'sorting and pagination' do - let(:start_cursor) { graphql_data['project']['issues']['pageInfo']['startCursor'] } - let(:end_cursor) { graphql_data['project']['issues']['pageInfo']['endCursor'] } + let_it_be(:data_path) { [:project, :issues] } - context 'when sorting by due date' do - let(:sort_project) { create(:project, :public) } - - let!(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } - let!(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } - let!(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } - let!(:due_issue4) { create(:issue, project: sort_project, due_date: nil) } - let!(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) } - - let(:params) { 'sort: DUE_DATE_ASC' } - - def query(issue_params = params) - graphql_query_for( - 'project', - { 'fullPath' => sort_project.full_path }, - <<~ISSUES - issues(#{issue_params}) { - pageInfo { - endCursor - } - edges { - node { - iid - dueDate - } - } - } - ISSUES - ) - end + def pagination_query(params, page_info) + graphql_query_for( + 'project', + { 'fullPath' => sort_project.full_path }, + "issues(#{params}) { #{page_info} edges { node { iid dueDate } } }" + ) + end - before do - post_graphql(query, current_user: current_user) - end + def pagination_results_data(data) + data.map { |issue| issue.dig('node', 'iid').to_i } + end - it_behaves_like 'a working graphql query' + context 'when sorting by due date' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } + let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } + let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } + let_it_be(:due_issue4) { create(:issue, project: sort_project, due_date: nil) } + let_it_be(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) } context 'when ascending' do - it 'sorts issues' do - expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] - end - - context 'when paginating' do - let(:params) { 'sort: DUE_DATE_ASC, first: 2' } - - it 'sorts issues' do - expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid] - - cursored_query = query("sort: DUE_DATE_ASC, after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] - - expect(grab_iids(response_data)).to eq [due_issue1.iid, due_issue4.iid, due_issue2.iid] - end + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'DUE_DATE_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] } end end context 'when descending' do - let(:params) { 'sort: DUE_DATE_DESC' } - - it 'sorts issues' do - expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'DUE_DATE_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] } end + end + end - context 'when paginating' do - let(:params) { 'sort: DUE_DATE_DESC, first: 2' } - - it 'sorts issues' do - expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid] - - cursored_query = query("sort: DUE_DATE_DESC, after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + context 'when sorting by relative position' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } + let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } + let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } + let_it_be(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } + let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } - expect(grab_iids(response_data)).to eq [due_issue3.iid, due_issue4.iid, due_issue2.iid] - end + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'RELATIVE_POSITION_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] } end end end - context 'when sorting by relative position' do - let(:sort_project) { create(:project, :public) } - - let!(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } - let!(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } - let!(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } - let!(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) } - let!(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } - - let(:params) { 'sort: RELATIVE_POSITION_ASC' } - - def query(issue_params = params) - graphql_query_for( - 'project', - { 'fullPath' => sort_project.full_path }, - "issues(#{issue_params}) { pageInfo { endCursor} edges { node { iid dueDate } } }" - ) + context 'when sorting by priority' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } + let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) } + let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) } + let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) } + let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) } + let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) } + let_it_be(:priority_issue4) { create(:issue, project: sort_project) } + + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'PRIORITY_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] } + end end - before do - post_graphql(query, current_user: current_user) + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'PRIORITY_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] } + end end + end - it_behaves_like 'a working graphql query' + context 'when sorting by label priority' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:label1) { create(:label, project: sort_project, priority: 1) } + let_it_be(:label2) { create(:label, project: sort_project, priority: 5) } + let_it_be(:label3) { create(:label, project: sort_project, priority: 10) } + let_it_be(:label_issue1) { create(:issue, project: sort_project, labels: [label1]) } + let_it_be(:label_issue2) { create(:issue, project: sort_project, labels: [label2]) } + let_it_be(:label_issue3) { create(:issue, project: sort_project, labels: [label1, label3]) } + let_it_be(:label_issue4) { create(:issue, project: sort_project) } context 'when ascending' do - it 'sorts issues' do - expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'LABEL_PRIORITY_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] } end + end - context 'when paginating' do - let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' } + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'LABEL_PRIORITY_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] } + end + end + end - it 'sorts issues' do - expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid] + context 'when sorting by milestone due date' do + let_it_be(:sort_project) { create(:project, :public) } + let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } + let_it_be(:milestone_issue1) { create(:issue, project: sort_project) } + let_it_be(:milestone_issue2) { create(:issue, project: sort_project, milestone: early_milestone) } + let_it_be(:milestone_issue3) { create(:issue, project: sort_project, milestone: late_milestone) } - cursored_query = query("sort: RELATIVE_POSITION_ASC, after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - response_data = JSON.parse(response.body)['data']['project']['issues']['edges'] + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'MILESTONE_DUE_ASC' } + let(:first_param) { 2 } + let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] } + end + end - expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] - end + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'MILESTONE_DUE_DESC' } + let(:first_param) { 2 } + let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] } end end end diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 43e1bb13342..e063068eb1a 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'query jira import data' do +describe 'query Jira import data' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -18,6 +18,7 @@ describe 'query jira import data' do jiraImports { nodes { jiraProjectKey + createdAt scheduledAt scheduledBy { username |