diff options
Diffstat (limited to 'spec/requests/api/graphql')
45 files changed, 1817 insertions, 127 deletions
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb index 5f8a895b16e..960fda80dd9 100644 --- a/spec/requests/api/graphql/ci/config_spec.rb +++ b/spec/requests/api/graphql/ci/config_spec.rb @@ -173,7 +173,7 @@ RSpec.describe 'Query.ciConfig' do { "name" => "docker", "size" => 1, - "jobs" => + "jobs" => { "nodes" => [ { @@ -206,7 +206,7 @@ RSpec.describe 'Query.ciConfig' do { "name" => "deploy_job", "size" => 1, - "jobs" => + "jobs" => { "nodes" => [ { @@ -332,7 +332,7 @@ RSpec.describe 'Query.ciConfig' do "only" => { "refs" => %w[branches tags] }, "when" => "on_success", "tags" => [], - "needs" => { "nodes" => [] } } + "needs" => { "nodes" => [] } } ] } } diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb new file mode 100644 index 00000000000..2b5a5d0dc93 --- /dev/null +++ b/spec/requests/api/graphql/ci/config_variables_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)' do + include GraphqlHelpers + include ReactiveCachingHelpers + + let_it_be(:content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + end + + let_it_be(:project) { create(:project, :custom_repo, :public, files: { '.gitlab-ci.yml' => content }) } + let_it_be(:user) { create(:user) } + + let(:service) { Ci::ListConfigVariablesService.new(project, user) } + let(:sha) { project.repository.commit.sha } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + ciConfigVariables(sha: "#{sha}") { + key + value + description + } + } + } + ) + end + + context 'when the user has the correct permissions' do + before do + project.add_maintainer(user) + allow(Ci::ListConfigVariablesService) + .to receive(:new) + .and_return(service) + end + + context 'when the cache is not empty' do + before do + synchronous_reactive_cache(service) + end + + it 'returns the CI variables for the config' do + expect(service) + .to receive(:execute) + .with(sha) + .and_call_original + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to contain_exactly( + { + 'key' => 'DB_NAME', + 'value' => 'postgres', + 'description' => nil + }, + { + 'key' => 'ENVIRONMENT_VAR', + 'value' => 'env var value', + 'description' => 'env var description' + } + ) + end + end + + context 'when the cache is empty' do + it 'returns nothing' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil + end + end + end + + context 'when the user is not authorized' do + before do + project.add_guest(user) + allow(Ci::ListConfigVariablesService) + .to receive(:new) + .and_return(service) + synchronous_reactive_cache(service) + end + + it 'returns nothing' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb index 5ea6646ec2c..7baf26c7648 100644 --- a/spec/requests/api/graphql/ci/group_variables_spec.rb +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do query { group(fullPath: "#{group.full_path}") { ciVariables { + limit nodes { id key @@ -35,11 +36,18 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do end it "returns the group's CI variables" do - variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test', - masked: false, protected: true, raw: true, environment_scope: 'staging') + variable = create(:ci_group_variable, + group: group, + key: 'TEST_VAR', + value: 'test', + masked: false, + protected: true, + raw: true, + environment_scope: 'staging') post_graphql(query, current_user: user) + expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(200) expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({ 'id' => variable.to_global_id.to_s, 'key' => 'TEST_VAR', diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index c5c88697bf4..cd6b2de98a1 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -29,7 +29,7 @@ RSpec.describe 'Query.ciVariables' do it "returns the instance's CI variables" do variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test', - masked: false, protected: true, raw: true) + masked: false, protected: true, raw: true) post_graphql(query, current_user: user) diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 8c4ab13fc35..fa8fb1d54aa 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -335,4 +335,35 @@ RSpec.describe 'Query.project.pipeline' do end end end + + context 'when querying jobs for multiple projects' do + let(:query) do + %( + query { + projects { + nodes { + jobs { + nodes { + name + } + } + } + } + } + ) + end + + before do + create_list(:project, 2).each do |project| + project.add_developer(user) + create(:ci_build, project: project) + end + end + + it 'returns an error' do + post_graphql(query, current_user: user) + + expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 Project\(s\) at a time./] + end + end end diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb index e61f146b24c..d49a4a7e768 100644 --- a/spec/requests/api/graphql/ci/project_variables_spec.rb +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do query { project(fullPath: "#{project.full_path}") { ciVariables { + limit nodes { id key @@ -36,10 +37,11 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do it "returns the project's CI variables" do variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test', - masked: false, protected: true, raw: true, environment_scope: 'production') + masked: false, protected: true, raw: true, environment_scope: 'production') post_graphql(query, current_user: user) + expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(200) expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({ 'id' => variable.to_global_id.to_s, 'key' => 'TEST_VAR', diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index e17a83d8e47..bd90753f9ad 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -9,24 +9,53 @@ RSpec.describe 'Query.runner(id)' do let_it_be(:group) { create(:group) } let_it_be(:active_instance_runner) do - create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago, - active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, - access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom, - maintenance_note: '**Test maintenance note**') + create(:ci_runner, :instance, + description: 'Runner 1', + contacted_at: 2.hours.ago, + active: true, + version: 'adfe156', + revision: 'a', + locked: true, + ip_address: '127.0.0.1', + maximum_timeout: 600, + access_level: 0, + tag_list: %w[tag1 tag2], + run_untagged: true, + executor_type: :custom, + maintenance_note: '**Test maintenance note**') end let_it_be(:inactive_instance_runner) do - create(:ci_runner, :instance, description: 'Runner 2', contacted_at: 1.day.ago, active: false, - version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + create(:ci_runner, :instance, + description: 'Runner 2', + contacted_at: 1.day.ago, + active: false, + version: 'adfe157', + revision: 'b', + ip_address: '10.10.10.10', + access_level: 1, + run_untagged: true) end let_it_be(:active_group_runner) do - create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago, - active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, - access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :shell) + create(:ci_runner, :group, + groups: [group], + description: 'Group runner 1', + contacted_at: 2.hours.ago, + active: true, + version: 'adfe156', + revision: 'a', + locked: true, + ip_address: '127.0.0.1', + maximum_timeout: 600, + access_level: 0, + tag_list: %w[tag1 tag2], + run_untagged: true, + executor_type: :shell) end - let_it_be(:active_project_runner) { create(:ci_runner, :project) } + let_it_be(:project1) { create(:project) } + let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) } shared_examples 'runner details fetch' do let(:query) do @@ -159,8 +188,16 @@ RSpec.describe 'Query.runner(id)' do with_them do let(:project_runner) do - create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked, - version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + create(:ci_runner, :project, + description: 'Runner 3', + contacted_at: 1.day.ago, + active: false, + locked: is_locked, + version: 'adfe157', + revision: 'b', + ip_address: '10.10.10.10', + access_level: 1, + run_untagged: true) end let(:query) do @@ -187,7 +224,6 @@ RSpec.describe 'Query.runner(id)' do end describe 'ownerProject' do - let_it_be(:project1) { create(:project) } let_it_be(:project2) { create(:project) } let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) } let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) } @@ -301,7 +337,6 @@ RSpec.describe 'Query.runner(id)' do end describe 'for multiple runners' do - let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) } let_it_be(:project_runner1) { create(:ci_runner, :project, projects: [project1, project2], description: 'Runner 1') } let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [], description: 'Runner 2') } @@ -394,6 +429,8 @@ RSpec.describe 'Query.runner(id)' do 'jobs' => nil, # returning jobs not allowed for more than 1 runner (see RunnerJobsResolver) 'projectCount' => nil, 'projects' => nil) + + expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 CiRunner\(s\) at a time./] end end end @@ -472,8 +509,8 @@ RSpec.describe 'Query.runner(id)' do <<~QUERY { instance_runner1: #{runner_query(active_instance_runner)} - project_runner1: #{runner_query(active_project_runner)} group_runner1: #{runner_query(active_group_runner)} + project_runner1: #{runner_query(active_project_runner)} } QUERY end @@ -493,12 +530,13 @@ RSpec.describe 'Query.runner(id)' do it 'does not execute more queries per runner', :aggregate_failures do # warm-up license cache and so on: - post_graphql(double_query, current_user: user) + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(double_query, **args) - control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) } + control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) } - expect { post_graphql(double_query, current_user: user) } - .not_to exceed_query_limit(control) + expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control) expect(graphql_data.count).to eq 6 expect(graphql_data).to match( @@ -528,4 +566,91 @@ RSpec.describe 'Query.runner(id)' do )) end end + + describe 'sorting and pagination' do + let(:query) do + <<~GQL + query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { + runner(id: $id) { + #{fields} + } + } + GQL + end + + before do + post_graphql(query, current_user: user, variables: variables) + end + + context 'with project search term' do + let_it_be(:project1) { create(:project, description: 'abc') } + let_it_be(:project2) { create(:project, description: 'def') } + let_it_be(:project_runner) do + create(:ci_runner, :project, projects: [project1, project2]) + end + + let(:variables) { { id: project_runner.to_global_id.to_s, n: n, project_search_term: search_term } } + + let(:fields) do + <<~QUERY + projects(search: $projectSearchTerm, first: $n, after: $cursor) { + count + nodes { + id + } + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } + } + QUERY + end + + let(:projects_data) { graphql_data_at('runner', 'projects') } + + context 'set to empty string' do + let(:search_term) { '' } + + context 'with n = 1' do + let(:n) { 1 } + + it_behaves_like 'a working graphql query' + + it 'returns paged result' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 2 + expect(projects_data['pageInfo']['hasNextPage']).to eq true + end + end + + context 'with n = 2' do + let(:n) { 2 } + + it 'returns non-paged result' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 2 + expect(projects_data['pageInfo']['hasNextPage']).to eq false + end + end + end + + context 'set to partial match' do + let(:search_term) { 'def' } + + context 'with n = 1' do + let(:n) { 1 } + + it_behaves_like 'a working graphql query' + + it 'returns paged result with no additional pages' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 1 + expect(projects_data['pageInfo']['hasNextPage']).to eq false + end + end + end + end + end end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 749f6839cb5..3054b866812 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -69,15 +69,6 @@ RSpec.describe 'Query.runners' do it_behaves_like 'a working graphql query returning expected runner' end - - context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do - let(:runner_type) { 'PROJECT_TYPE' } - let(:status) { 'NEVER_CONTACTED' } - - let!(:expected_runner) { project_runner } - - it_behaves_like 'a working graphql query returning expected runner' - end end describe 'pagination' do @@ -141,8 +132,13 @@ RSpec.describe 'Group.runners' do describe 'edges' do let_it_be(:runner) do - create(:ci_runner, :group, active: false, version: 'def', revision: '456', - description: 'Project runner', groups: [group], ip_address: '127.0.0.1') + create(:ci_runner, :group, + active: false, + version: 'def', + revision: '456', + description: 'Project runner', + groups: [group], + ip_address: '127.0.0.1') end let(:query) do diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 13b7a22e791..5dd5ad117b0 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -35,7 +35,17 @@ RSpec.describe 'getting custom emoji within namespace' do expect(graphql_data['group']['customEmoji']['nodes'].first['name']).to eq(custom_emoji.name) end - it 'returns nil when unauthorised' do + it 'returns nil custom emoji when the custom_emoji feature flag is disabled' do + stub_feature_flags(custom_emoji: false) + + post_graphql(custom_emoji_query(group), current_user: current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_data['group']).to be_present + expect(graphql_data['group']['customEmoji']).to be_nil + end + + it 'returns nil group when unauthorised' do user = create(:user) post_graphql(custom_emoji_query(group), current_user: user) diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb new file mode 100644 index 00000000000..6da00057449 --- /dev/null +++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb @@ -0,0 +1,487 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Environments Deployments query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + + let(:user) { developer } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + context 'when there are deployments in the environment' do + let_it_be(:finished_deployment_old) do + create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago) + end + + let_it_be(:finished_deployment_new) do + create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago) + end + + let_it_be(:upcoming_deployment_old) do + create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago) + end + + let_it_be(:upcoming_deployment_new) do + create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago) + end + + let_it_be(:other_environment) { create(:environment, project: project) } + let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + id + iid + ref + tag + sha + createdAt + updatedAt + finishedAt + status + } + } + } + } + } + ) + end + + it 'returns all deployments of the environment' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(4) + end + + context 'when query last deployment' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployment' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(1) + expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s) + end + end + + context 'when query latest upcoming deployment' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployment' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(1) + expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s) + end + end + + context 'when query finished deployments in descending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s) + expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s) + end + end + + context 'when query finished deployments in ascending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s) + expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s) + end + end + + context 'when query upcoming deployments in descending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s) + expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s) + end + end + + context 'when query upcoming deployments in ascending order' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'returns deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + expect(deployments.count).to eq(2) + expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s) + expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s) + end + end + + context 'when query last deployments of multiple environments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environments { + nodes { + name + deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) { + nodes { + iid + } + } + } + } + } + } + ) + end + + it 'returns an error for preventing N+1 queries' do + expect(subject['errors'][0]['message']) + .to include('"deployments" field can be requested only for 1 Environment(s) at a time.') + end + end + + context 'when query finished and upcoming deployments together' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [CREATED SUCCESS]) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'raises an error' do + expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError) + end + end + + context 'when multiple orderBy input are specified' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) { + nodes { + iid + } + } + } + } + } + ) + end + + it 'raises an error' do + expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.') + end + end + + context 'when user is guest' do + let(:user) { guest } + + it 'returns nothing' do + expect(subject['data']['project']['environment']).to be_nil + end + end + + shared_examples_for 'avoids N+1 database queries' do + it 'does not increase the query count' do + create_deployments + + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + create_deployments + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + expect(multi).not_to exceed_query_limit(baseline) + end + + def create_deployments + create_list(:deployment, 3, environment: environment, project: project).each do |deployment| + deployment.user = create(:user).tap { |u| project.add_developer(u) } + deployment.deployable = + create(:ci_build, project: project, environment: environment.name, deployment: deployment, + user: deployment.user) + + deployment.save! + end + end + end + + context 'when requesting commits of deployments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + iid + commit { + author { + avatarUrl + name + webPath + } + fullTitle + webPath + sha + } + } + } + } + } + } + ) + end + + it_behaves_like 'avoids N+1 database queries' + + it 'returns commits of deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + deployments.each do |deployment| + deployment_in_record = project.deployments.find_by_iid(deployment['iid']) + + expect(deployment_in_record.sha).to eq(deployment['commit']['sha']) + end + end + end + + context 'when requesting triggerers of deployments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + iid + triggerer { + id + avatarUrl + name + webPath + } + } + } + } + } + } + ) + end + + it_behaves_like 'avoids N+1 database queries' + + it 'returns triggerers of deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + deployments.each do |deployment| + deployment_in_record = project.deployments.find_by_iid(deployment['iid']) + + expect(deployment_in_record.deployed_by.name).to eq(deployment['triggerer']['name']) + end + end + end + + context 'when requesting jobs of deployments' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments { + nodes { + iid + job { + id + status + name + webPath + } + } + } + } + } + } + ) + end + + it_behaves_like 'avoids N+1 database queries' + + it 'returns jobs of deployments' do + deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes') + + deployments.each do |deployment| + deployment_in_record = project.deployments.find_by_iid(deployment['iid']) + + expect(deployment_in_record.build.to_global_id.to_s).to eq(deployment['job']['id']) + end + end + end + + describe 'sorting and pagination' do + let(:data_path) { [:project, :environment, :deployments] } + let(:current_user) { user } + + def pagination_query(params) + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + deployments(statuses: [SUCCESS], #{params}) { + nodes { + iid + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + } + } + ) + end + + def pagination_results_data(nodes) + nodes.map { |deployment| deployment['iid'].to_i } + end + + context 'when sorting by finished_at in ascending order' do + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) } + let(:first_param) { 2 } + let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] } + end + end + + context 'when sorting by finished_at in descending order' do + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) } + let(:first_param) { 2 } + let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] } + end + end + end + end +end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index bab8d5b770c..5f8becc0726 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -156,13 +156,20 @@ RSpec.describe 'getting group members information' do expect_array_response(child_user) end - it 'returns invited members plus inherited members' do + it 'returns invited members and inherited members of a shared group' do fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] }) expect(graphql_errors).to be_nil expect_array_response(invited_user, user_1, user_2, child_user) end + it 'returns invited members and inherited members of an ancestor of a shared group' do + fetch_members(group: grandchild_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] }) + + expect(graphql_errors).to be_nil + expect_array_response(grandchild_user, invited_user, user_1, user_2, child_user) + end + it 'returns direct and inherited members' do fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] }) diff --git a/spec/requests/api/graphql/group/packages_spec.rb b/spec/requests/api/graphql/group/packages_spec.rb index adee556db3a..cf8736db5af 100644 --- a/spec/requests/api/graphql/group/packages_spec.rb +++ b/spec/requests/api/graphql/group/packages_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'getting a package list for a group' do it 'returns an error for the second group and data for the first' do expect(a_packages_names).to contain_exactly(group_one_package.name) - expect_graphql_errors_to_include [/Packages can be requested only for one group at a time/] + expect_graphql_errors_to_include [/"packages" field can be requested only for 1 Group\(s\) at a time./] expect(graphql_data_at(:b, :packages)).to be(nil) end end diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb index a33e3ae5427..d6b0673e4f8 100644 --- a/spec/requests/api/graphql/group/work_item_types_spec.rb +++ b/spec/requests/api/graphql/group/work_item_types_spec.rb @@ -46,7 +46,7 @@ RSpec.describe 'getting a list of work item types for a group' do end end - context "when user doesn't have acces to the group" do + context "when user doesn't have access to the group" do let(:current_user) { create(:user) } before do diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb index 46ec22e7ef8..06093e9f7c2 100644 --- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -100,6 +100,20 @@ RSpec.describe 'Reposition and move issue within board lists' do expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) end end + + context 'when moving an issue using position_in_list' do + let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id, position_in_list: 0 } } + + it 'repositions an issue' do + post_graphql_mutation(mutation(params), current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + response_issue = json_response['data'][mutation_result_identifier]['issue'] + expect(response_issue['iid']).to eq(issue1.iid.to_s) + expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title) + expect(response_issue['relativePosition']).to be < existing_issue1.relative_position + end + end end context 'when user has no access to resources' do diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb index 6a098002963..9ee2f41e8fc 100644 --- a/spec/requests/api/graphql/mutations/branches/create_spec.rb +++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb @@ -5,26 +5,18 @@ require 'spec_helper' RSpec.describe 'Creation of a new branch' do include GraphqlHelpers + let_it_be(:group) { create(:group, :public) } let_it_be(:current_user) { create(:user) } - let_it_be(:project) { create(:project, :public, :empty_repo) } let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } } - let(:new_branch) { 'new_branch' } + let(:new_branch) { "new_branch_#{SecureRandom.hex(4)}" } let(:ref) { 'master' } let(:mutation) { graphql_mutation(:create_branch, input) } let(:mutation_response) { graphql_mutation_response(:create_branch) } - context 'the user is not allowed to create a branch' do - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to create a branch' do - before do - project.add_developer(current_user) - end - - it 'creates a new branch' do + shared_examples 'creates a new branch' do + specify do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) @@ -33,14 +25,75 @@ RSpec.describe 'Creation of a new branch' do 'commit' => a_hash_including('id') ) end + end + + context 'when project is public' do + let_it_be(:project) { create(:project, :public, :empty_repo) } + + context 'when user is not allowed to create a branch' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is a direct project member' do + context 'and user is a developer' do + before do + project.add_developer(current_user) + end + + it_behaves_like 'creates a new branch' + + context 'when ref is not correct' do + err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\'' + let(:new_branch) { 'another_branch' } + let(:ref) { 'unknown' } + + it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg] + end + end + end + + context 'when user is an inherited member from the group' do + context 'when project has a private repository' do + let_it_be(:project) { create(:project, :public, :empty_repo, :repository_private, group: group) } + + context 'and user is a guest' do + before do + group.add_guest(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'and user is a developer' do + before do + group.add_developer(current_user) + end + + it_behaves_like 'creates a new branch' + end + end + end + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private, :empty_repo, group: group) } + + context 'when user is an inherited member from the group' do + context 'and user is a guest' do + before do + group.add_guest(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end - context 'when ref is not correct' do - err_msg = 'Failed to create branch \'another_branch\': invalid reference name \'unknown\'' - let(:new_branch) { 'another_branch' } - let(:ref) { 'unknown' } + context 'and user is a developer' do + before do + group.add_developer(current_user) + end - it_behaves_like 'a mutation that returns errors in the response', - errors: [err_msg] + it_behaves_like 'creates a new branch' + end end end end diff --git a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb new file mode 100644 index 00000000000..5855eb6bb51 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'JobArtifactsDestroy' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:job) { create(:ci_build) } + + let(:mutation) do + variables = { + id: job.to_global_id.to_s + } + graphql_mutation(:job_artifacts_destroy, variables, <<~FIELDS) + job { + name + } + destroyedArtifactsCount + errors + FIELDS + end + + before do + create(:ci_job_artifact, :archive, job: job) + create(:ci_job_artifact, :junit, job: job) + end + + it 'returns an error if the user is not allowed to destroy the job artifacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(job.reload.job_artifacts.count).to be(2) + end + + it 'destroys the job artifacts and returns the expected data' do + job.project.add_maintainer(user) + expected_data = { + 'jobArtifactsDestroy' => { + 'errors' => [], + 'destroyedArtifactsCount' => 2, + 'job' => { + 'name' => job.name + } + } + } + + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_data).to eq(expected_data) + expect(job.reload.job_artifacts.count).to be(0) + end +end diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb new file mode 100644 index 00000000000..a5ec9ea343d --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ArtifactDestroy' do + include GraphqlHelpers + + let(:user) { create(:user) } + let(:artifact) { create(:ci_job_artifact) } + + let(:mutation) do + variables = { + id: artifact.to_global_id.to_s + } + graphql_mutation(:artifact_destroy, variables, 'errors') + end + + it 'returns an error if the user is not allowed to destroy the artifact' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + end + + context 'when the user is allowed to destroy the artifact' do + before do + artifact.job.project.add_maintainer(user) + end + + it 'destroys the artifact' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns error if destory fails' do + allow_next_found_instance_of(Ci::JobArtifact) do |instance| + allow(instance).to receive(:destroy).and_return(false) + allow(instance).to receive_message_chain(:errors, :full_messages).and_return(['cannot be removed']) + end + + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:artifact_destroy, :errors)).to contain_exactly('cannot be removed') + end + end +end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb index c91437fa355..66facdebe78 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb @@ -39,5 +39,19 @@ RSpec.describe 'Creation of a new Custom Emoji' do expect(gql_response['customEmoji']['name']).to eq(attributes[:name]) expect(gql_response['customEmoji']['url']).to eq(attributes[:url]) end + + context 'when the custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it 'does nothing and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to not_change(CustomEmoji, :count) + + expect_graphql_errors_to_include('Custom emoji feature is disabled') + end + end end end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb index 07fd57a2cee..7d25206e617 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb @@ -68,6 +68,20 @@ RSpec.describe 'Deletion of custom emoji' do end it_behaves_like 'deletes custom emoji' + + context 'when the custom_emoji feature flag is disabled' do + before do + stub_feature_flags(custom_emoji: false) + end + + it_behaves_like 'does not delete custom emoji' + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('Custom emoji feature is disabled') + end + end end end end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index 9272e218172..85eaec90f47 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Promote an incident timeline event from a comment' do include GraphqlHelpers + include NotesHelper let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } @@ -53,7 +54,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do 'promotedFromNote' => { 'id' => comment.to_global_id.to_s }, - 'note' => comment.note, + 'note' => "@#{comment.author.username} [commented](#{noteable_note_url(comment)}): '#{comment.note}'", 'action' => 'comment', 'editable' => true, 'occurredAt' => comment.created_at.iso8601 diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index 608b36e4f15..8cec5867aca 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -93,6 +93,16 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled do expect(response).to have_gitlab_http_status(:success) expect(mutation_assignee_nodes).to match_array(expected_result) end + + it 'triggers webhooks', :sidekiq_inline do + hook = create(:project_hook, merge_requests_events: true, project: merge_request.project) + + expect(WebHookWorker).to receive(:perform_async).with(hook.id, anything, 'merge_request_hooks', anything) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + end end context 'when passing an empty list of assignees' do diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb index 33d4e57904c..240db764f40 100644 --- a/spec/requests/api/graphql/mutations/releases/update_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb @@ -22,9 +22,14 @@ RSpec.describe 'Updating an existing release' do let_it_be(:milestones) { [milestone_12_3, milestone_12_4] } let_it_be(:release) do - create(:release, project: project, tag: tag_name, name: name, - description: description, released_at: Time.parse(released_at).utc, - created_at: Time.parse(created_at).utc, milestones: milestones) + create(:release, + project: project, + tag: tag_name, + name: name, + description: description, + released_at: Time.parse(released_at).utc, + created_at: Time.parse(created_at).utc, + milestones: milestones) end let(:mutation_name) { :release_update } diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb index 9830623ede8..89c01d44771 100644 --- a/spec/requests/api/graphql/packages/composer_spec.rb +++ b/spec/requests/api/graphql/packages/composer_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:composer_package, project: project) } + let_it_be(:package) { create(:composer_package, :last_downloaded_at, project: project) } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_metadatum) do # we are forced to manually create the metadatum, without using the factory to force the sha to be a string diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb index 5bd5a71bbeb..7ad85edecef 100644 --- a/spec/requests/api/graphql/packages/conan_spec.rb +++ b/spec/requests/api/graphql/packages/conan_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'conan package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:conan_package, project: project) } + let_it_be(:package) { create(:conan_package, :last_downloaded_at, project: project) } let(:metadata) { query_graphql_fragment('ConanMetadata') } let(:package_files_metadata) { query_graphql_fragment('ConanFileMetadata') } diff --git a/spec/requests/api/graphql/packages/helm_spec.rb b/spec/requests/api/graphql/packages/helm_spec.rb index 1675b8faa23..79a589e2dc2 100644 --- a/spec/requests/api/graphql/packages/helm_spec.rb +++ b/spec/requests/api/graphql/packages/helm_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'helm package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:helm_package, project: project) } + let_it_be(:package) { create(:helm_package, :last_downloaded_at, project: project) } let(:package_files_metadata) { query_graphql_fragment('HelmFileMetadata') } diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb index 9d59a922660..b7f39efcf73 100644 --- a/spec/requests/api/graphql/packages/maven_spec.rb +++ b/spec/requests/api/graphql/packages/maven_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'maven package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:maven_package, project: project) } + let_it_be(:package) { create(:maven_package, :last_downloaded_at, project: project) } let(:metadata) { query_graphql_fragment('MavenMetadata') } @@ -31,7 +31,9 @@ RSpec.describe 'maven package details' do context 'a versionless maven package' do let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) } - let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) } + let_it_be(:package) do + create(:maven_package, :last_downloaded_at, project: project, version: nil, maven_metadatum: maven_metadatum) + end subject { post_graphql(query, current_user: user) } diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb index 87cffc67ce5..7de132d1574 100644 --- a/spec/requests/api/graphql/packages/nuget_spec.rb +++ b/spec/requests/api/graphql/packages/nuget_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'nuget package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) } + let_it_be(:package) { create(:nuget_package, :last_downloaded_at, :with_metadatum, project: project) } let_it_be(:dependency_link) { create(:packages_dependency_link, :with_nuget_metadatum, package: package) } let(:metadata) { query_graphql_fragment('NugetMetadata') } diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb index c28b37db5af..e9f82d66775 100644 --- a/spec/requests/api/graphql/packages/package_spec.rb +++ b/spec/requests/api/graphql/packages/package_spec.rb @@ -6,8 +6,8 @@ RSpec.describe 'package details' do let_it_be_with_reload(:group) { create(:group) } let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be_with_reload(:composer_package) { create(:composer_package, :last_downloaded_at, project: project) } let_it_be(:user) { create(:user) } - let_it_be(:composer_package) { create(:composer_package, project: project) } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_metadatum) do # we are forced to manually create the metadatum, without using the factory to force the sha to be a string @@ -65,6 +65,17 @@ RSpec.describe 'package details' do end end + context 'with package without last_downloaded_at' do + before do + composer_package.update!(last_downloaded_at: nil) + subject + end + + it 'matches the JSON schema' do + expect(package_details).to match_schema('graphql/packages/package_details') + end + end + context 'with package files pending destruction' do let_it_be(:package_file) { create(:package_file, package: composer_package) } let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) } @@ -97,7 +108,7 @@ RSpec.describe 'package details' do expect(graphql_data_at(:a, :name)).to eq(composer_package.name) - expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/] + expect_graphql_errors_to_include [/"package" field can be requested only for 1 Query\(s\) at a time./] expect(graphql_data_at(:b)).to be(nil) end end diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb index 0cc5bd2e3b2..c0e589f3597 100644 --- a/spec/requests/api/graphql/packages/pypi_spec.rb +++ b/spec/requests/api/graphql/packages/pypi_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'pypi package details' do include GraphqlHelpers include_context 'package details setup' - let_it_be(:package) { create(:pypi_package, project: project) } + let_it_be(:package) { create(:pypi_package, :last_downloaded_at, project: project) } let(:metadata) { query_graphql_fragment('PypiMetadata') } diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb new file mode 100644 index 00000000000..cb5006ec8e4 --- /dev/null +++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting merge access levels for a branch protection' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + let(:merge_access_level_data) { merge_access_levels_data[0] } + + let(:merge_access_levels_data) do + graphql_data_at('project', + 'branchRules', + 'nodes', + 0, + 'branchProtection', + 'mergeAccessLevels', + 'nodes') + end + + let(:project) { protected_branch.project } + + let(:merge_access_levels_count) { protected_branch.merge_access_levels.size } + + let(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for('MergeAccessLevel') } + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + mergeAccessLevels { + nodes { + #{fields} + } + } + } + } + } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + let_it_be(:protected_branch) { create(:protected_branch) } + + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(merge_access_levels_data).not_to be_present } + end + + shared_examples 'merge access request' do + let(:merge_access) { protected_branch.merge_access_levels.first } + + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'returns all merge access levels' do + expect(merge_access_levels_data.size).to eq(merge_access_levels_count) + end + + it 'includes access_level' do + expect(merge_access_level_data['accessLevel']) + .to eq(merge_access.access_level) + end + + it 'includes access_level_description' do + expect(merge_access_level_data['accessLevelDescription']) + .to eq(merge_access.humanize) + end + end + + context 'when the user does have read_protected_branch abilities' do + let(:merge_access) { protected_branch.merge_access_levels.first } + + context 'when no one has access' do + let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_merge) } + + it_behaves_like 'merge access request' + end + + context 'when developers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :developers_can_merge) } + + it_behaves_like 'merge access request' + end + + context 'when maintainers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_merge) } + + it_behaves_like 'merge access request' + end + end +end diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb new file mode 100644 index 00000000000..59f9c7d61cb --- /dev/null +++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting push access levels for a branch protection' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + let(:push_access_level_data) { push_access_levels_data[0] } + + let(:push_access_levels_data) do + graphql_data_at('project', + 'branchRules', + 'nodes', + 0, + 'branchProtection', + 'pushAccessLevels', + 'nodes') + end + + let(:project) { protected_branch.project } + + let(:push_access_levels_count) { protected_branch.push_access_levels.size } + + let(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for('PushAccessLevel'.classify) } + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + pushAccessLevels { + nodes { + #{fields} + } + } + } + } + } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + let_it_be(:protected_branch) { create(:protected_branch) } + + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(push_access_levels_data).not_to be_present } + end + + shared_examples 'push access request' do + let(:push_access) { protected_branch.push_access_levels.first } + + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'returns all push access levels' do + expect(push_access_levels_data.size).to eq(push_access_levels_count) + end + + it 'includes access_level' do + expect(push_access_level_data['accessLevel']) + .to eq(push_access.access_level) + end + + it 'includes access_level_description' do + expect(push_access_level_data['accessLevelDescription']) + .to eq(push_access.humanize) + end + end + + context 'when the user does have read_protected_branch abilities' do + let(:push_access) { protected_branch.push_access_levels.first } + + context 'when no one has access' do + let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_push) } + + it_behaves_like 'push access request' + end + + context 'when developers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :developers_can_push) } + + it_behaves_like 'push access request' + end + + context 'when maintainers have access' do + let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_push) } + + it_behaves_like 'push access request' + end + end +end diff --git a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb new file mode 100644 index 00000000000..8a3f546ef95 --- /dev/null +++ b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting branch protection for a branch rule' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:branch_rule) { create(:protected_branch) } + let_it_be(:project) { branch_rule.project } + + let(:branch_protection_data) do + graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection') + end + + let(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for('BranchProtection') } + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + #{fields} + } + } + } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(branch_protection_data).not_to be_present } + end + + context 'when the user does have read_protected_branch abilities' do + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'includes allow_force_push' do + expect(branch_protection_data['allowForcePush']).to be_in([true, false]) + expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push) + end + end +end diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb new file mode 100644 index 00000000000..70fb37941e2 --- /dev/null +++ b/spec/requests/api/graphql/project/branch_rules_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting list of branch rules for a project' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:current_user) { create(:user) } + let_it_be(:branch_name_a) { 'branch_name_a' } + let_it_be(:branch_name_b) { 'wildcard-*' } + let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] } + + let_it_be(:branch_rule_a) do + create(:protected_branch, project: project, name: branch_name_a) + end + + let_it_be(:branch_rule_b) do + create(:protected_branch, project: project, name: branch_name_b) + end + + let(:branch_rules_data) { graphql_data_at('project', 'branchRules', 'edges') } + let(:variables) { { path: project.full_path } } + + let(:fields) do + <<~QUERY + pageInfo { + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + #{all_graphql_fields_for('branch_rules'.classify)} + } + } + QUERY + end + + let(:query) do + <<~GQL + query($path: ID!, $n: Int, $cursor: String) { + project(fullPath: $path) { + branchRules(first: $n, after: $cursor) { #{fields} } + } + } + GQL + end + + context 'when the user does not have read_protected_branch abilities' do + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(branch_rules_data).to be_empty } + end + + context 'when the user does have read_protected_branch abilities' do + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'includes a name' do + expect(branch_rules_data.dig(0, 'node', 'name')).to be_present + end + + it 'includes created_at and updated_at' do + expect(branch_rules_data.dig(0, 'node', 'createdAt')).to be_present + expect(branch_rules_data.dig(1, 'node', 'updatedAt')).to be_present + end + + context 'when limiting the number of results' do + let(:branch_rule_limit) { 1 } + let(:variables) { { path: project.full_path, n: branch_rule_limit } } + let(:next_variables) do + { path: project.full_path, n: branch_rule_limit, cursor: last_cursor } + end + + it_behaves_like 'a working graphql query' do + it 'only returns N branch_rules' do + expect(branch_rules_data.size).to eq(branch_rule_limit) + expect(has_next_page).to be_truthy + expect(has_prev_page).to be_falsey + post_graphql(query, current_user: current_user, variables: next_variables) + expect(branch_rules_data.size).to eq(branch_rule_limit) + expect(has_next_page).to be_falsey + expect(has_prev_page).to be_truthy + end + end + + context 'when no limit is provided' do + let(:branch_rule_limit) { nil } + + it 'returns all branch_rules' do + expect(branch_rules_data.size).to eq(branch_rules.size) + end + end + end + end + + def pagination_info + graphql_data_at('project', 'branchRules', 'pageInfo') + end + + def has_next_page + pagination_info['hasNextPage'] + end + + def has_prev_page + pagination_info['hasPreviousPage'] + end + + def last_cursor + branch_rules_data.last['cursor'] + end +end diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb new file mode 100644 index 00000000000..e5ef7bcafbf --- /dev/null +++ b/spec/requests/api/graphql/project/deployment_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Deployment query' do + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:deployment) { create(:deployment, environment: environment, project: project) } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + deployment(iid: #{deployment.iid}) { + id + iid + ref + tag + sha + createdAt + updatedAt + finishedAt + status + } + } + } + ) + end + + it 'returns the deployment of the project' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data['iid']).to eq(deployment.iid.to_s) + end + + context 'when user is guest' do + let(:user) { guest } + + it 'returns nothing' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb new file mode 100644 index 00000000000..e5b6aebbf2c --- /dev/null +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Environments query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be_with_refind(:production) { create(:environment, :production, project: project) } + let_it_be_with_refind(:staging) { create(:environment, :staging, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + + subject { post_graphql(query, current_user: user) } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{production.name}") { + slug + createdAt + updatedAt + autoStopAt + autoDeleteAt + tier + environmentType + } + } + } + ) + end + + it 'returns the specified fields of the environment', :aggregate_failures do + production.update!(auto_stop_at: 1.day.ago, auto_delete_at: 2.days.ago, environment_type: 'review') + + subject + + environment_data = graphql_data.dig('project', 'environment') + expect(environment_data['slug']).to eq(production.slug) + expect(environment_data['createdAt']).to eq(production.created_at.iso8601) + expect(environment_data['updatedAt']).to eq(production.updated_at.iso8601) + expect(environment_data['autoStopAt']).to eq(production.auto_stop_at.iso8601) + expect(environment_data['autoDeleteAt']).to eq(production.auto_delete_at.iso8601) + expect(environment_data['tier']).to eq(production.tier.upcase) + expect(environment_data['environmentType']).to eq(production.environment_type) + end + + describe 'last deployments of environments' do + ::Deployment.statuses.each do |status, _| + let_it_be(:"production_#{status}_deployment") do + create(:deployment, status.to_sym, environment: production, project: project) + end + + let_it_be(:"staging_#{status}_deployment") do + create(:deployment, status.to_sym, environment: staging, project: project) + end + end + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environments { + nodes { + name + lastSuccessDeployment: lastDeployment(status: SUCCESS) { + iid + } + lastRunningDeployment: lastDeployment(status: RUNNING) { + iid + } + lastBlockedDeployment: lastDeployment(status: BLOCKED) { + iid + } + } + } + } + } + ) + end + + it 'returns all last deployments of the environment' do + subject + + environments_data = graphql_data_at(:project, :environments, :nodes) + + environments_data.each do |environment_data| + name = environment_data['name'] + success_deployment = public_send(:"#{name}_success_deployment") + running_deployment = public_send(:"#{name}_running_deployment") + blocked_deployment = public_send(:"#{name}_blocked_deployment") + + expect(environment_data['lastSuccessDeployment']['iid']).to eq(success_deployment.iid.to_s) + expect(environment_data['lastRunningDeployment']['iid']).to eq(running_deployment.iid.to_s) + expect(environment_data['lastBlockedDeployment']['iid']).to eq(blocked_deployment.iid.to_s) + end + end + + it 'executes the same number of queries in single environment and multiple environments' do + single_environment_query = + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{production.name}") { + name + lastSuccessDeployment: lastDeployment(status: SUCCESS) { + iid + } + lastRunningDeployment: lastDeployment(status: RUNNING) { + iid + } + lastBlockedDeployment: lastDeployment(status: BLOCKED) { + iid + } + } + } + } + ) + + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(single_environment_query, context: { current_user: user }) + end + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + expect(multi).not_to exceed_query_limit(baseline) + end + end +end 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 index 8cda61f0628..0444ce43c22 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -11,14 +11,14 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) 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)) + 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)) + create(:design_version, + issue: issue, + modified_designs: old_version.designs, + created_designs: create_list(:design, 2, issue: issue)) end let(:current_user) { developer } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 596e023a027..28282860416 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -27,14 +27,6 @@ RSpec.describe 'getting an issue list for a project' do QUERY end - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - query_graphql_field('issues', issue_filter_params, fields) - ) - end - it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) @@ -89,6 +81,14 @@ RSpec.describe 'getting an issue list for a project' do end end + context 'when filtering by search' do + it_behaves_like 'query with a search term' do + let(:issuable_data) { issues_data } + let(:user) { current_user } + let_it_be(:issuable) { create(:issue, project: project, description: 'bar') } + end + end + context 'when limiting the number of results' do let(:query) do <<~GQL @@ -301,7 +301,7 @@ RSpec.describe 'getting an issue list for a project' do let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) } context 'when ascending' do - it_behaves_like 'sorted paginated query' do + it_behaves_like 'sorted paginated query', is_reversible: true do let(:sort_param) { :RELATIVE_POSITION_ASC } let(:first_param) { 2 } let(:all_records) do @@ -679,4 +679,12 @@ RSpec.describe 'getting an issue list for a project' do def issues_ids graphql_dig_at(issues_data, :node, :id) end + + def query(params = issue_filter_params) + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('issues', params, fields) + ) + end end diff --git a/spec/requests/api/graphql/project/job_spec.rb b/spec/requests/api/graphql/project/job_spec.rb new file mode 100644 index 00000000000..6edd4cf753f --- /dev/null +++ b/spec/requests/api/graphql/project/job_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project.job' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:job) { create(:ci_build, project: project, name: 'GQL test job') } + + let(:query) do + <<~QUERY + { + project(fullPath: "#{project.full_path}") { + job(id: "#{job.to_global_id}") { + name + } + } + } + QUERY + end + + context 'when the user can read jobs on the project' do + before do + project.add_developer(user) + end + + it 'returns the job that matches the given ID' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'job', 'name')).to eq('GQL test job') + end + + context 'when no job matches the given ID' do + let(:job) { create(:ci_build, project: create(:project), name: 'Job from another project') } + + it 'returns null' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'job')).to be_nil + end + end + end + + context 'when the user cannot read jobs on the project' do + it 'returns null' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'job')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index d2f34080be3..6a59df81405 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -365,7 +365,7 @@ RSpec.describe 'getting merge request information nested in a project' do expect(interaction_data).to contain_exactly a_hash_including( 'canMerge' => false, 'canUpdate' => can_update, - 'reviewState' => attention_requested, + 'reviewState' => unreviewed, 'reviewed' => false, 'approved' => false ) @@ -398,8 +398,8 @@ RSpec.describe 'getting merge request information nested in a project' do describe 'scalability' do let_it_be(:other_users) { create_list(:user, 3) } - let(:attention_requested) do - { 'reviewState' => 'ATTENTION_REQUESTED' } + let(:unreviewed) do + { 'reviewState' => 'UNREVIEWED' } end let(:reviewed) do @@ -425,15 +425,15 @@ RSpec.describe 'getting merge request information nested in a project' do other_users.each do |user| assign_user(user) - merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user, state: :attention_requested) + merge_request.merge_request_reviewers.find_or_create_by!(reviewer: user) end expect { post_graphql(query) }.not_to exceed_query_limit(baseline) expect(interaction_data).to contain_exactly( - include(attention_requested), - include(attention_requested), - include(attention_requested), + include(unreviewed), + include(unreviewed), + include(unreviewed), include(reviewed) ) end @@ -462,17 +462,17 @@ RSpec.describe 'getting merge request information nested in a project' do it_behaves_like 'when requesting information about MR interactions' do let(:field) { :reviewers } - let(:attention_requested) { 'ATTENTION_REQUESTED' } + let(:unreviewed) { 'UNREVIEWED' } let(:can_update) { false } def assign_user(user) - merge_request.merge_request_reviewers.create!(reviewer: user, state: :attention_requested) + merge_request.merge_request_reviewers.create!(reviewer: user) end end it_behaves_like 'when requesting information about MR interactions' do let(:field) { :assignees } - let(:attention_requested) { nil } + let(:unreviewed) { nil } let(:can_update) { true } # assignees can update MRs def assign_user(user) diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index 08c6a2d9927..41915d3cdee 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -111,7 +111,7 @@ RSpec.describe 'getting pipeline information nested in a project' do name: build_job.name, pipeline: pipeline, stage_idx: 0, - stage: build_job.stage) + stage: build_job.stage_name) end let(:fields) do diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb index 8f2d2cffef2..5e207ec0963 100644 --- a/spec/requests/api/graphql/project/terraform/state_spec.rb +++ b/spec/requests/api/graphql/project/terraform/state_spec.rb @@ -60,17 +60,17 @@ RSpec.describe 'query a single terraform state' do expect(data).to match a_graphql_entity_for( terraform_state, :name, - 'lockedAt' => terraform_state.locked_at.iso8601, - 'createdAt' => terraform_state.created_at.iso8601, - 'updatedAt' => terraform_state.updated_at.iso8601, - 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), + 'lockedAt' => terraform_state.locked_at.iso8601, + 'createdAt' => terraform_state.created_at.iso8601, + 'updatedAt' => terraform_state.updated_at.iso8601, + 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), 'latestVersion' => a_graphql_entity_for( latest_version, - 'serial' => eq(latest_version.version), - 'createdAt' => eq(latest_version.created_at.iso8601), - 'updatedAt' => eq(latest_version.updated_at.iso8601), + 'serial' => eq(latest_version.version), + 'createdAt' => eq(latest_version.created_at.iso8601), + 'updatedAt' => eq(latest_version.updated_at.iso8601), 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user), - 'job' => { 'name' => eq(latest_version.build.name) } + 'job' => { 'name' => eq(latest_version.build.name) } ) ) end diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index a7ec6f69776..cc3660bcc6b 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -64,18 +64,18 @@ RSpec.describe 'query terraform states' do expect(data['nodes']).to contain_exactly a_graphql_entity_for( terraform_state, :name, - 'lockedAt' => terraform_state.locked_at.iso8601, - 'createdAt' => terraform_state.created_at.iso8601, - 'updatedAt' => terraform_state.updated_at.iso8601, - 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), + 'lockedAt' => terraform_state.locked_at.iso8601, + 'createdAt' => terraform_state.created_at.iso8601, + 'updatedAt' => terraform_state.updated_at.iso8601, + 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), 'latestVersion' => a_graphql_entity_for( latest_version, - 'serial' => eq(latest_version.version), - 'downloadPath' => eq(download_path), - 'createdAt' => eq(latest_version.created_at.iso8601), - 'updatedAt' => eq(latest_version.updated_at.iso8601), + 'serial' => eq(latest_version.version), + 'downloadPath' => eq(download_path), + 'createdAt' => eq(latest_version.created_at.iso8601), + 'updatedAt' => eq(latest_version.updated_at.iso8601), 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user), - 'job' => { 'name' => eq(latest_version.build.name) } + 'job' => { 'name' => eq(latest_version.build.name) } ) ) end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 6ef28392b8b..69f8d1cac74 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -10,7 +10,10 @@ RSpec.describe 'getting an work item list for a project' do let_it_be(:current_user) { create(:user) } let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1') } - let_it_be(:item2) { create(:work_item, project: project, title: 'item2') } + let_it_be(:item2) do + create(:work_item, project: project, title: 'item2', last_edited_by: current_user, last_edited_at: 1.day.ago) + end + let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') } let_it_be(:other_item) { create(:work_item) } @@ -27,14 +30,6 @@ RSpec.describe 'getting an work item list for a project' do QUERY end - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - query_graphql_field('workItems', item_filter_params, fields) - ) - end - it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) @@ -83,6 +78,48 @@ RSpec.describe 'getting an work item list for a project' do end end + context 'when fetching description edit information' do + let(:fields) do + <<~GRAPHQL + nodes { + widgets { + type + ... on WorkItemWidgetDescription { + edited + lastEditedAt + lastEditedBy { + webPath + username + } + } + } + } + GRAPHQL + end + + it 'avoids N+1 queries' do + post_graphql(query, current_user: current_user) # warm-up + + control = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: current_user) + end + expect_graphql_errors_to_be_empty + + create_list(:work_item, 3, :last_edited_by_user, last_edited_at: 1.week.ago, project: project) + + expect_graphql_errors_to_be_empty + expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) + end + end + + context 'when filtering by search' do + it_behaves_like 'query with a search term' do + let(:issuable_data) { items_data } + let(:user) { current_user } + let_it_be(:issuable) { create(:work_item, project: project, description: 'bar') } + end + end + describe 'sorting and pagination' do let(:data_path) { [:project, :work_items] } @@ -118,4 +155,12 @@ RSpec.describe 'getting an work item list for a project' do def item_ids graphql_dig_at(items_data, :node, :id) end + + def query(params = item_filter_params) + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('workItems', params, fields) + ) + end end diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb index 4aa9c4b8254..359c599cd3a 100644 --- a/spec/requests/api/graphql/query_spec.rb +++ b/spec/requests/api/graphql/query_spec.rb @@ -108,8 +108,8 @@ RSpec.describe 'Query' do design_at_version, 'filename' => design_at_version.design.filename, 'version' => a_graphql_entity_for(version, :sha), - 'design' => a_graphql_entity_for(design), - 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, + 'design' => a_graphql_entity_for(design), + 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, 'project' => a_graphql_entity_for(project, :full_path) ) end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 34644e5893a..e4bb4109c76 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -14,7 +14,10 @@ RSpec.describe 'Query.work_item(id)' do project: project, description: '- List item', start_date: Date.today, - due_date: 1.week.from_now + due_date: 1.week.from_now, + created_at: 1.week.ago, + last_edited_at: 1.day.ago, + last_edited_by: guest ) end @@ -67,6 +70,12 @@ RSpec.describe 'Query.work_item(id)' do ... on WorkItemWidgetDescription { description descriptionHtml + edited + lastEditedBy { + webPath + username + } + lastEditedAt } } GRAPHQL @@ -79,7 +88,13 @@ RSpec.describe 'Query.work_item(id)' do hash_including( 'type' => 'DESCRIPTION', 'description' => work_item.description, - 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}) + 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}), + 'edited' => true, + 'lastEditedAt' => work_item.last_edited_at.iso8601, + 'lastEditedBy' => { + 'webPath' => "/#{guest.full_path}", + 'username' => guest.username + } ) ) ) |