diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-16 13:42:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-16 13:42:19 +0300 |
commit | 84d1bd786125c1c14a3ba5f63e38a4cc736a9027 (patch) | |
tree | f550fa965f507077e20dbb6d61a8269a99ef7107 /spec/requests/api/graphql | |
parent | 3a105e36e689f7b75482236712f1a47fd5a76814 (diff) |
Add latest changes from gitlab-org/gitlab@16-8-stable-eev16.8.0-rc42
Diffstat (limited to 'spec/requests/api/graphql')
24 files changed, 852 insertions, 345 deletions
diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb index 32048ea1432..94678bd18da 100644 --- a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb +++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb @@ -89,14 +89,14 @@ RSpec.describe 'UserAchievements', feature_category: :user_profile do end it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: user) - end.count + end user2 = create(:user) create(:user_achievement, achievement: achievement, user: user2) - expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count) + expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control) end context 'when the achievements feature flag is disabled' do diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index 86e2b288890..312700b1dcf 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -115,8 +115,8 @@ RSpec.describe 'get board lists', feature_category: :team_planning do let(:issue_params) { { filters: { or: { assignee_usernames: [user.username, another_user.username] } } } } it 'returns correctly filtered issues' do - issue1.assignee_ids = user.id - issue2.assignee_ids = another_user.id + IssueAssignee.create!(issue_id: issue1.id, user_id: user.id) + IssueAssignee.create!(issue_id: issue2.id, user_id: another_user.id) subject diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb index 9fe73e7ba45..836e52197a3 100644 --- a/spec/requests/api/graphql/ci/catalog/resource_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio ) end - let_it_be(:resource) { create(:ci_catalog_resource, :published, project: project) } + let_it_be_with_reload(:resource) { create(:ci_catalog_resource, :published, project: project) } let(:query) do <<~GQL @@ -81,7 +81,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio nodes { id name - path + includePath inputs { name default @@ -126,7 +126,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio a_graphql_entity_for( components.first, name: components.first.name, - path: components.first.path, + include_path: components.first.path, inputs: [ a_graphql_entity_for( name: 'tags', @@ -148,48 +148,68 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio a_graphql_entity_for( components.last, name: components.last.name, - path: components.last.path + include_path: components.last.path ) ) end end end - describe 'versions' do - let(:query) do - <<~GQL - query { - ciCatalogResource(id: "#{resource.to_global_id}") { - id - versions { - nodes { - id - tagName - tagPath - releasedAt - author { + describe 'version fields' do + before_all do + # To test the readme_html field, we need to create versions with real commit shas + project.repository.create_branch('branch_v2', project.default_branch) + project.repository.update_file( + user, 'README.md', 'Readme v2', message: 'Update readme', branch_name: 'branch_v2') + + project.repository.add_tag(user, 'v1', project.default_branch) + project.repository.add_tag(user, 'v2', 'branch_v2') + end + + let_it_be(:author) { create(:user, name: 'author') } + + let_it_be(:version1) do + create(:release, :with_catalog_resource_version, + project: project, + tag: 'v1', + sha: project.commit('v1').sha, + released_at: '2023-01-01T00:00:00Z', + author: author + ).catalog_resource_version + end + + let_it_be(:version2) do + create(:release, :with_catalog_resource_version, + project: project, + tag: 'v2', + sha: project.commit('v2').sha, + released_at: '2023-02-01T00:00:00Z', + author: author + ).catalog_resource_version + end + + describe 'versions' do + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { + id + versions { + nodes { id name - webUrl + path + releasedAt + author { + id + name + webUrl + } } } } } - } - GQL - end - - context 'when the resource has versions' do - let_it_be(:author) { create(:user, name: 'author') } - - let_it_be(:version1) do - create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', - author: author).catalog_resource_version - end - - let_it_be(:version2) do - create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', - author: author).catalog_resource_version + GQL end it 'returns the resource with the versions data' do @@ -202,67 +222,124 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly( a_graphql_entity_for( version1, - tagName: version1.name, - tagPath: project_tag_path(project, version1.name), + name: version1.name, + path: project_tag_path(project, version1.name), releasedAt: version1.released_at, author: a_graphql_entity_for(author, :name) ), a_graphql_entity_for( version2, - tagName: version2.name, - tagPath: project_tag_path(project, version2.name), + name: version2.name, + path: project_tag_path(project, version2.name), releasedAt: version2.released_at, author: a_graphql_entity_for(author, :name) ) ) end - end - context 'when the resource does not have a version' do - it 'returns versions as an empty array' do - post_query + context 'when the readmeHtml field is requested on more than one version' do + let(:query) do + <<~GQL + query { + ciCatalogResource(fullPath: "#{resource.project.full_path}") { + versions { + nodes { + readmeHtml + } + } + } + } + GQL + end - expect(graphql_data_at(:ciCatalogResource)).to match( - a_graphql_entity_for(resource, versions: { 'nodes' => [] }) - ) + it 'limits the request to 1 version at a time' do + post_query + + expect_graphql_errors_to_include \ + [/"readmeHtml" field can be requested only for 1 CiCatalogResourceVersion\(s\) at a time./] + end + end + + context 'when the name argument is provided' do + let(:name) { 'v1' } + + let(:query) do + <<~GQL + query { + ciCatalogResource(fullPath: "#{resource.project.full_path}") { + versions(name: "#{name}") { + nodes { + id + name + path + releasedAt + readmeHtml + } + } + } + } + GQL + end + + it 'returns the version that matches the name' do + post_query + + expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly( + a_graphql_entity_for( + version1, + name: version1.name, + path: project_tag_path(project, version1.name), + releasedAt: version1.released_at, + readmeHtml: a_string_including( + "#{project.full_path}/-/blob/#{project.default_branch}/README.md" + ) + ) + ) + end + + context 'when no version matches the name' do + let(:name) { 'does_not_exist' } + + it 'returns an empty array' do + post_query + + expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to eq([]) + end + end + end + + context 'when the resource does not have a version' do + it 'returns an empty array' do + resource.versions.delete_all(:delete_all) + + post_query + + expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to eq([]) + end end end - end - describe 'latestVersion' do - let(:query) do - <<~GQL - query { - ciCatalogResource(id: "#{resource.to_global_id}") { - id - latestVersion { + describe 'latestVersion' do + let(:query) do + <<~GQL + query { + ciCatalogResource(id: "#{resource.to_global_id}") { id - tagName - tagPath - releasedAt - author { + latestVersion { id name - webUrl + path + releasedAt + readmeHtml + author { + id + name + webUrl + } } } } - } - GQL - end - - context 'when the resource has versions' do - let_it_be(:author) { create(:user, name: 'author') } - - let_it_be(:latest_version) do - create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', - author: author).catalog_resource_version - end - - before_all do - # Previous version of the catalog resource - create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', - author: author) + GQL end it 'returns the resource with the latest version data' do @@ -272,24 +349,27 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio a_graphql_entity_for( resource, latestVersion: a_graphql_entity_for( - latest_version, - tagName: latest_version.name, - tagPath: project_tag_path(project, latest_version.name), - releasedAt: latest_version.released_at, + version2, + name: version2.name, + path: project_tag_path(project, version2.name), + releasedAt: version2.released_at, + readmeHtml: a_string_including('Readme v2'), author: a_graphql_entity_for(author, :name) ) ) ) end - end - context 'when the resource does not have a version' do - it 'returns nil' do - post_query + context 'when the resource does not have a version' do + it 'returns nil' do + resource.versions.delete_all(:delete_all) - expect(graphql_data_at(:ciCatalogResource)).to match( - a_graphql_entity_for(resource, latestVersion: nil) - ) + post_query + + expect(graphql_data_at(:ciCatalogResource)).to match( + a_graphql_entity_for(resource, latestVersion: nil) + ) + end end end end diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb index 49a3f3be1d7..150507fc442 100644 --- a/spec/requests/api/graphql/ci/catalog/resources_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -106,7 +106,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi versions { nodes { id - tagName + name releasedAt author { id @@ -153,7 +153,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi id latestVersion { id - tagName + name releasedAt author { id @@ -185,7 +185,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi resource1, latestVersion: a_graphql_entity_for( latest_version1, - tagName: latest_version1.name, + name: latest_version1.name, releasedAt: latest_version1.released_at, author: a_graphql_entity_for(author1, :name) ) @@ -194,7 +194,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi public_resource, latestVersion: a_graphql_entity_for( latest_version2, - tagName: latest_version2.name, + name: latest_version2.name, releasedAt: latest_version2.released_at, author: a_graphql_entity_for(author2, :name) ) diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index a612b4c91b6..6731631a075 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Query.ciVariables', feature_category: :secrets_management do nodes { id key + description value variableType protected @@ -36,6 +37,7 @@ RSpec.describe 'Query.ciVariables', feature_category: :secrets_management do expect(graphql_data.dig('ciVariables', 'nodes')).to contain_exactly({ 'id' => variable.to_global_id.to_s, 'key' => 'TEST_VAR', + 'description' => nil, 'value' => 'test', 'variableType' => 'ENV_VAR', 'masked' => false, diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 8262640b283..1b6948d0380 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -876,107 +876,95 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibi end describe 'Query limits' do - def runner_query(runner) - <<~SINGLE - runner(id: "#{runner.to_global_id}") { - #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} - createdBy { - id - username - webPath - webUrl - } - groups { - nodes { - id - path - fullPath - webUrl - } - } - projects { - nodes { - id - path - fullPath - webUrl - } - } - ownerProject { - id - path - fullPath - webUrl - } + let_it_be(:user2) { another_admin } + let_it_be(:user3) { create(:user) } + let_it_be(:tag_list) { %w[n_plus_1_test some_tag] } + let_it_be(:args) do + { current_user: user, token: { personal_access_token: create(:personal_access_token, user: user) } } + end + + let_it_be(:runner1) { create(:ci_runner, tag_list: tag_list, creator: user) } + let_it_be(:runner2) do + create(:ci_runner, :group, groups: [group], tag_list: tag_list, creator: user) + end + + let_it_be(:runner3) do + create(:ci_runner, :project, projects: [project1], tag_list: tag_list, creator: user) + end + + let(:single_discrete_runners_query) do + multiple_discrete_runners_query([]) + end + + let(:runner_fragment) do + <<~QUERY + #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} + createdBy { + id + username + webPath + webUrl } - SINGLE + QUERY end - let(:active_project_runner2) { create(:ci_runner, :project) } - let(:active_group_runner2) { create(:ci_runner, :group) } + # Exclude fields that are already hardcoded above (or tested separately), + # and also some fields from deeper objects which are problematic: + # - createdBy: Known N+1 issues, but only on exotic fields which we don't normally use + # - ownerProject.pipeline: Needs arguments (iid or sha) + # - project.productAnalyticsState: Can be requested only for 1 Project(s) at a time. + let(:excluded_fields) { %w[createdBy jobs pipeline productAnalyticsState] } + + it 'avoids N+1 queries', :use_sql_query_cache do + discrete_runners_control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(single_discrete_runners_query, **args) + end + + additional_runners = setup_additional_records + + expect do + post_graphql(multiple_discrete_runners_query(additional_runners), **args) - # Exclude fields that are already hardcoded above - let(:excluded_fields) { %w[createdBy jobs groups projects ownerProject] } + raise StandardError, flattened_errors if graphql_errors # Ensure any error in query causes test to fail + end.not_to exceed_query_limit(discrete_runners_control) + end - let(:single_query) do + def runner_query(runner, nr) <<~QUERY - { - instance_runner1: #{runner_query(active_instance_runner)} - group_runner1: #{runner_query(active_group_runner)} - project_runner1: #{runner_query(active_project_runner)} + runner#{nr}: runner(id: "#{runner.to_global_id}") { + #{runner_fragment} } QUERY end - let(:double_query) do + def multiple_discrete_runners_query(additional_runners) <<~QUERY { - instance_runner1: #{runner_query(active_instance_runner)} - instance_runner2: #{runner_query(inactive_instance_runner)} - group_runner1: #{runner_query(active_group_runner)} - group_runner2: #{runner_query(active_group_runner2)} - project_runner1: #{runner_query(active_project_runner)} - project_runner2: #{runner_query(active_project_runner2)} + #{runner_query(runner1, 1)} + #{runner_query(runner2, 2)} + #{runner_query(runner3, 3)} + #{additional_runners.each_with_index.map { |r, i| runner_query(r, 4 + i) }.join("\n")} } QUERY end - it 'does not execute more queries per runner', :aggregate_failures, quarantine: "https://gitlab.com/gitlab-org/gitlab/-/issues/391442" do - # warm-up license cache and so on: - 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, **args) } - - personal_access_token = create(:personal_access_token, user: another_admin) - args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } - expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control) - - expect(graphql_data.count).to eq 6 - expect(graphql_data).to match( - a_hash_including( - 'instance_runner1' => a_graphql_entity_for(active_instance_runner), - 'instance_runner2' => a_graphql_entity_for(inactive_instance_runner), - 'group_runner1' => a_graphql_entity_for( - active_group_runner, - groups: { 'nodes' => contain_exactly(a_graphql_entity_for(group)) } - ), - 'group_runner2' => a_graphql_entity_for( - active_group_runner2, - groups: { 'nodes' => active_group_runner2.groups.map { |g| a_graphql_entity_for(g) } } - ), - 'project_runner1' => a_graphql_entity_for( - active_project_runner, - projects: { 'nodes' => active_project_runner.projects.map { |p| a_graphql_entity_for(p) } }, - owner_project: a_graphql_entity_for(active_project_runner.projects[0]) - ), - 'project_runner2' => a_graphql_entity_for( - active_project_runner2, - projects: { 'nodes' => active_project_runner2.projects.map { |p| a_graphql_entity_for(p) } }, - owner_project: a_graphql_entity_for(active_project_runner2.projects[0]) - ) - )) + def setup_additional_records + # Add more runners (including owned by other users) + runner4 = create(:ci_runner, tag_list: tag_list + %w[tag1 tag2], creator: user2) + runner5 = create(:ci_runner, :group, groups: [create(:group)], tag_list: tag_list + %w[tag2 tag3], creator: user3) + # Add one more project to runner + runner3.assign_to(create(:project)) + + # Add more runner managers (including to existing runners) + runner_manager1 = create(:ci_runner_machine, runner: runner1) + create(:ci_runner_machine, runner: runner1) + create(:ci_runner_machine, runner: runner2, system_xid: runner_manager1.system_xid) + create(:ci_runner_machine, runner: runner3) + create(:ci_runner_machine, runner: runner4, version: '16.4.1') + create(:ci_runner_machine, runner: runner5, version: '16.4.0', system_xid: runner_manager1.system_xid) + create(:ci_runner_machine, runner: runner3) + + [runner4, runner5] end end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 0fe14bef778..bfe5282cbaa 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -18,22 +18,34 @@ RSpec.describe 'Query.runners', feature_category: :fleet_visibility do let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('CiRunner', excluded: %w[createdBy ownerProject])} - createdBy { - username - webPath - webUrl - } - ownerProject { - id - path - fullPath - webUrl - } + #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} } QUERY end + let(:query) do + %( + query { + runners { + #{fields} + } + } + ) + end + + # Exclude fields from deeper objects which are problematic: + # - ownerProject.pipeline: Needs arguments (iid or sha) + # - project.productAnalyticsState: Can be requested only for 1 Project(s) at a time. + let(:excluded_fields) { %w[pipeline productAnalyticsState] } + + it 'returns expected runners' do + post_graphql(query, current_user: current_user) + + expect(runners_graphql_data['nodes']).to contain_exactly( + *Ci::Runner.all.map { |expected_runner| a_graphql_entity_for(expected_runner) } + ) + end + context 'with filters' do shared_examples 'a working graphql query returning expected runners' do it_behaves_like 'a working graphql query' do @@ -49,31 +61,6 @@ RSpec.describe 'Query.runners', feature_category: :fleet_visibility do *Array(expected_runners).map { |expected_runner| a_graphql_entity_for(expected_runner) } ) end - - it 'does not execute more queries per runner', :aggregate_failures do - # warm-up license cache and so on: - personal_access_token = create(:personal_access_token, user: current_user) - args = { current_user: current_user, token: { personal_access_token: personal_access_token } } - post_graphql(query, **args) - expect(graphql_data_at(:runners, :nodes)).not_to be_empty - - admin2 = create(:admin) - personal_access_token = create(:personal_access_token, user: admin2) - args = { current_user: admin2, token: { personal_access_token: personal_access_token } } - control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } - - runner2 = create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2) - runner3 = create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8], - creator: current_user) - - create(:ci_build, :failed, runner: runner2) - create(:ci_runner_machine, runner: runner2, version: '16.4.1') - - create(:ci_build, :failed, runner: runner3) - create(:ci_runner_machine, runner: runner3, version: '16.4.0') - - expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) - end end context 'when filtered on type and status' do @@ -178,56 +165,129 @@ RSpec.describe 'Query.runners', feature_category: :fleet_visibility do end end end - end - context 'without filters' do - context 'with managers requested for multiple runners' do - let(:fields) do - <<~QUERY - nodes { - managers { - nodes { - #{all_graphql_fields_for('CiRunnerManager', max_depth: 1)} - } - } - } - QUERY - end + context 'when filtered by creator' do + let_it_be(:user) { create(:user) } + let_it_be(:runner_created_by_user) { create(:ci_runner, :project, creator: user) } let(:query) do %( query { - runners { + runners(creatorId: "#{creator.to_global_id}") { #{fields} } } ) end - it 'does not execute more queries per runner', :aggregate_failures do - # warm-up license cache and so on: - personal_access_token = create(:personal_access_token, user: current_user) - args = { current_user: current_user, token: { personal_access_token: personal_access_token } } - post_graphql(query, **args) - expect(graphql_data_at(:runners, :nodes)).not_to be_empty - - admin2 = create(:admin) - personal_access_token = create(:personal_access_token, user: admin2) - args = { current_user: admin2, token: { personal_access_token: personal_access_token } } - control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } - - create(:ci_runner, :instance, :with_runner_manager, version: '14.0.0', tag_list: %w[tag5 tag6], - creator: admin2) - create(:ci_runner, :project, :with_runner_manager, version: '14.0.1', projects: [project], - tag_list: %w[tag3 tag8], - creator: current_user) - - expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) + context 'when existing user id given' do + let(:creator) { user } + + before do + create(:ci_runner, :project, creator: create(:user)) # Should not be returned + end + + it_behaves_like 'a working graphql query returning expected runners' do + let(:expected_runners) { runner_created_by_user } + end + end + + context 'when non existent user id given' do + let(:creator) { User.new(id: non_existing_record_id) } + + it 'does not return any runners' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:runners, :nodes)).to be_empty + end end end end end + describe 'Runner query limits' do + let_it_be(:user) { create(:user, :admin) } + let_it_be(:user2) { create(:user) } + let_it_be(:user3) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project) } + let_it_be(:tag_list) { %w[n_plus_1_test some_tag] } + let_it_be(:args) do + { current_user: user, token: { personal_access_token: create(:personal_access_token, user: user) } } + end + + let_it_be(:runner1) { create(:ci_runner, tag_list: tag_list, creator: user) } + let_it_be(:runner2) do + create(:ci_runner, :group, groups: [group], tag_list: tag_list, creator: user) + end + + let_it_be(:runner3) do + create(:ci_runner, :project, projects: [project], tag_list: tag_list, creator: user) + end + + let(:runner_fragment) do + <<~QUERY + #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} + createdBy { + id + username + webPath + webUrl + } + QUERY + end + + # Exclude fields that are already hardcoded above (or tested separately), + # and also some fields from deeper objects which are problematic: + # - createdBy: Known N+1 issues, but only on exotic fields which we don't normally use + # - ownerProject.pipeline: Needs arguments (iid or sha) + # - project.productAnalyticsState: Can be requested only for 1 Project(s) at a time. + let(:excluded_fields) { %w[createdBy jobs pipeline productAnalyticsState] } + + let(:runners_query) do + <<~QUERY + { + runners { + nodes { #{runner_fragment} } + } + } + QUERY + end + + it 'avoids N+1 queries', :use_sql_query_cache do + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + + runners_control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(runners_query, **args) } + + setup_additional_records + + expect { post_graphql(runners_query, **args) }.not_to exceed_query_limit(runners_control) + end + + def setup_additional_records + # Add more runners (including owned by other users) + runner4 = create(:ci_runner, tag_list: tag_list + %w[tag1 tag2], creator: user2) + runner5 = create(:ci_runner, :group, groups: [create(:group)], tag_list: tag_list + %w[tag2 tag3], creator: user3) + # Add one more project to runner + runner3.assign_to(create(:project)) + + # Add more runner managers (including to existing runners) + runner_manager1 = create(:ci_runner_machine, runner: runner1) + create(:ci_runner_machine, runner: runner1) + create(:ci_runner_machine, runner: runner2, system_xid: runner_manager1.system_xid) + create(:ci_runner_machine, runner: runner3) + create(:ci_runner_machine, runner: runner4, version: '16.4.1') + create(:ci_runner_machine, runner: runner5, version: '16.4.0', system_xid: runner_manager1.system_xid) + create(:ci_runner_machine, runner: runner3) + + create(:ci_build, :failed, runner: runner4) + create(:ci_build, :failed, runner: runner5) + + [runner4, runner5] + end + end + describe 'pagination' do let(:data_path) { [:runners] } diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 2acdd509355..46563aba992 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -430,6 +430,85 @@ RSpec.describe 'container repository details', feature_category: :container_regi it_behaves_like 'returning an invalid value error' end + + context 'with referrers' do + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:raw_tags_response) do + [ + { + name: 'latest', + digest: 'sha256:1234567892', + config_digest: 'sha256:3332132331', + media_type: 'application/vnd.oci.image.manifest.v1+json', + size_bytes: 1234509876, + created_at: 10.minutes.ago, + updated_at: 10.minutes.ago, + referrers: [ + { + artifactType: 'application/vnd.example+type', + digest: 'sha256:57d3be92c2f857566ecc7f9306a80021c0a7fa631e0ef5146957235aea859961' + }, + { + artifactType: 'application/vnd.example+type+2', + digest: 'sha256:01db72e42d61b8d2183d53475814cce2bfb9c8a254e97539a852441979cd5c90' + } + ] + }, + { + name: 'latest', + digest: 'sha256:1234567893', + config_digest: 'sha256:3332132331', + media_type: 'application/vnd.oci.image.manifest.v1+json', + size_bytes: 1234509877, + created_at: 9.minutes.ago, + updated_at: 9.minutes.ago + } + ] + end + + let(:query) do + <<~GQL + query($id: ContainerRepositoryID!, $n: String) { + containerRepository(id: $id) { + tags(name: $n, referrers: true) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + let(:url) { URI('/gitlab/v1/repositories/group1/proj1/tags/list/?before=tag1&referrers=true') } + + let(:response_body) do + { + pagination: { previous: { uri: url }, next: { uri: url } }, + response_body: ::Gitlab::Json.parse(raw_tags_response.to_json) + } + end + + it 'includes referrers in response' do + subject + + refs = tags_response.map { |tag| tag.dig('node', 'referrers') } + + expect(refs.first.size).to eq(2) + expect(refs.first.first).to include({ + 'artifactType' => 'application/vnd.example+type', + 'digest' => 'sha256:57d3be92c2f857566ecc7f9306a80021c0a7fa631e0ef5146957235aea859961' + }) + expect(refs.first.second).to include({ + 'artifactType' => 'application/vnd.example+type+2', + 'digest' => 'sha256:01db72e42d61b8d2183d53475814cce2bfb9c8a254e97539a852441979cd5c90' + }) + + expect(refs.second).to be_empty + end + end end it_behaves_like 'handling graphql network errors with the container registry' diff --git a/spec/requests/api/graphql/mutations/branch_rules/create_spec.rb b/spec/requests/api/graphql/mutations/branch_rules/create_spec.rb new file mode 100644 index 00000000000..85ba3d58ee5 --- /dev/null +++ b/spec/requests/api/graphql/mutations/branch_rules/create_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'BranchRuleCreate', feature_category: :source_code_management do + include GraphqlHelpers + let_it_be(:project) { create(:project, :public) } + let_it_be(:current_user, reload: true) { create(:user) } + + let(:params) do + { + project_path: project.full_path, + name: branch_name + } + end + + let(:branch_name) { 'branch_name/*' } + let(:mutation) { graphql_mutation(:branch_rule_create, params) } + let(:mutation_response) { graphql_mutation_response(:branch_rule_create) } + let(:mutation_errors) { mutation_response['errors'] } + + subject(:post_mutation) { post_graphql_mutation(mutation, current_user: current_user) } + + context 'when the user does not have permission' do + before_all do + project.add_developer(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create the board' do + expect { post_mutation }.not_to change { ProtectedBranch.count } + end + end + + context 'when the user can create a branch rules' do + before_all do + project.add_maintainer(current_user) + end + + it 'creates the protected branch' do + expect { post_mutation }.to change { ProtectedBranch.count }.by(1) + end + + it 'returns the created branch rule' do + post_mutation + + expect(mutation_response).to have_key('branchRule') + expect(mutation_response['branchRule']['name']).to eq(branch_name) + expect(mutation_errors).to be_empty + end + + context 'when the branch rule already exist' do + let!(:existing_rule) { create :protected_branch, name: branch_name, project: project } + + it 'does not create the protected branch' do + expect { post_mutation }.not_to change { ProtectedBranch.count } + end + + it 'return an error message' do + post_mutation + + expect(mutation_errors).to include 'Name has already been taken' + expect(mutation_response['branchRule']).to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/ml/models/create_spec.rb b/spec/requests/api/graphql/mutations/ml/models/create_spec.rb new file mode 100644 index 00000000000..0daabeab0d1 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ml/models/create_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creation of a machine learning model', feature_category: :mlops do + include GraphqlHelpers + + let_it_be(:model) { create(:ml_models) } + let_it_be(:project) { model.project } + let_it_be(:current_user) { project.owner } + + let(:input) { { project_path: project.full_path, name: name, description: description } } + let(:name) { 'some_name' } + let(:description) { 'A description' } + + let(:mutation) { graphql_mutation(:ml_model_create, input) } + let(:mutation_response) { graphql_mutation_response(:ml_model_create) } + + context 'when user is not allowed write changes' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :write_model_registry, project) + .and_return(false) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is allowed write changes' do + it 'creates a models' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['model']).to include( + 'name' => name, + 'description' => description + ) + end + + context 'when name already exists' do + err_msg = "Name has already been taken" + let(:name) { model.name } + + it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg] + end + end +end diff --git a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb index 05c1a2d96d9..7c5d86b9f5c 100644 --- a/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb +++ b/spec/requests/api/graphql/mutations/namespace/package_settings/update_spec.rb @@ -23,7 +23,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis lock_npm_package_requests_forwarding: true, pypi_package_requests_forwarding: true, lock_pypi_package_requests_forwarding: true, - nuget_symbol_server_enabled: true + nuget_symbol_server_enabled: true, + terraform_module_duplicates_allowed: true, + terraform_module_duplicate_exception_regex: 'foo-.*' } end @@ -44,6 +46,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis pypiPackageRequestsForwarding lockPypiPackageRequestsForwarding nugetSymbolServerEnabled + terraformModuleDuplicatesAllowed + terraformModuleDuplicateExceptionRegex } errors QL @@ -73,6 +77,8 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis expect(package_settings_response['npmPackageRequestsForwarding']).to eq(params[:npm_package_requests_forwarding]) expect(package_settings_response['lockNpmPackageRequestsForwarding']).to eq(params[:lock_npm_package_requests_forwarding]) expect(package_settings_response['nugetSymbolServerEnabled']).to eq(params[:nuget_symbol_server_enabled]) + expect(package_settings_response['terraformModuleDuplicatesAllowed']).to eq(params[:terraform_module_duplicates_allowed]) + expect(package_settings_response['terraformModuleDuplicateExceptionRegex']).to eq(params[:terraform_module_duplicate_exception_regex]) end end @@ -115,7 +121,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis lock_npm_package_requests_forwarding: false, pypi_package_requests_forwarding: nil, lock_pypi_package_requests_forwarding: false, - nuget_symbol_server_enabled: false + nuget_symbol_server_enabled: false, + terraform_module_duplicates_allowed: false, + terraform_module_duplicate_exception_regex: 'foo' }, to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'foo-.*', @@ -129,7 +137,9 @@ RSpec.describe 'Updating the package settings', feature_category: :package_regis lock_npm_package_requests_forwarding: true, pypi_package_requests_forwarding: true, lock_pypi_package_requests_forwarding: true, - nuget_symbol_server_enabled: true + nuget_symbol_server_enabled: true, + terraform_module_duplicates_allowed: true, + terraform_module_duplicate_exception_regex: 'foo-.*' } it_behaves_like 'returning a success' diff --git a/spec/requests/api/graphql/mutations/organizations/update_spec.rb b/spec/requests/api/graphql/mutations/organizations/update_spec.rb index 4e819c280d0..33890ae4592 100644 --- a/spec/requests/api/graphql/mutations/organizations/update_spec.rb +++ b/spec/requests/api/graphql/mutations/organizations/update_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Mutations::Organizations::Update, feature_category: :cell do let_it_be(:user) { create(:user) } let_it_be_with_reload(:organization) do - create(:organization) { |org| create(:organization_user, organization: org, user: user) } + create(:organization) { |org| create(:organization_user, :owner, organization: org, user: user) } end let(:mutation) { graphql_mutation(:organization_update, params) } diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 78b93c3210b..2c2cd5f2acc 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -281,6 +281,18 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do it_behaves_like 'creates work item' + # This is a temporary measure just to ensure the internal id migration doesn't get conflicts + # More info in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139367 + context 'when making the request in a production environment' do + before do + stub_rails_env('production') + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [ + 'Group level work items are disabled. Only project paths allowed in `namespacePath`.' + ] + end + context 'when the namespace_level_work_items feature flag is disabled' do before do stub_feature_flags(namespace_level_work_items: false) diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index a4bc94798be..107fcd8dcdd 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -20,6 +20,7 @@ RSpec.describe 'getting projects', feature_category: :groups_and_projects do 'namespace', { 'fullPath' => subject.full_path }, <<~QUERY + id projects(includeSubgroups: #{include_subgroups}) { edges { node { @@ -53,24 +54,30 @@ RSpec.describe 'getting projects', feature_category: :groups_and_projects do expect(graphql_data['namespace']['projects']['edges'].size).to eq(count) end + end - context 'with no user' do - it 'finds only public projects' do - post_graphql(query, current_user: nil) + it_behaves_like 'a graphql namespace' - expect(graphql_data['namespace']).to be_nil - end + context 'when no user is given' do + it 'finds only public projects' do + post_graphql(query, current_user: nil) + + expect(graphql_data_at(:namespace, :projects, :edges).size).to eq(1) end end - it_behaves_like 'a graphql namespace' - context 'when the namespace is a user' do subject { user.namespace } let(:include_subgroups) { false } it_behaves_like 'a graphql namespace' + + it 'does not show namespace entity for anonymous user' do + post_graphql(query, current_user: nil) + + expect(graphql_data['namespace']).to be_nil + end end context 'when not including subgroups' do diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb index c8819f1e38f..273b6b8c25b 100644 --- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'rendering namespace statistics', feature_category: :metrics do it 'hides statistics for unauthenticated requests' do post_graphql(query, current_user: nil) - expect(graphql_data['namespace']).to be_blank + expect(graphql_data_at(:namespace, :root_storage_statistics)).to be_blank end end end diff --git a/spec/requests/api/graphql/namespace_query_spec.rb b/spec/requests/api/graphql/namespace_query_spec.rb index c0c7c5fee2b..86808915564 100644 --- a/spec/requests/api/graphql/namespace_query_spec.rb +++ b/spec/requests/api/graphql/namespace_query_spec.rb @@ -8,7 +8,8 @@ RSpec.describe 'Query', feature_category: :groups_and_projects do let_it_be(:user) { create(:user) } let_it_be(:other_user) { create(:user) } - let_it_be(:group_namespace) { create(:group) } + let_it_be(:group_namespace) { create(:group, :private) } + let_it_be(:public_group_namespace) { create(:group, :public) } let_it_be(:user_namespace) { create(:user_namespace, owner: user) } let_it_be(:project_namespace) { create(:project_namespace, parent: group_namespace) } @@ -60,6 +61,51 @@ RSpec.describe 'Query', feature_category: :groups_and_projects do end end + context 'when used with a public group' do + let(:target_namespace) { public_group_namespace } + + before do + subject + end + + it_behaves_like 'a working graphql query' + + context 'when user is a member' do + before do + public_group_namespace.add_developer(user) + end + + it 'fetches the expected data' do + expect(query_result).to include( + 'fullPath' => target_namespace.full_path, + 'name' => target_namespace.name + ) + end + end + + context 'when user is anonymous' do + let(:current_user) { nil } + + it 'fetches the expected data' do + expect(query_result).to include( + 'fullPath' => target_namespace.full_path, + 'name' => target_namespace.name + ) + end + end + + context 'when user is not a member' do + let(:current_user) { other_user } + + it 'fetches the expected data' do + expect(query_result).to include( + 'fullPath' => target_namespace.full_path, + 'name' => target_namespace.name + ) + end + end + end + it_behaves_like 'retrieving a namespace' do let(:target_namespace) { group_namespace } diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index c485e3b170d..14becd52e93 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -7,7 +7,6 @@ RSpec.describe 'getting organization information', feature_category: :cell do let(:query) { graphql_query_for(:organization, { id: organization.to_global_id }, organization_fields) } let(:current_user) { user } - let(:groups) { graphql_data_at(:organization, :groups, :nodes) } let(:organization_fields) do <<~FIELDS id @@ -23,24 +22,9 @@ RSpec.describe 'getting organization information', feature_category: :cell do let_it_be(:organization_user) { create(:organization_user) } let_it_be(:organization) { organization_user.organization } let_it_be(:user) { organization_user.user } - let_it_be(:parent_group) { create(:group, name: 'parent-group', organization: organization) } - let_it_be(:public_group) { create(:group, name: 'public-group', parent: parent_group, organization: organization) } - let_it_be(:other_group) { create(:group, name: 'other-group', organization: organization) } - let_it_be(:outside_organization_group) { create(:group) } - - let_it_be(:private_group) do - create(:group, :private, name: 'private-group', organization: organization) - end - - let_it_be(:no_access_group_in_org) do - create(:group, :private, name: 'no-access', organization: organization) - end - - before_all do - private_group.add_developer(user) - public_group.add_developer(user) - other_group.add_developer(user) - outside_organization_group.add_developer(user) + let_it_be(:project) { create(:project, organization: organization) { |p| p.add_developer(user) } } + let_it_be(:other_group) do + create(:group, name: 'other-group', organization: organization) { |g| g.add_developer(user) } end subject(:request_organization) { post_graphql(query, current_user: current_user) } @@ -62,25 +46,6 @@ RSpec.describe 'getting organization information', feature_category: :cell do end end - context 'when resolve_organization_groups feature flag is disabled' do - before do - stub_feature_flags(resolve_organization_groups: false) - end - - it 'returns no groups' do - request_organization - - expect(graphql_data_at(:organization)).not_to be_nil - expect(graphql_data_at(:organization, :groups, :nodes)).to be_empty - end - end - - it 'does not return ancestors of authorized groups' do - request_organization - - expect(groups.pluck('id')).not_to include(parent_group.to_global_id.to_s) - end - context 'when requesting organization user' do let(:organization_fields) do <<~FIELDS @@ -102,13 +67,13 @@ RSpec.describe 'getting organization information', feature_category: :cell do it 'returns correct organization user fields' do request_organization - organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first + organization_user_nodes = graphql_data_at(:organization, :organizationUsers, :nodes) expected_attributes = { "badges" => [{ "text" => "It's you!", "variant" => 'muted' }], "id" => organization_user.to_global_id.to_s, "user" => { "id" => user.to_global_id.to_s } } - expect(organization_user_node).to match(expected_attributes) + expect(organization_user_nodes).to include(expected_attributes) end it 'avoids N+1 queries for all the fields' do @@ -116,6 +81,8 @@ RSpec.describe 'getting organization information', feature_category: :cell do organization_user_2 = create(:organization_user, organization: organization) other_group.add_developer(organization_user_2.user) + organization_user_from_project = create(:organization_user, organization: organization) + project.add_developer(organization_user_from_project.user) expect { run_query }.not_to exceed_query_limit(base_query_count) end @@ -127,62 +94,144 @@ RSpec.describe 'getting organization information', feature_category: :cell do end end - context 'with `search` argument' do - let(:search) { 'oth' } - let(:organization_fields) do - <<~FIELDS - id - path - groups(search: "#{search}") { - nodes { - id - name - } - } - FIELDS + context 'when requesting groups' do + let(:groups) { graphql_data_at(:organization, :groups, :nodes) } + let_it_be(:parent_group) { create(:group, name: 'parent-group', organization: organization) } + let_it_be(:public_group) do + create(:group, name: 'public-group', parent: parent_group, organization: organization) end - it 'filters groups by name' do - request_organization + let_it_be(:private_group) do + create(:group, :private, name: 'private-group', organization: organization) + end - expect(groups).to contain_exactly(a_graphql_entity_for(other_group)) + before_all do + create(:group, :private, name: 'no-access', organization: organization) + private_group.add_developer(user) + public_group.add_developer(user) + create(:group) { |g| g.add_developer(user) } # outside organization end - end - context 'with `sort` argument' do - using RSpec::Parameterized::TableSyntax + context 'when resolve_organization_groups feature flag is disabled' do + before do + stub_feature_flags(resolve_organization_groups: false) + end + + it 'returns no groups' do + request_organization + + expect(graphql_data_at(:organization)).not_to be_nil + expect(graphql_data_at(:organization, :groups, :nodes)).to be_empty + end + end - let(:authorized_groups) { [public_group, private_group, other_group] } + it 'does not return ancestors of authorized groups' do + request_organization - where(:field, :direction, :sorted_groups) do - 'id' | 'asc' | lazy { authorized_groups.sort_by(&:id) } - 'id' | 'desc' | lazy { authorized_groups.sort_by(&:id).reverse } - 'name' | 'asc' | lazy { authorized_groups.sort_by(&:name) } - 'name' | 'desc' | lazy { authorized_groups.sort_by(&:name).reverse } - 'path' | 'asc' | lazy { authorized_groups.sort_by(&:path) } - 'path' | 'desc' | lazy { authorized_groups.sort_by(&:path).reverse } + expect(groups.pluck('id')).not_to include(parent_group.to_global_id.to_s) end - with_them do - let(:sort) { "#{field}_#{direction}".upcase } + context 'with `search` argument' do + let(:search) { 'oth' } let(:organization_fields) do <<~FIELDS id path - groups(sort: #{sort}) { + groups(search: "#{search}") { nodes { id + name } } FIELDS end - it 'sorts the groups' do + it 'filters groups by name' do request_organization - expect(groups.pluck('id')).to eq(sorted_groups.map(&:to_global_id).map(&:to_s)) + expect(groups).to contain_exactly(a_graphql_entity_for(other_group)) end end + + context 'with `sort` argument' do + using RSpec::Parameterized::TableSyntax + + let(:authorized_groups) { [public_group, private_group, other_group] } + + where(:field, :direction, :sorted_groups) do + 'id' | 'asc' | lazy { authorized_groups.sort_by(&:id) } + 'id' | 'desc' | lazy { authorized_groups.sort_by(&:id).reverse } + 'name' | 'asc' | lazy { authorized_groups.sort_by(&:name) } + 'name' | 'desc' | lazy { authorized_groups.sort_by(&:name).reverse } + 'path' | 'asc' | lazy { authorized_groups.sort_by(&:path) } + 'path' | 'desc' | lazy { authorized_groups.sort_by(&:path).reverse } + end + + with_them do + let(:sort) { "#{field}_#{direction}".upcase } + let(:organization_fields) do + <<~FIELDS + id + path + groups(sort: #{sort}) { + nodes { + id + } + } + FIELDS + end + + it 'sorts the groups' do + request_organization + + expect(groups.pluck('id')).to eq(sorted_groups.map(&:to_global_id).map(&:to_s)) + end + end + end + end + + context 'when requesting projects' do + let(:projects) { graphql_data_at(:organization, :projects, :nodes) } + let(:organization_fields) do + <<~FIELDS + projects { + nodes { + id + } + } + FIELDS + end + + before_all do + create(:project) { |p| p.add_developer(user) } # some other project that shouldn't show up in our results + end + + before do + request_organization + end + + it_behaves_like 'a working graphql query' + + it 'returns projects' do + expect(projects).to contain_exactly(a_graphql_entity_for(project)) + end + + it_behaves_like 'sorted paginated query' do + include_context 'no sort argument' + + let_it_be(:another_project) { create(:project, organization: organization) { |p| p.add_developer(user) } } + let_it_be(:another_project2) { create(:project, organization: organization) { |p| p.add_developer(user) } } + let(:first_param) { 2 } + let(:data_path) { [:organization, :projects] } + let(:all_records) { [another_project2, another_project, project].map { |p| global_id_of(p).to_s } } + end + + def pagination_query(params) + graphql_query_for( + :organization, { id: organization.to_global_id }, + query_nodes(:projects, :id, include_pagination_info: true, args: params) + ) + end end end end diff --git a/spec/requests/api/graphql/organizations/organizations_query_spec.rb b/spec/requests/api/graphql/organizations/organizations_query_spec.rb new file mode 100644 index 00000000000..12d81ed7412 --- /dev/null +++ b/spec/requests/api/graphql/organizations/organizations_query_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting organizations information', feature_category: :cell do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + + let(:query) { graphql_query_for(:organizations, organizations_fields) } + let(:organizations) { graphql_data_at(:organizations, :nodes) } + let(:organizations_fields) do + <<~FIELDS + nodes { + id + path + } + FIELDS + end + + before_all { create_list(:organization, 3) } + + subject(:request_organization) { post_graphql(query, current_user: current_user) } + + context 'without authenticated user' do + let(:current_user) { nil } + + it_behaves_like 'a working graphql query' do + before do + request_organization + end + end + end + + context 'with authenticated user' do + let(:current_user) { user } + + it_behaves_like 'a working graphql query' do + before do + request_organization + end + end + + it_behaves_like 'sorted paginated query' do + include_context 'no sort argument' + + let(:first_param) { 2 } + let(:data_path) { [:organizations] } + let(:all_records) { Organizations::Organization.order(id: :desc).map { |o| global_id_of(o).to_s } } + end + + def pagination_query(params) + graphql_query_for(:organizations, params, "#{page_info} nodes { id }") + end + end +end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index c86d3bdd14c..2307409c383 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project', feature_category: let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } let_it_be(:container_expiration_policy) { project.container_expiration_policy } - let(:excluded_fields) { %w[pipeline jobs productAnalyticsState] } + let(:excluded_fields) { %w[pipeline jobs productAnalyticsState mlModels] } let(:container_repositories_fields) do <<~GQL edges { @@ -155,7 +155,7 @@ RSpec.describe 'getting container repositories in a project', feature_category: it_behaves_like 'handling graphql network errors with the container registry' it_behaves_like 'not hitting graphql network errors with the container registry' do - let(:excluded_fields) { %w[pipeline jobs tags tagsCount productAnalyticsState] } + let(:excluded_fields) { %w[pipeline jobs tags tagsCount productAnalyticsState mlModels] } end it 'returns the total count of container repositories' do diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index 23be9fa5286..96933505838 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'getting merge request information nested in a project', feature_ # we exclude Project.pipeline because it needs arguments, # codequalityReportsComparer because it is behind a feature flag # and runners because the user is not an admin and therefore has no access - let(:excluded) { %w[jobs pipeline runners codequalityReportsComparer] } + let(:excluded) { %w[jobs pipeline runners codequalityReportsComparer mlModels] } let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: excluded) } before do diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb index 77b72bf39a1..d71908d6458 100644 --- a/spec/requests/api/graphql/project/tree/tree_spec.rb +++ b/spec/requests/api/graphql/project/tree/tree_spec.rb @@ -167,6 +167,8 @@ RSpec.describe 'getting a tree in a project', feature_category: :source_code_man end context 'when the ref points to a SSH-signed commit' do + let_it_be(:project) { create(:project, :repository, :in_group) } + let_it_be(:ref) { 'ssh-signed-commit' } let_it_be(:commit) { project.commit(ref) } let_it_be(:current_user) { create(:user, email: commit.committer_email).tap { |user| project.add_owner(user) } } diff --git a/spec/requests/api/graphql/projects/projects_spec.rb b/spec/requests/api/graphql/projects/projects_spec.rb index 84b8c2285f0..dfebcb7c42c 100644 --- a/spec/requests/api/graphql/projects/projects_spec.rb +++ b/spec/requests/api/graphql/projects/projects_spec.rb @@ -45,14 +45,14 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod it 'avoids N+1 queries', :use_sql_query_cache, :clean_gitlab_redis_cache do post_graphql(single_project_query, current_user: current_user) - query_count = ActiveRecord::QueryRecorder.new do + control = ActiveRecord::QueryRecorder.new do post_graphql(single_project_query, current_user: current_user) - end.count + end # There is an N+1 query for max_member_access_for_user_ids expect do post_graphql(query, current_user: current_user) - end.not_to exceed_all_query_limit(query_count + 5) + end.not_to exceed_all_query_limit(control).with_threshold(5) end it 'returns the expected projects' do diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb index 2e6c3dcba61..ccff5bdf919 100644 --- a/spec/requests/api/graphql/user/user_achievements_query_spec.rb +++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb @@ -60,14 +60,14 @@ RSpec.describe 'UserAchievements', feature_category: :user_profile do end it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: user) - end.count + end achievement2 = create(:achievement, namespace: group) create_list(:user_achievement, 2, achievement: achievement2, user: user) - expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count) + expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control) end context 'when the achievements feature flag is disabled for a namespace' do diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index fe77b7ae736..c6d44b057a7 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -199,7 +199,7 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do it 'avoids N+1 queries' do post_graphql(query, current_user: current_user) # warm up - control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: current_user) end @@ -207,7 +207,7 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do expect do post_graphql(query, current_user: current_user) - end.not_to exceed_all_query_limit(control_count) + end.not_to exceed_all_query_limit(control) end context 'when user is guest' do |