diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9e112f4e5f02dfd5b625d160556bdf239fa480be..2a1dcbaaf1dfdc485e43630f6672573c1f2771a9 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -395,8 +395,6 @@ def by_negated_assignee(items) # We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB" if not_params.assignees.present? items.not_assigned_to(not_params.assignees) - elsif not_params.assignee_id? || not_params.assignee_username? # assignee not found - items.none else items end diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 84b0dafe213228d6073e8f26df6476b91bf5396e..f95a676054adf978628f57e663aca567e8aa31b3 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -12,10 +12,10 @@ module IssueResolverArguments argument :iids, [GraphQL::STRING_TYPE], required: false, description: 'List of IIDs of issues. For example, [1, 2].' - argument :label_name, GraphQL::STRING_TYPE.to_list_type, + argument :label_name, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Labels applied to this issue.' - argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, + argument :milestone_title, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Milestone applied to this issue.' argument :author_username, GraphQL::STRING_TYPE, @@ -55,6 +55,10 @@ module IssueResolverArguments as: :issue_types, description: 'Filter issues by the given issue types.', required: false + argument :not, Types::Issues::NegatedIssueFilterInputType, + description: 'List of negated params.', + prepare: ->(negated_args, ctx) { negated_args.to_h }, + required: false end def resolve_with_lookahead(**args) @@ -69,11 +73,22 @@ def resolve_with_lookahead(**args) args[:iids] ||= [args.delete(:iid)].compact if args[:iid] args[:attempt_project_search_optimizations] = true if args[:search].present? + prepare_assignee_username_params(args) + finder = IssuesFinder.new(current_user, args) continue_issue_resolve(parent, finder, **args) end + def ready?(**args) + if args.slice(*mutually_exclusive_assignee_username_args).compact.size > 1 + arg_str = mutually_exclusive_assignee_username_args.map { |x| x.to_s.camelize(:lower) }.join(', ') + raise Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time." + end + + super + end + class_methods do def resolver_complexity(args, child_complexity:) complexity = super @@ -82,4 +97,15 @@ def resolver_complexity(args, child_complexity:) complexity end end + + private + + def prepare_assignee_username_params(args) + args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present? + args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present? + end + + def mutually_exclusive_assignee_username_args + [:assignee_usernames, :assignee_username] + end end diff --git a/app/graphql/types/boards/board_issuable_input_base_type.rb b/app/graphql/types/boards/board_issuable_input_base_type.rb index a70d90ce253f1725153a2733ac8a93b88ddda4ba..2cd057347d60753e9e330e853bdd6e8a252f759f 100644 --- a/app/graphql/types/boards/board_issuable_input_base_type.rb +++ b/app/graphql/types/boards/board_issuable_input_base_type.rb @@ -4,7 +4,7 @@ module Types module Boards # Common arguments that we can be used to filter boards epics and issues class BoardIssuableInputBaseType < BaseInputObject - argument :label_name, GraphQL::STRING_TYPE.to_list_type, + argument :label_name, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Filter by label name.' diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb index 5bd2334c7221d25ff53d5361377ebf4d7113ce6b..7cf2dcb9c8294920e588d6f200d3e14ae23b2347 100644 --- a/app/graphql/types/boards/board_issue_input_base_type.rb +++ b/app/graphql/types/boards/board_issue_input_base_type.rb @@ -8,7 +8,7 @@ class BoardIssueInputBaseType < BoardIssuableInputBaseType required: false, description: 'Filter by milestone title.' - argument :assignee_username, GraphQL::STRING_TYPE.to_list_type, + argument :assignee_username, [GraphQL::STRING_TYPE, null: true], required: false, description: 'Filter by assignee username.' diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..10bf6f21792e9da51c24a650481bb31dbeb78af8 --- /dev/null +++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Issues + class NegatedIssueFilterInputType < BaseInputObject + graphql_name 'NegatedIssueFilterInput' + + argument :iids, [GraphQL::STRING_TYPE], + required: false, + description: 'List of IIDs of issues to exclude. For example, [1, 2].' + argument :label_name, [GraphQL::STRING_TYPE], + required: false, + description: 'Labels not applied to this issue.' + argument :milestone_title, [GraphQL::STRING_TYPE], + required: false, + description: 'Milestone not applied to this issue.' + argument :assignee_usernames, [GraphQL::STRING_TYPE], + required: false, + description: 'Usernames of users not assigned to the issue.' + argument :assignee_id, GraphQL::STRING_TYPE, + required: false, + description: 'ID of a user not assigned to the issues.' + end + end +end + +Types::Issues::NegatedIssueFilterInputType.prepend_if_ee('::EE::Types::Issues::NegatedIssueFilterInputType') diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index 3f54aa4817d15cbf7add4e650d9edc0c5a77bcc9..9efea284b5ce0e12c4a254772a939e537197181f 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -52,8 +52,6 @@ def zip_source return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project, default_enabled: :yaml) - return if deployment.migrated? && !Feature.enabled?(:pages_serve_from_migrated_zip, project, default_enabled: true) - global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s { diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml index 2ddfcf437563c0bd29d66f808b5ac767943e3a5c..4d0858165a28b564d02272b7c9edb47e6c48cf10 100644 --- a/app/views/shared/deploy_tokens/_form.html.haml +++ b/app/views/shared/deploy_tokens/_form.html.haml @@ -22,29 +22,29 @@ = f.label :scopes, _('Scopes [Select 1 or more]'), class: 'label-bold' %fieldset.form-group.form-check = f.check_box :read_repository, class: 'form-check-input qa-deploy-token-read-repository' - = label_tag ("deploy_token_read_repository"), 'read_repository', class: 'label-bold form-check-label' + = f.label :read_repository, 'read_repository', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows read-only access to the repository.') - if container_registry_enabled?(group_or_project) %fieldset.form-group.form-check = f.check_box :read_registry, class: 'form-check-input qa-deploy-token-read-registry' - = label_tag ("deploy_token_read_registry"), 'read_registry', class: 'label-bold form-check-label' + = f.label :read_registry, 'read_registry', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows read-only access to registry images.') %fieldset.form-group.form-check = f.check_box :write_registry, class: 'form-check-input' - = label_tag ("deploy_token_write_registry"), 'write_registry', class: 'label-bold form-check-label' + = f.label :write_registry, 'write_registry', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows write access to registry images.') - if packages_registry_enabled?(group_or_project) %fieldset.form-group.form-check = f.check_box :read_package_registry, class: 'form-check-input' - = label_tag ("deploy_token_read_package_registry"), 'read_package_registry', class: 'label-bold form-check-label' + = f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows read access to the package registry.') %fieldset.form-group.form-check = f.check_box :write_package_registry, class: 'form-check-input' - = label_tag ("deploy_token_write_package_registry"), 'write_package_registry', class: 'label-bold form-check-label' + = f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows write access to the package registry.') .gl-mt-3 diff --git a/changelogs/unreleased/300021-rollout-serving-migrated-data-feature-flag-pages_serve_from_migrat.yml b/changelogs/unreleased/300021-rollout-serving-migrated-data-feature-flag-pages_serve_from_migrat.yml new file mode 100644 index 0000000000000000000000000000000000000000..ccf19216909be1852a6c019bb8330469b44c9237 --- /dev/null +++ b/changelogs/unreleased/300021-rollout-serving-migrated-data-feature-flag-pages_serve_from_migrat.yml @@ -0,0 +1,5 @@ +--- +title: Remove pages_serve_from_migrated_zip feature flag +merge_request: 59002 +author: +type: added diff --git a/changelogs/unreleased/300115-support-negated-issue-filters-gql.yml b/changelogs/unreleased/300115-support-negated-issue-filters-gql.yml new file mode 100644 index 0000000000000000000000000000000000000000..356a5aed1c3cf422bbbd20653baedb73a45420d7 --- /dev/null +++ b/changelogs/unreleased/300115-support-negated-issue-filters-gql.yml @@ -0,0 +1,5 @@ +--- +title: Support negated filtering of issues by iids, label_name, milestone_title, assignee_usernames and assignee_id in GraphQL +merge_request: 58154 +author: +type: added diff --git a/config/feature_flags/development/pages_serve_from_migrated_zip.yml b/config/feature_flags/development/pages_serve_from_migrated_zip.yml deleted file mode 100644 index 2912beb39856545997e193569663f1c812b08fd4..0000000000000000000000000000000000000000 --- a/config/feature_flags/development/pages_serve_from_migrated_zip.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: pages_serve_from_migrated_zip -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52573 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300021 -milestone: '13.9' -type: development -group: group::release -default_enabled: true diff --git a/doc/administration/geo/replication/docker_registry.md b/doc/administration/geo/replication/docker_registry.md index 1669abbc52ae7a79cc775c810453692e85cbc84c..745ed28056f188af729047d1bf9fe18acdcd7c28 100644 --- a/doc/administration/geo/replication/docker_registry.md +++ b/doc/administration/geo/replication/docker_registry.md @@ -5,16 +5,16 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: howto --- -# Docker Registry for a secondary node **(PREMIUM SELF)** +# Docker Registry for a secondary site **(PREMIUM SELF)** You can set up a [Docker Registry](https://docs.docker.com/registry/) on your -**secondary** Geo node that mirrors the one on the **primary** Geo node. +**secondary** Geo site that mirrors the one on the **primary** Geo site. ## Storage support Docker Registry currently supports a few types of storage. If you choose a distributed storage (`azure`, `gcs`, `s3`, `swift`, or `oss`) for your Docker -Registry on the **primary** node, you can use the same storage for a **secondary** +Registry on the **primary** site, you can use the same storage for a **secondary** Docker Registry as well. For more information, read the [Load balancing considerations](https://docs.docker.com/registry/deploying/#load-balancing-considerations) when deploying the Registry, and how to set up the storage driver for the GitLab @@ -24,22 +24,22 @@ integrated [Container Registry](../../packages/container_registry.md#use-object- You can enable a storage-agnostic replication so it can be used for cloud or local storage. Whenever a new image is pushed to the -**primary** node, each **secondary** node will pull it to its own container +**primary** site, each **secondary** site will pull it to its own container repository. To configure Docker Registry replication: -1. Configure the [**primary** node](#configure-primary-node). -1. Configure the [**secondary** node](#configure-secondary-node). +1. Configure the [**primary** site](#configure-primary-site). +1. Configure the [**secondary** site](#configure-secondary-site). 1. Verify Docker Registry [replication](#verify-replication). -### Configure **primary** node +### Configure **primary** site Make sure that you have Container Registry set up and working on -the **primary** node before following the next steps. +the **primary** site before following the next steps. We need to make Docker Registry send notification events to the -**primary** node. +**primary** site. 1. SSH into your GitLab **primary** server and login as root: @@ -85,27 +85,29 @@ We need to make Docker Registry send notification events to the gitlab-ctl reconfigure ``` -### Configure **secondary** node +### Configure **secondary** site Make sure you have Container Registry set up and working on -the **secondary** node before following the next steps. +the **secondary** site before following the next steps. -The following steps should be done on each **secondary** node you're +The following steps should be done on each **secondary** site you're expecting to see the Docker images replicated. -Because we need to allow the **secondary** node to communicate securely with -the **primary** node Container Registry, we need to have a single key -pair for all the nodes. The **secondary** node will use this key to +Because we need to allow the **secondary** site to communicate securely with +the **primary** site Container Registry, we need to have a single key +pair for all the sites. The **secondary** site will use this key to generate a short-lived JWT that is pull-only-capable to access the -**primary** node Container Registry. +**primary** site Container Registry. -1. SSH into the **secondary** node and login as the `root` user: +For each application node on the **secondary** site: + +1. SSH into the node and login as the `root` user: ```shell sudo -i ``` -1. Copy `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` from the **primary** to the **secondary** node. +1. Copy `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` from the **primary** to the node. 1. Edit `/etc/gitlab/gitlab.rb`: @@ -114,7 +116,7 @@ generate a short-lived JWT that is pull-only-capable to access the gitlab_rails['geo_registry_replication_primary_api_url'] = 'https://primary.example.com:5050/' # Primary registry address, it will be used by the secondary node to directly communicate to primary registry ``` -1. Reconfigure the **secondary** node for the change to take effect: +1. Reconfigure the node for the change to take effect: ```shell gitlab-ctl reconfigure @@ -123,6 +125,6 @@ generate a short-lived JWT that is pull-only-capable to access the ### Verify replication To verify Container Registry replication is working, go to **Admin Area > Geo** -(`/admin/geo/nodes`) on the **secondary** node. +(`/admin/geo/nodes`) on the **secondary** site. The initial replication, or "backfill", will probably still be in progress. -You can monitor the synchronization process on each Geo node from the **primary** node's **Geo Nodes** dashboard in your browser. +You can monitor the synchronization process on each Geo site from the **primary** site's **Geo Nodes** dashboard in your browser. diff --git a/ee/app/finders/ee/issues_finder/params.rb b/ee/app/finders/ee/issues_finder/params.rb index bfc26ac2513e5fd91c322898a0c2758286a8d58f..cbd020a475f33db5b8858604b03f5ad50f0bfdcd 100644 --- a/ee/app/finders/ee/issues_finder/params.rb +++ b/ee/app/finders/ee/issues_finder/params.rb @@ -19,7 +19,7 @@ def filter_by_any_epic? end def weights? - params[:weight].present? && params[:weight] != ::Issue::WEIGHT_ALL + params[:weight].present? && params[:weight].to_s.casecmp(::Issue::WEIGHT_ALL) != 0 end def filter_by_no_weight? diff --git a/ee/app/graphql/ee/resolvers/issues_resolver.rb b/ee/app/graphql/ee/resolvers/issues_resolver.rb index 72e831716d2b46abaeb1a169e454414efea9181c..ed0ada5a4cc230e94375f87d5de4d01920feb9ea 100644 --- a/ee/app/graphql/ee/resolvers/issues_resolver.rb +++ b/ee/app/graphql/ee/resolvers/issues_resolver.rb @@ -7,13 +7,15 @@ module IssuesResolver extend ::Gitlab::Utils::Override prepended do - argument :iteration_id, ::GraphQL::ID_TYPE.to_list_type, + argument :iteration_id, [::GraphQL::ID_TYPE, null: true], required: false, description: 'Iterations applied to the issue.' - argument :epic_id, GraphQL::STRING_TYPE, required: false, description: 'ID of an epic associated with the issues, "none" and "any" values are supported.' + argument :weight, GraphQL::STRING_TYPE, + required: false, + description: 'Weight applied to the issue, "none" and "any" values are supported.' end private diff --git a/ee/app/graphql/ee/types/issues/negated_issue_filter_input_type.rb b/ee/app/graphql/ee/types/issues/negated_issue_filter_input_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..40dd8615e305c8c5d257e115b9f6a3cecf6fa8ad --- /dev/null +++ b/ee/app/graphql/ee/types/issues/negated_issue_filter_input_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module EE + module Types + module Issues + module NegatedIssueFilterInputType + extend ActiveSupport::Concern + + prepended do + argument :epic_id, GraphQL::STRING_TYPE, + required: false, + description: 'ID of an epic not associated with the issues.' + argument :weight, GraphQL::STRING_TYPE, + required: false, + description: 'Weight not applied to the issue.' + end + end + end + end +end diff --git a/ee/changelogs/unreleased/300115-support-negated-issue-filters-gql.yml b/ee/changelogs/unreleased/300115-support-negated-issue-filters-gql.yml new file mode 100644 index 0000000000000000000000000000000000000000..13f7ce96b7fdfe24b9a5d33481296a0ad0b1a5b4 --- /dev/null +++ b/ee/changelogs/unreleased/300115-support-negated-issue-filters-gql.yml @@ -0,0 +1,5 @@ +--- +title: Support negated filtering of issues by epic_id and weight in GraphQL +merge_request: 58154 +author: +type: added diff --git a/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb b/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb index 558aee95d3381891564e108d0ea99ce074de6166..3f31a37e7067df636f684d15de0fe7e75c79abc9 100644 --- a/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb +++ b/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb @@ -11,36 +11,39 @@ context "with a project" do describe '#resolve' do + let_it_be(:epic1) { create :epic, group: group } + let_it_be(:epic2) { create :epic, group: group } + + let_it_be(:iteration1) { create(:iteration, group: group, start_date: 2.weeks.ago, due_date: 1.week.ago) } + let_it_be(:current_iteration) { create(:iteration, :started, group: group, start_date: Date.today, due_date: 1.day.from_now) } + + let_it_be(:issue1) { create :issue, project: project, epic: epic1, iteration: iteration1 } + let_it_be(:issue2) { create :issue, project: project, epic: epic2, weight: 1 } + let_it_be(:issue3) { create :issue, project: project, weight: 3, iteration: current_iteration } + let_it_be(:issue4) { create :issue, :published, project: project } + before do project.add_developer(current_user) end describe 'sorting' do context 'when sorting by weight' do - let_it_be(:weight_issue1) { create(:issue, project: project, weight: 5) } - let_it_be(:weight_issue2) { create(:issue, project: project, weight: nil) } - let_it_be(:weight_issue3) { create(:issue, project: project, weight: 1) } - let_it_be(:weight_issue4) { create(:issue, project: project, weight: nil) } - it 'sorts issues ascending' do - expect(resolve_issues(sort: :weight_asc).to_a).to eq [weight_issue3, weight_issue1, weight_issue4, weight_issue2] + expect(resolve_issues(sort: :weight_asc).to_a).to eq [issue2, issue3, issue4, issue1] end it 'sorts issues descending' do - expect(resolve_issues(sort: :weight_desc).to_a).to eq [weight_issue1, weight_issue3, weight_issue4, weight_issue2] + expect(resolve_issues(sort: :weight_desc).to_a).to eq [issue3, issue2, issue4, issue1] end end context 'when sorting by published' do - let_it_be(:not_published) { create(:issue, project: project) } - let_it_be(:published) { create(:issue, :published, project: project) } - it 'sorts issues ascending' do - expect(resolve_issues(sort: :published_asc).to_a).to eq [not_published, published] + expect(resolve_issues(sort: :published_asc).to_a).to eq [issue3, issue2, issue1, issue4] end it 'sorts issues descending' do - expect(resolve_issues(sort: :published_desc).to_a).to eq [published, not_published] + expect(resolve_issues(sort: :published_desc).to_a).to eq [issue4, issue3, issue2, issue1] end end @@ -64,32 +67,56 @@ end describe 'filtering by iteration' do - let_it_be(:iteration1) { create(:iteration, group: group) } - let_it_be(:issue_with_iteration) { create(:issue, project: project, iteration: iteration1) } - let_it_be(:issue_without_iteration) { create(:issue, project: project) } - it 'returns issues with iteration' do - expect(resolve_issues(iteration_id: [iteration1.id])).to contain_exactly(issue_with_iteration) + expect(resolve_issues(iteration_id: [iteration1.id.to_s])).to contain_exactly(issue1) end end describe 'filter by epic' do - let_it_be(:epic) { create :epic, group: group } - let_it_be(:epic2) { create :epic, group: group } - let_it_be(:issue1) { create :issue, project: project, epic: epic } - let_it_be(:issue2) { create :issue, project: project, epic: epic2 } - let_it_be(:issue3) { create :issue, project: project } - it 'returns issues without epic when epic_id is "none"' do - expect(resolve_issues(epic_id: 'none')).to match_array([issue3]) + expect(resolve_issues(epic_id: 'none')).to contain_exactly(issue4, issue3) end it 'returns issues with any epic when epic_id is "any"' do - expect(resolve_issues(epic_id: 'any')).to match_array([issue1, issue2]) + expect(resolve_issues(epic_id: 'any')).to contain_exactly(issue1, issue2) end it 'returns issues with any epic when epic_id is specific' do - expect(resolve_issues(epic_id: epic.id)).to match_array([issue1]) + expect(resolve_issues(epic_id: epic1.id.to_s)).to contain_exactly(issue1) + end + end + + describe 'filter by weight' do + context 'when filtering by any weight' do + it 'only returns issues that have a weight assigned' do + expect(resolve_issues(weight: 'any')).to contain_exactly(issue2, issue3) + end + end + + context 'when filtering by no weight' do + it 'only returns issues that have no weight assigned' do + expect(resolve_issues(weight: 'none')).to contain_exactly(issue1, issue4) + end + end + + context 'when filtering by specific weight' do + it 'only returns issues that have the specified weight assigned' do + expect(resolve_issues(weight: '3')).to contain_exactly(issue3) + end + end + end + + describe 'filtering by negated params' do + describe 'filter by negated epic' do + it 'returns issues without the specified epic_id' do + expect(resolve_issues(not: { epic_id: epic2.id.to_s })).to contain_exactly(issue1, issue3, issue4) + end + end + + describe 'filtering by negated weight' do + it 'only returns issues that do not have the specified weight assigned' do + expect(resolve_issues(not: { weight: '3' })).to contain_exactly(issue1, issue2, issue4) + end end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index b794ab626bfb88060cd24be158542a9a9df8ed56..a2aac857bf5f0cf7b56b6a16b53eedce00ca05ba 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -49,6 +49,13 @@ let(:expected_issuables) { [issue3, issue4] } end + context 'when assignee_id does not exist' do + it_behaves_like 'assignee NOT ID filter' do + let(:params) { { not: { assignee_id: -100 } } } + let(:expected_issuables) { [issue1, issue2, issue3, issue4, issue5] } + end + end + context 'filter by username' do let_it_be(:user3) { create(:user) } @@ -71,6 +78,17 @@ let(:params) { { not: { assignee_username: [user.username, user2.username] } } } let(:expected_issuables) { [issue3, issue4] } end + + context 'when assignee_username does not exist' do + it_behaves_like 'assignee NOT username filter' do + before do + issue2.assignees = [user2] + end + + let(:params) { { not: { assignee_username: 'non_existent_username' } } } + let(:expected_issuables) { [issue1, issue2, issue3, issue4, issue5] } + end + end end it_behaves_like 'no assignee filter' do diff --git a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb index decc3569d6cdc2ff9100568a430e2fcf8f41c99a..3fbd9bd2368e96fbb24c2132baadde83efb10a07 100644 --- a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb +++ b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb @@ -69,6 +69,14 @@ expect(result.closed).to eq 1 end + context 'when both assignee_username and assignee_usernames are provided' do + it 'raises a mutually exclusive filter error' do + expect do + resolve_issue_status_counts(assignee_usernames: [current_user.username], assignee_username: current_user.username) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') + end + end + private def resolve_issue_status_counts(args = {}, context = { current_user: current_user }) diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 6e802bf7d255f0c527d7bdb6f056dd206f5c958b..7c2ceb5006645f2dc13ecf365c652e364fdccd4a 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -46,10 +46,6 @@ expect(resolve_issues(milestone_title: [milestone.title])).to contain_exactly(issue1) end - it 'filters by assignee_username' do - expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2) - end - it 'filters by two assignees' do assignee2 = create(:user) issue2.update!(assignees: [assignee, assignee2]) @@ -78,6 +74,24 @@ expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) end + describe 'filters by assignee_username' do + it 'filters by assignee_username' do + expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2) + end + + it 'filters by assignee_usernames' do + expect(resolve_issues(assignee_usernames: [assignee.username])).to contain_exactly(issue2) + end + + context 'when both assignee_username and assignee_usernames are provided' do + it 'raises a mutually exclusive filter error' do + expect do + resolve_issues(assignee_usernames: [assignee.username], assignee_username: assignee.username) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') + end + end + end + describe 'filters by created_at' do it 'filters by created_before' do expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1) @@ -144,6 +158,29 @@ end end + describe 'filters by negated params' do + it 'returns issues without the specified iids' do + expect(resolve_issues(not: { iids: [issue1.iid] })).to contain_exactly(issue2) + end + + it 'returns issues without the specified label names' do + expect(resolve_issues(not: { label_name: [label1.title] })).to be_empty + expect(resolve_issues(not: { label_name: [label2.title] })).to contain_exactly(issue1) + end + + it 'returns issues without the specified milestone' do + expect(resolve_issues(not: { milestone_title: [milestone.title] })).to contain_exactly(issue2) + end + + it 'returns issues without the specified assignee_usernames' do + expect(resolve_issues(not: { assignee_usernames: [assignee.username] })).to contain_exactly(issue1) + end + + it 'returns issues without the specified assignee_id' do + expect(resolve_issues(not: { assignee_id: [assignee.id] })).to contain_exactly(issue1) + end + end + describe 'sorting' do context 'when sorting by created' do it 'sorts issues ascending' do diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index 9e65635da91b296266c7d78be38d434eb3bd4966..17b1193b8adc30ccd2013f7f4fd88016acf67f3e 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -136,14 +136,6 @@ ) end end - - context 'when pages_serve_from_migrated_zip feature flag is disabled' do - before do - stub_feature_flags(pages_serve_from_migrated_zip: false) - end - - include_examples 'uses disk storage' - end end end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index f93822825e0853f3671d81e95196dd092ffa0670..dd9d44136e56b6f845ef0d5c19979da47037161d 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -12,6 +12,7 @@ let_it_be(:issues, reload: true) { [issue_a, issue_b] } let(:issues_data) { graphql_data['project']['issues']['edges'] } + let(:issue_filter_params) { {} } let(:fields) do <<~QUERY @@ -27,7 +28,7 @@ graphql_query_for( 'project', { 'fullPath' => project.full_path }, - query_graphql_field('issues', {}, fields) + query_graphql_field('issues', issue_filter_params, fields) ) end @@ -50,6 +51,16 @@ expect(issues_data[1]['node']['discussionLocked']).to eq(true) end + context 'when both assignee_username filters are provided' do + let(:issue_filter_params) { { assignee_username: current_user.username, assignee_usernames: [current_user.username] } } + + it 'returns a mutually exclusive param error' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_include('only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') + end + end + context 'when limiting the number of results' do let(:query) do <<~GQL