diff options
67 files changed, 670 insertions, 127 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index a8524470aa2..00f65ab7ca8 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -21,7 +21,7 @@ .minimal-rspec-tests: variables: - MINIMAL_RSPEC_ENABLED: "true" + RSPEC_TESTS_MAPPING_ENABLED: "true" .decomposed-database-rspec: variables: diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 47bdd3b538c..f2d5d872d64 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -82,6 +82,7 @@ detect-tests: - .detect-test-base - .rails:rules:detect-tests variables: + RSPEC_TESTS_MAPPING_ENABLED: "true" MATCHED_TESTS_FILE: tmp/matching_tests.txt detect-tests as-if-foss: diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 3219d74f85f..cdf25b2d428 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,6 +1,6 @@ import { sortBy, cloneDeep } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { ListType } from './constants'; +import { ListType, MilestoneIDs } from './constants'; export function getMilestone() { return null; @@ -108,7 +108,10 @@ export function formatIssueInput(issueInput, boardConfig) { return { ...issueInput, - milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null, + milestoneId: + milestoneId && milestoneId !== MilestoneIDs.ANY + ? fullMilestoneId(milestoneId) + : issueInput?.milestoneId, labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])], assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])], }; diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 16fb4596726..391e0d1fb0a 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -119,6 +119,11 @@ export const DraggableItemTypes = { list: 'list', }; +export const MilestoneIDs = { + NONE: 0, + ANY: -1, +}; + export default { BoardType, ListType, diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js index a2056165cb2..1ac6781a0e3 100644 --- a/app/assets/javascripts/deploy_freeze/store/actions.js +++ b/app/assets/javascripts/deploy_freeze/store/actions.js @@ -1,5 +1,6 @@ import Api from '~/api'; import createFlash from '~/flash'; +import { logError } from '~/lib/logger'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -63,8 +64,7 @@ export const deleteFreezePeriod = ({ state, commit }, { id }) => { }); commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id); - // eslint-disable-next-line no-console - console.error('[gitlab] Unable to delete deploy freeze:', e); + logError(`Unable to delete deploy freeze`, e); }); }; diff --git a/app/assets/javascripts/lib/logger/index.js b/app/assets/javascripts/lib/logger/index.js new file mode 100644 index 00000000000..0f5353fcbed --- /dev/null +++ b/app/assets/javascripts/lib/logger/index.js @@ -0,0 +1,6 @@ +/* eslint-disable no-console */ +export const LOG_PREFIX = '[gitlab]'; + +export const logError = (message = '', ...args) => { + console.error(LOG_PREFIX, `${message}\n`, ...args); +}; diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 8d77c0f3a8d..9de36b5b7d1 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -30,7 +30,7 @@ module IssueResolverArguments description: 'Usernames of users assigned to the issue.' argument :assignee_id, GraphQL::Types::String, required: false, - description: 'ID of a user assigned to the issues, "none" and "any" values are supported.' + description: 'ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported.' argument :created_before, Types::TimeType, required: false, description: 'Issues created before this date.' @@ -59,6 +59,9 @@ module IssueResolverArguments argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum, required: false, description: 'Filter issues by milestone ID wildcard.' + argument :my_reaction_emoji, GraphQL::Types::String, + required: false, + description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.' argument :not, Types::Issues::NegatedIssueFilterInputType, description: 'Negated arguments.', prepare: ->(negated_args, ctx) { negated_args.to_h }, 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 326f73846d0..81dd21aebec 100644 --- a/app/graphql/types/boards/board_issuable_input_base_type.rb +++ b/app/graphql/types/boards/board_issuable_input_base_type.rb @@ -14,7 +14,7 @@ module Types argument :my_reaction_emoji, GraphQL::Types::String, required: false, - description: 'Filter by reaction emoji applied by the current user.' + description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.' end end end diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb index 5c43c5c33b5..4f620a5b3d9 100644 --- a/app/graphql/types/issues/negated_issue_filter_input_type.rb +++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb @@ -26,6 +26,9 @@ module Types argument :milestone_wildcard_id, ::Types::NegatedMilestoneWildcardIdEnum, required: false, description: 'Filter by negated milestone wildcard values.' + argument :my_reaction_emoji, GraphQL::Types::String, + required: false, + description: 'Filter by reaction emoji applied by the current user.' end end end diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index a0e8886414b..3dca77af051 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -8,7 +8,7 @@ module Ci alias_attribute :secret_value, :value - validates :key, uniqueness: { scope: :pipeline_id } + validates :key, presence: true def hook_attrs { key: key, value: value } diff --git a/app/models/work_item/type.rb b/app/models/work_item/type.rb index 16cb7a8be45..03badf95908 100644 --- a/app/models/work_item/type.rb +++ b/app/models/work_item/type.rb @@ -9,14 +9,18 @@ class WorkItem::Type < ApplicationRecord include CacheMarkdownField + # Base types need to exist on the DB on app startup + # This constant is used by the DB seeder + BASE_TYPES = { + issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 }, + incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 }, + test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only + requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 } ## EE-only + }.freeze + cache_markdown_field :description, pipeline: :single_line - enum base_type: { - issue: 0, - incident: 1, - test_case: 2, ## EE-only - requirement: 3 ## EE-only - } + enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] } belongs_to :namespace, optional: true has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type diff --git a/config/feature_flags/development/keyset_pagination_for_groups_api.yml b/config/feature_flags/development/keyset_pagination_for_groups_api.yml new file mode 100644 index 00000000000..d4bd37565ee --- /dev/null +++ b/config/feature_flags/development/keyset_pagination_for_groups_api.yml @@ -0,0 +1,8 @@ +--- +name: keyset_pagination_for_groups_api +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68346 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339831 +milestone: '14.3' +type: development +group: group::access +default_enabled: false diff --git a/db/fixtures/development/001_create_base_work_item_types.rb b/db/fixtures/development/001_create_base_work_item_types.rb new file mode 100644 index 00000000000..7a541ca20b0 --- /dev/null +++ b/db/fixtures/development/001_create_base_work_item_types.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Gitlab::Seeder.quiet do + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import +end diff --git a/db/fixtures/production/003_create_base_work_item_types.rb b/db/fixtures/production/003_create_base_work_item_types.rb new file mode 100644 index 00000000000..7a541ca20b0 --- /dev/null +++ b/db/fixtures/production/003_create_base_work_item_types.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Gitlab::Seeder.quiet do + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import +end diff --git a/db/migrate/20210831203408_upsert_base_work_item_types.rb b/db/migrate/20210831203408_upsert_base_work_item_types.rb new file mode 100644 index 00000000000..314412d8d3d --- /dev/null +++ b/db/migrate/20210831203408_upsert_base_work_item_types.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class UpsertBaseWorkItemTypes < ActiveRecord::Migration[6.1] + module WorkItem + class Type < ActiveRecord::Base + self.table_name = 'work_item_types' + + enum base_type: { + issue: 0, + incident: 1, + test_case: 2, + requirement: 3 + } + end + end + + def up + # upsert default types + WorkItem::Type.find_or_create_by(name: 'Issue', namespace_id: nil, base_type: :issue, icon_name: 'issue-type-issue') + WorkItem::Type.find_or_create_by(name: 'Incident', namespace_id: nil, base_type: :incident, icon_name: 'issue-type-incident') + WorkItem::Type.find_or_create_by(name: 'Test Case', namespace_id: nil, base_type: :test_case, icon_name: 'issue-type-test-case') + WorkItem::Type.find_or_create_by(name: 'Requirement', namespace_id: nil, base_type: :requirement, icon_name: 'issue-type-requirements') + end + + def down + # We expect this table to be empty at the point of the up migration, + # however there is a remote possibility that issues could already be + # using one of these types, with a tight foreign constraint. + # Therefore we will not attempt to remove any data. + end +end diff --git a/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb b/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb new file mode 100644 index 00000000000..77b9e5297a7 --- /dev/null +++ b/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOnNameAndIdToPublicGroups < Gitlab::Database::Migration[1.0] + INDEX_NAME = 'index_namespaces_public_groups_name_id' + PUBLIC_VISIBILITY_LEVEL = 20 + + disable_ddl_transaction! + + def up + add_concurrent_index :namespaces, [:name, :id], name: INDEX_NAME, + where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}" + end + + def down + remove_concurrent_index_by_name :namespaces, INDEX_NAME + end +end diff --git a/db/schema_migrations/20210831203408 b/db/schema_migrations/20210831203408 new file mode 100644 index 00000000000..6ab3f810e57 --- /dev/null +++ b/db/schema_migrations/20210831203408 @@ -0,0 +1 @@ +50a06a2a57ed26c25af53d3d7f6f5ef73efde8a23a36c5f825af56b4d0c4c3f6
\ No newline at end of file diff --git a/db/schema_migrations/20210901065504 b/db/schema_migrations/20210901065504 new file mode 100644 index 00000000000..f62e6ce1272 --- /dev/null +++ b/db/schema_migrations/20210901065504 @@ -0,0 +1 @@ +9724a5fc1703418f9b1ea1d5375fc3b01834b30e5ff16c60537db5cb00bc210a
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f149a299c45..d6b561419ed 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25691,6 +25691,8 @@ CREATE INDEX index_namespaces_on_traversal_ids ON namespaces USING gin (traversa CREATE INDEX index_namespaces_on_type_and_id_partial ON namespaces USING btree (type, id) WHERE (type IS NOT NULL); +CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20)); + CREATE INDEX index_non_requested_project_members_on_source_id_and_type ON members USING btree (source_id, source_type) WHERE ((requested_at IS NULL) AND ((type)::text = 'ProjectMember'::text)); CREATE UNIQUE INDEX index_note_diff_files_on_diff_note_id ON note_diff_files USING btree (diff_note_id); diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md index 78421e2d567..cb002ef5643 100644 --- a/doc/administration/auth/ldap/ldap-troubleshooting.md +++ b/doc/administration/auth/ldap/ldap-troubleshooting.md @@ -336,7 +336,7 @@ Gitlab::Auth::Ldap::Person.find_by_uid('<uid>', adapter) ### Group memberships **(PREMIUM SELF)** -#### Membership(s) not granted **(PREMIUM SELF)** +#### Membership(s) not granted Sometimes you may think a particular user should be added to a GitLab group via LDAP group sync, but for some reason it's not happening. There are several @@ -395,7 +395,7 @@ group sync](#sync-all-groups) in the rails console and [look through the output](#example-console-output-after-a-group-sync) to see what happens when GitLab syncs the `admin_group`. -#### Sync all groups **(PREMIUM SELF)** +#### Sync all groups NOTE: To sync all groups manually when debugging is unnecessary, [use the Rake @@ -413,7 +413,7 @@ LdapAllGroupsSyncWorker.new.perform Next, [learn how to read the output](#example-console-output-after-a-group-sync). -##### Example console output after a group sync **(PREMIUM SELF)** +##### Example console output after a group sync Like the output from the user sync, the output from the [manual group sync](#sync-all-groups) is also very verbose. However, it contains lots @@ -503,7 +503,7 @@ stating as such: No `admin_group` configured for 'ldapmain' provider. Skipping ``` -#### Sync one group **(PREMIUM SELF)** +#### Sync one group [Syncing all groups](#sync-all-groups) can produce a lot of noise in the output, which can be distracting when you're only interested in troubleshooting the memberships of @@ -525,7 +525,7 @@ EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group) The output is similar to [that you get from syncing all groups](#example-console-output-after-a-group-sync). -#### Query a group in LDAP **(PREMIUM SELF)** +#### Query a group in LDAP When you'd like to confirm that GitLab can read a LDAP group and see all its members, you can run the following: diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md index 07c29984552..7e2699d5eb3 100644 --- a/doc/administration/auth/smartcard.md +++ b/doc/administration/auth/smartcard.md @@ -28,7 +28,7 @@ GitLab supports two authentication methods: ### Authentication against a local database with X.509 certificates -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/726) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.6 as an experimental feature. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/726) in GitLab 11.6 as an experimental feature. WARNING: Smartcard authentication against local databases may change or be removed completely in future @@ -55,7 +55,7 @@ Certificate: ### Authentication against a local database with X.509 certificates and SAN extension -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8605) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8605) in GitLab 12.3. Smartcards with X.509 certificates using SAN extensions can be used to authenticate with GitLab. @@ -98,7 +98,7 @@ Certificate: ### Authentication against an LDAP server -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7693) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8 as an experimental feature. Smartcard authentication against an LDAP server may change or be removed completely in future releases. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7693) in GitLab 11.8 as an experimental feature. Smartcard authentication against an LDAP server may change or be removed completely in the future. GitLab implements a standard way of certificate matching following [RFC4523](https://tools.ietf.org/html/rfc4523). It uses the diff --git a/doc/administration/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md index 374eebeb773..814e742b026 100644 --- a/doc/administration/operations/ssh_certificates.md +++ b/doc/administration/operations/ssh_certificates.md @@ -6,8 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # User lookup via OpenSSH's AuthorizedPrincipalsCommand **(FREE SELF)** -> [Available in](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19911) GitLab -> Community Edition 11.2. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19911) in GitLab 11.2. The default SSH authentication for GitLab requires users to upload their SSH public keys before they can use the SSH transport. diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md index 0338732e886..f29e2a6c7f6 100644 --- a/doc/administration/raketasks/github_import.md +++ b/doc/administration/raketasks/github_import.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # GitHub import **(FREE SELF)** -> [Introduced]( https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10308) in GitLab 9.1. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10308) in GitLab 9.1. To retrieve and import GitHub repositories, you need a [GitHub personal access token](https://github.com/settings/tokens). A username should be passed as the second argument to the Rake task, diff --git a/doc/administration/raketasks/praefect.md b/doc/administration/raketasks/praefect.md index 5fe0546999b..d2fd4943c68 100644 --- a/doc/administration/raketasks/praefect.md +++ b/doc/administration/raketasks/praefect.md @@ -7,7 +7,7 @@ type: reference # Praefect Rake tasks **(FREE SELF)** -> [Introduced]( https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28369) in GitLab 12.10. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28369) in GitLab 12.10. Rake tasks are available for projects that have been created on Praefect storage. See the [Praefect documentation](../gitaly/praefect.md) for information on configuring Praefect. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 880788b361b..4622618db42 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9999,7 +9999,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="groupissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. | +| <a id="groupissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. | | <a id="groupissuesassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. | | <a id="groupissuesassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | | <a id="groupissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | @@ -10016,6 +10016,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="groupissueslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. | | <a id="groupissuesmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. | | <a id="groupissuesmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | +| <a id="groupissuesmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="groupissuesnot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | | <a id="groupissuessearch"></a>`search` | [`String`](#string) | Search query for issue title or description. | | <a id="groupissuessort"></a>`sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. | @@ -12428,7 +12429,7 @@ Returns [`Issue`](#issue). | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="projectissueassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. | +| <a id="projectissueassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. | | <a id="projectissueassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. | | <a id="projectissueassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | | <a id="projectissueauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | @@ -12444,6 +12445,7 @@ Returns [`Issue`](#issue). | <a id="projectissuelabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. | | <a id="projectissuemilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. | | <a id="projectissuemilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | +| <a id="projectissuemyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="projectissuenot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | | <a id="projectissuesearch"></a>`search` | [`String`](#string) | Search query for issue title or description. | | <a id="projectissuesort"></a>`sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. | @@ -12463,7 +12465,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="projectissuestatuscountsassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. | +| <a id="projectissuestatuscountsassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. | | <a id="projectissuestatuscountsassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. | | <a id="projectissuestatuscountsassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | | <a id="projectissuestatuscountsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | @@ -12476,6 +12478,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | <a id="projectissuestatuscountslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. | | <a id="projectissuestatuscountsmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. | | <a id="projectissuestatuscountsmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | +| <a id="projectissuestatuscountsmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="projectissuestatuscountsnot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | | <a id="projectissuestatuscountssearch"></a>`search` | [`String`](#string) | Search query for issue title or description. | | <a id="projectissuestatuscountstypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter issues by the given issue types. | @@ -12496,7 +12499,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="projectissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. | +| <a id="projectissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. | | <a id="projectissuesassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. | | <a id="projectissuesassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | | <a id="projectissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | @@ -12512,6 +12515,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectissueslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. | | <a id="projectissuesmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. | | <a id="projectissuesmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | +| <a id="projectissuesmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="projectissuesnot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | | <a id="projectissuessearch"></a>`search` | [`String`](#string) | Search query for issue title or description. | | <a id="projectissuessort"></a>`sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. | @@ -17359,7 +17363,7 @@ Field that are available while modifying the custom mapping attributes for an HT | <a id="boardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. | | <a id="boardissueinputlabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. | | <a id="boardissueinputmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Filter by milestone title. | -| <a id="boardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. | +| <a id="boardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="boardissueinputnot"></a>`not` | [`NegatedBoardIssueInput`](#negatedboardissueinput) | List of negated arguments. | | <a id="boardissueinputreleasetag"></a>`releaseTag` | [`String`](#string) | Filter by release tag. | | <a id="boardissueinputsearch"></a>`search` | [`String`](#string) | Search query for issue title or description. | @@ -17452,7 +17456,7 @@ Input type for DastSiteProfile authentication. | ---- | ---- | ----------- | | <a id="epicfiltersauthorusername"></a>`authorUsername` | [`String`](#string) | Filter by author username. | | <a id="epicfilterslabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. | -| <a id="epicfiltersmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. | +| <a id="epicfiltersmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="epicfiltersnot"></a>`not` | [`NegatedEpicBoardIssueInput`](#negatedepicboardissueinput) | Negated epic arguments. | | <a id="epicfilterssearch"></a>`search` | [`String`](#string) | Search query for epic title or description. | @@ -17515,7 +17519,7 @@ Represents an escalation rule. | <a id="negatedboardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`NegatedIterationWildcardId`](#negatediterationwildcardid) | Filter by iteration ID wildcard. | | <a id="negatedboardissueinputlabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. | | <a id="negatedboardissueinputmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Filter by milestone title. | -| <a id="negatedboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. | +| <a id="negatedboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | <a id="negatedboardissueinputreleasetag"></a>`releaseTag` | [`String`](#string) | Filter by release tag. | | <a id="negatedboardissueinputtypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter by the given issue types. | | <a id="negatedboardissueinputweight"></a>`weight` | [`String`](#string) | Filter by weight. | @@ -17528,7 +17532,7 @@ Represents an escalation rule. | ---- | ---- | ----------- | | <a id="negatedepicboardissueinputauthorusername"></a>`authorUsername` | [`String`](#string) | Filter by author username. | | <a id="negatedepicboardissueinputlabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. | -| <a id="negatedepicboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. | +| <a id="negatedepicboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | ### `NegatedEpicFilterInput` @@ -17556,6 +17560,7 @@ Represents an escalation rule. | <a id="negatedissuefilterinputlabelname"></a>`labelName` | [`[String!]`](#string) | Labels not applied to this issue. | | <a id="negatedissuefilterinputmilestonetitle"></a>`milestoneTitle` | [`[String!]`](#string) | Milestone not applied to this issue. | | <a id="negatedissuefilterinputmilestonewildcardid"></a>`milestoneWildcardId` | [`NegatedMilestoneWildcardId`](#negatedmilestonewildcardid) | Filter by negated milestone wildcard values. | +| <a id="negatedissuefilterinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. | | <a id="negatedissuefilterinputweight"></a>`weight` | [`String`](#string) | Weight not applied to the issue. | ### `OncallRotationActivePeriodInputType` diff --git a/doc/api/groups.md b/doc/api/groups.md index 3831aef10c9..b1119c9d5a4 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -13,6 +13,11 @@ authentication, only public groups are returned. By default, this request returns 20 results at a time because the API results [are paginated](index.md#pagination). +When accessed without authentication, this endpoint also supports [keyset pagination](index.md#keyset-based-pagination): + +- When requesting consecutive pages of results, we recommend you use keyset pagination. +- Beyond a specific offset limit (specified by [max offset allowed by the REST API for offset-based pagination](../administration/instance_limits.md#max-offset-allowed-by-the-rest-api-for-offset-based-pagination)), offset pagination is unavailable. + Parameters: | Attribute | Type | Required | Description | diff --git a/doc/api/index.md b/doc/api/index.md index f7148dcf472..ca8b7392e32 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -462,22 +462,43 @@ The response header includes a link to the next page. For example: ```http HTTP/1.1 200 OK ... -Links: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next" Link: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next" Status: 200 OK ... ``` +The link to the next page contains an additional filter `id_after=42` that +excludes already-retrieved records. + +As another example, the following request lists 50 [groups](groups.md) per page ordered +by `name` ascending using keyset pagination: + +```shell +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups?pagination=keyset&per_page=50&order_by=name&sort=asc" +``` + +The response header includes a link to the next page: + +```http +HTTP/1.1 200 OK +... +Link: <https://gitlab.example.com/api/v4/groups?pagination=keyset&per_page=50&order_by=name&sort=asc&cursor=eyJuYW1lIjoiRmxpZ2h0anMiLCJpZCI6IjI2IiwiX2tkIjoibiJ9>; rel="next" +Status: 200 OK +... +``` + +The link to the next page contains an additional filter `cursor=eyJuYW1lIjoiRmxpZ2h0anMiLCJpZCI6IjI2IiwiX2tkIjoibiJ9` that +excludes already-retrieved records. + +The type of filter depends on the +`order_by` option used, and we can have more than one additional filter. + WARNING: -The `Links` header is scheduled to be removed in GitLab 14.0 to be aligned with the +The `Links` header was removed in GitLab 14.0 to be aligned with the [W3C `Link` specification](https://www.w3.org/wiki/LinkHeader). The `Link` header was [added in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33714) and should be used instead. -The link to the next page contains an additional filter `id_after=42` that -excludes already-retrieved records. The type of filter depends on the -`order_by` option used, and we may have more than one additional filter. - When the end of the collection is reached and there are no additional records to retrieve, the `Link` header is absent and the resulting array is empty. @@ -489,9 +510,10 @@ pagination headers. Keyset-based pagination is supported only for selected resources and ordering options: -| Resource | Order | -|-------------------------|-------| -| [Projects](projects.md) | `order_by=id` only. | +| Resource | Options | Availability | +|:-------------------------|:---------------------------------|:----------------------------------------| +| [Projects](projects.md) | `order_by=id` only | Authenticated and unauthenticated users | +| [Groups](groups.md) | `order_by=name`, `sort=asc` only | Unauthenticated users only | ## Path parameters diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md index 0a39400dfd4..ae3b58d0764 100644 --- a/doc/api/job_artifacts.md +++ b/doc/api/job_artifacts.md @@ -4,7 +4,7 @@ group: Testing info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Job Artifacts API +# Job Artifacts API **(FREE)** ## Get job artifacts diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 42774b80b27..ac8b756beac 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -466,7 +466,7 @@ Example of response ## Get Kubernetes Agents by `CI_JOB_TOKEN` **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324269) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.11. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324269) in GitLab 13.11. Retrieve the job that generated the `CI_JOB_TOKEN`, along with a list of allowed GitLab Kubernetes Agents. diff --git a/doc/api/packages/composer.md b/doc/api/packages/composer.md index 0e66654b494..b3a27519729 100644 --- a/doc/api/packages/composer.md +++ b/doc/api/packages/composer.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Composer API +# Composer API **(FREE)** This is the API documentation for [Composer Packages](../../user/packages/composer_repository/index.md). diff --git a/doc/api/packages/conan.md b/doc/api/packages/conan.md index 88ed2524173..f5d08ed7ef8 100644 --- a/doc/api/packages/conan.md +++ b/doc/api/packages/conan.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Conan API +# Conan API **(FREE)** This is the API documentation for [Conan Packages](../../user/packages/conan_repository/index.md). diff --git a/doc/api/packages/go_proxy.md b/doc/api/packages/go_proxy.md index 2f81435db42..ffafc387951 100644 --- a/doc/api/packages/go_proxy.md +++ b/doc/api/packages/go_proxy.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Go Proxy API +# Go Proxy API **(FREE SELF)** This is the API documentation for [Go Packages](../../user/packages/go_proxy/index.md). This API is behind a feature flag that is disabled by default. GitLab administrators with access to diff --git a/doc/api/packages/helm.md b/doc/api/packages/helm.md index f1d5f24cd99..fba3898a247 100644 --- a/doc/api/packages/helm.md +++ b/doc/api/packages/helm.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Helm API +# Helm API **(FREE)** This is the API documentation for [Helm](../../user/packages/helm_repository/index.md). diff --git a/doc/api/packages/maven.md b/doc/api/packages/maven.md index d03c9be3060..b4b3d579ffb 100644 --- a/doc/api/packages/maven.md +++ b/doc/api/packages/maven.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Maven API +# Maven API **(FREE)** This is the API documentation for [Maven Packages](../../user/packages/maven_repository/index.md). diff --git a/doc/api/packages/npm.md b/doc/api/packages/npm.md index a1d29e9691c..24ac1a640c9 100644 --- a/doc/api/packages/npm.md +++ b/doc/api/packages/npm.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.example/handbook/engineering/ux/technical-writing/#assignments --- -# npm API +# npm API **(FREE)** This is the API documentation for [npm Packages](../../user/packages/npm_registry/index.md). diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md index d19e2dfa65b..9d73beac452 100644 --- a/doc/api/packages/nuget.md +++ b/doc/api/packages/nuget.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about..example/handbook/engineering/ux/technical-writing/#assignments --- -# NuGet API +# NuGet API **(FREE)** This is the API documentation for [NuGet Packages](../../user/packages/nuget_repository/index.md). diff --git a/doc/api/packages/pypi.md b/doc/api/packages/pypi.md index 8a71bf4588a..a1c96d03297 100644 --- a/doc/api/packages/pypi.md +++ b/doc/api/packages/pypi.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# PyPI API +# PyPI API **(FREE)** This is the API documentation for [PyPI Packages](../../user/packages/pypi_repository/index.md). diff --git a/doc/api/packages/rubygems.md b/doc/api/packages/rubygems.md index 426548d5ed2..10dcaafda42 100644 --- a/doc/api/packages/rubygems.md +++ b/doc/api/packages/rubygems.md @@ -4,7 +4,7 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Ruby gems API +# Ruby gems API **(FREE SELF)** This is the API documentation for [Ruby gems](../../user/packages/rubygems_registry/index.md). diff --git a/doc/api/templates/dockerfiles.md b/doc/api/templates/dockerfiles.md index 2d7e926561f..5f862201571 100644 --- a/doc/api/templates/dockerfiles.md +++ b/doc/api/templates/dockerfiles.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# Dockerfiles API +# Dockerfiles API **(FREE)** GitLab provides an API endpoint for instance-level Dockerfile templates. Default templates are defined at diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md index f1bf8120574..71b791de16a 100644 --- a/doc/api/templates/gitignores.md +++ b/doc/api/templates/gitignores.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# .gitignore API +# .gitignore API **(FREE)** In GitLab, the `/gitignores` endpoint returns a list of Git `.gitignore` templates. For more information, see the [Git documentation for `.gitignore`](https://git-scm.com/docs/gitignore). diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md index 82abe598cf6..abed008218c 100644 --- a/doc/api/templates/gitlab_ci_ymls.md +++ b/doc/api/templates/gitlab_ci_ymls.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# GitLab CI YMLs API +# GitLab CI YMLs API **(FREE)** In GitLab, there is an API endpoint available to work with GitLab CI/CD YMLs. For more information on CI/CD pipeline configuration in GitLab, see the diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md index e7eaa6eb3e0..adba4f75255 100644 --- a/doc/api/templates/licenses.md +++ b/doc/api/templates/licenses.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# Licenses API +# Licenses API **(FREE)** In GitLab, there is an API endpoint available for working with various open source license templates. For more information on the terms of various diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 0896357cc73..a1123b6291b 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -108,6 +108,20 @@ module API present paginate(groups), options end + def present_groups_with_pagination_strategies(params, groups) + return present_groups(params, groups) if current_user.present? || Feature.disabled?(:keyset_pagination_for_groups_api) + + options = { + with: Entities::Group, + current_user: nil, + statistics: false + } + + groups, options = with_custom_attributes(groups, options) + + present paginate_with_strategies(groups), options + end + def delete_group(group) destroy_conditionally!(group) do |group| ::Groups::DestroyService.new(group, current_user).async_execute @@ -168,7 +182,7 @@ module API end get do groups = find_groups(declared_params(include_missing: false), params[:id]) - present_groups params, groups + present_groups_with_pagination_strategies params, groups end desc 'Create a group. Available only for users who can create groups.' do diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb index 61cff37e4ab..8c2186768ea 100644 --- a/lib/api/helpers/pagination_strategies.rb +++ b/lib/api/helpers/pagination_strategies.rb @@ -3,10 +3,16 @@ module API module Helpers module PaginationStrategies - def paginate_with_strategies(relation, request_scope) + def paginate_with_strategies(relation, request_scope = nil) paginator = paginator(relation, request_scope) - yield(paginator.paginate(relation)).tap do |records, _| + result = if block_given? + yield(paginator.paginate(relation)) + else + paginator.paginate(relation) + end + + result.tap do |records, _| paginator.finalize(records) end end @@ -20,17 +26,31 @@ module API private def keyset_paginator(relation) - request_context = Gitlab::Pagination::Keyset::RequestContext.new(self) - unless Gitlab::Pagination::Keyset.available?(request_context, relation) + if cursor_based_keyset_pagination_supported?(relation) + request_context_class = Gitlab::Pagination::Keyset::CursorBasedRequestContext + paginator_class = Gitlab::Pagination::Keyset::CursorPager + availability_checker = Gitlab::Pagination::CursorBasedKeyset + else + request_context_class = Gitlab::Pagination::Keyset::RequestContext + paginator_class = Gitlab::Pagination::Keyset::Pager + availability_checker = Gitlab::Pagination::Keyset + end + + request_context = request_context_class.new(self) + + unless availability_checker.available?(request_context, relation) return error!('Keyset pagination is not yet available for this type of request', 405) end - Gitlab::Pagination::Keyset::Pager.new(request_context) + paginator_class.new(request_context) end def offset_paginator(relation, request_scope) offset_limit = limit_for_scope(request_scope) - if Gitlab::Pagination::Keyset.available_for_type?(relation) && offset_limit_exceeded?(offset_limit) + if (Gitlab::Pagination::Keyset.available_for_type?(relation) || + cursor_based_keyset_pagination_supported?(relation)) && + offset_limit_exceeded?(offset_limit) + return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \ "for requests that return objects of type #{relation.klass}. " \ "Remaining records can be retrieved using keyset pagination.", 405) @@ -39,6 +59,10 @@ module API Gitlab::Pagination::OffsetPagination.new(self) end + def cursor_based_keyset_pagination_supported?(relation) + Gitlab::Pagination::CursorBasedKeyset.available_for_type?(relation) + end + def keyset_pagination_enabled? params[:pagination] == 'keyset' end diff --git a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb index acb4842db31..518a812b406 100644 --- a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb +++ b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb @@ -32,12 +32,10 @@ module Gitlab def set_data_consistency_locations!(job) # Once we add support for multiple databases to our load balancer, we would use something like this: # job['wal_locations'] = Gitlab::Database::DATABASES.transform_values do |connection| - # connection.load_balancer.primary_write_location. + # connection.load_balancer.primary_write_location # end # - # TODO: Replace hardcoded database config name :main when we merge unification strategy - # https://gitlab.com/gitlab-org/gitlab/-/issues/336566 - job['wal_locations'] = { main: wal_location } if wal_location + job['wal_locations'] = { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => wal_location } if wal_location end def wal_location diff --git a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb index 2555f693a5d..f7fda14b215 100644 --- a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb +++ b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb @@ -66,7 +66,7 @@ module Gitlab def legacy_wal_location(job) wal_location = job['database_write_location'] || job['database_replica_location'] - { main: wal_location } if wal_location + { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => wal_location } if wal_location end def load_balancing_available?(worker_class) diff --git a/lib/gitlab/database_importers/work_items/base_type_importer.rb b/lib/gitlab/database_importers/work_items/base_type_importer.rb new file mode 100644 index 00000000000..c5acdb41de5 --- /dev/null +++ b/lib/gitlab/database_importers/work_items/base_type_importer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module DatabaseImporters + module WorkItems + module BaseTypeImporter + def self.import + WorkItem::Type::BASE_TYPES.each do |type, attributes| + WorkItem::Type.create!(base_type: type, **attributes.slice(:name, :icon_name)) + end + end + end + end + end +end diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb new file mode 100644 index 00000000000..f19cdf06d9a --- /dev/null +++ b/lib/gitlab/pagination/cursor_based_keyset.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Pagination + module CursorBasedKeyset + SUPPORTED_ORDERING = { + Group => { name: :asc } + }.freeze + + def self.available_for_type?(relation) + SUPPORTED_ORDERING.key?(relation.klass) + end + + def self.available?(cursor_based_request_context, relation) + available_for_type?(relation) && + order_satisfied?(relation, cursor_based_request_context) + end + + def self.order_satisfied?(relation, cursor_based_request_context) + order_by_from_request = cursor_based_request_context.order_by + + SUPPORTED_ORDERING[relation.klass] == order_by_from_request + end + private_class_method :order_satisfied? + end + end +end diff --git a/lib/gitlab/pagination/keyset/cursor_based_request_context.rb b/lib/gitlab/pagination/keyset/cursor_based_request_context.rb index d9c118ceef5..18390f5b59d 100644 --- a/lib/gitlab/pagination/keyset/cursor_based_request_context.rb +++ b/lib/gitlab/pagination/keyset/cursor_based_request_context.rb @@ -4,11 +4,12 @@ module Gitlab module Pagination module Keyset class CursorBasedRequestContext - attr_reader :request - delegate :params, :header, to: :request + DEFAULT_SORT_DIRECTION = :desc + attr_reader :request_context + delegate :params, to: :request_context - def initialize(request) - @request = request + def initialize(request_context) + @request_context = request_context end def per_page @@ -21,9 +22,13 @@ module Gitlab def apply_headers(cursor_for_next_page) Gitlab::Pagination::Keyset::HeaderBuilder - .new(self) + .new(request_context) .add_next_page_header({ cursor: cursor_for_next_page }) end + + def order_by + { params[:order_by].to_sym => params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION } + end end end end diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index 4fc62038d4f..797d9188f81 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -159,7 +159,7 @@ function rspec_paralellized_job() { local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}" - if [[ -n $MINIMAL_RSPEC_ENABLED ]]; then + if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter "tmp/matching_tests.txt" else tooling/bin/parallel_rspec --rspec_args "${rspec_args}" diff --git a/spec/db/development/create_base_work_item_types_spec.rb b/spec/db/development/create_base_work_item_types_spec.rb new file mode 100644 index 00000000000..914b84d8668 --- /dev/null +++ b/spec/db/development/create_base_work_item_types_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create base work item types in development' do + subject { load Rails.root.join('db', 'fixtures', 'development', '001_create_base_work_item_types.rb') } + + it_behaves_like 'work item base types importer' +end diff --git a/spec/db/production/create_base_work_item_types_spec.rb b/spec/db/production/create_base_work_item_types_spec.rb new file mode 100644 index 00000000000..81d80104bb4 --- /dev/null +++ b/spec/db/production/create_base_work_item_types_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create base work item types in production' do + subject { load Rails.root.join('db', 'fixtures', 'production', '003_create_base_work_item_types.rb') } + + it_behaves_like 'work item base types importer' +end diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js index ac606f1b182..ad67afdce75 100644 --- a/spec/frontend/deploy_freeze/store/actions_spec.js +++ b/spec/frontend/deploy_freeze/store/actions_spec.js @@ -5,6 +5,7 @@ import * as actions from '~/deploy_freeze/store/actions'; import * as types from '~/deploy_freeze/store/mutation_types'; import getInitialState from '~/deploy_freeze/store/state'; import createFlash from '~/flash'; +import * as logger from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; import { freezePeriodsFixture, timezoneDataFixture } from '../helpers'; @@ -218,7 +219,7 @@ describe('deploy freeze store actions', () => { }); it('should show flash error and set error in state on delete failure', () => { - const errorSpy = jest.spyOn(console, 'error').mockImplementation(); + jest.spyOn(logger, 'logError').mockImplementation(); const error = new Error(); Api.deleteFreezePeriod.mockRejectedValue(error); @@ -234,7 +235,7 @@ describe('deploy freeze store actions', () => { () => { expect(createFlash).toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalledWith('[gitlab] Unable to delete deploy freeze:', error); + expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error); }, ); }); diff --git a/spec/frontend/lib/logger/index_spec.js b/spec/frontend/lib/logger/index_spec.js new file mode 100644 index 00000000000..9382fafe4de --- /dev/null +++ b/spec/frontend/lib/logger/index_spec.js @@ -0,0 +1,23 @@ +import { logError, LOG_PREFIX } from '~/lib/logger'; + +describe('~/lib/logger', () => { + let consoleErrorSpy; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error'); + consoleErrorSpy.mockImplementation(); + }); + + describe('logError', () => { + it('sends given message to console.error', () => { + const message = 'Lorem ipsum dolar sit amit'; + const error = new Error('lorem ipsum'); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + logError(message, error); + + expect(consoleErrorSpy).toHaveBeenCalledWith(LOG_PREFIX, `${message}\n`, error); + }); + }); +}); diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index d752496d809..719c14ee188 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -20,6 +20,7 @@ RSpec.describe Resolvers::IssuesResolver do let_it_be(:issue4) { create(:issue) } let_it_be(:label1) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) } + let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue1) } specify do expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type) @@ -200,6 +201,27 @@ RSpec.describe Resolvers::IssuesResolver do end end + context 'filtering by reaction emoji' do + let_it_be(:downvoted_issue) { create(:issue, project: project) } + let_it_be(:downvote_award) { create(:award_emoji, :downvote, user: current_user, awardable: downvoted_issue) } + + it 'filters by reaction emoji' do + expect(resolve_issues(my_reaction_emoji: upvote_award.name)).to contain_exactly(issue1) + end + + it 'filters by reaction emoji wildcard "none"' do + expect(resolve_issues(my_reaction_emoji: 'none')).to contain_exactly(issue2) + end + + it 'filters by reaction emoji wildcard "any"' do + expect(resolve_issues(my_reaction_emoji: 'any')).to contain_exactly(issue1, downvoted_issue) + end + + it 'filters by negated reaction emoji' do + expect(resolve_issues(not: { my_reaction_emoji: downvote_award.name })).to contain_exactly(issue1, issue2) + end + end + context 'when searching issues' do it 'returns correct issues' do expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb index 23a496d85f8..f683ade978a 100644 --- a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb @@ -85,7 +85,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do end it 'passes database_replica_location' do - expected_location = { main: location } + expected_location = { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } expect(load_balancer).to receive_message_chain(:host, "database_replica_location").and_return(location) @@ -103,7 +103,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do end it 'passes primary write location', :aggregate_failures do - expected_location = { main: location } + expected_location = { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } expect(load_balancer).to receive(:primary_write_location).and_return(location) @@ -116,28 +116,43 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do end end - shared_examples_for 'database location was already provided' do - shared_examples_for 'does not set database location again' do |use_primary| - before do - allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(use_primary) - end + context 'when worker cannot be constantized' do + let(:worker_class) { 'ActionMailer::MailDeliveryJob' } + let(:expected_consistency) { :always } - it 'does not set database locations again' do - run_middleware + include_examples 'does not pass database locations' + end - expect(job['wal_locations']).to eq({ main: old_location }) - end - end + context 'when worker class does not include ApplicationWorker' do + let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper } + let(:expected_consistency) { :always } + + include_examples 'does not pass database locations' + end + context 'database wal location was already provided' do let(:old_location) { '0/D525E3A8' } let(:new_location) { 'AB/12345' } - let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => { main: old_location } } } + let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => old_location } } + let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations } } before do allow(load_balancer).to receive(:primary_write_location).and_return(new_location) allow(load_balancer).to receive(:database_replica_location).and_return(new_location) end + shared_examples_for 'does not set database location again' do |use_primary| + before do + allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(use_primary) + end + + it 'does not set database locations again' do + run_middleware + + expect(job['wal_locations']).to eq(wal_locations) + end + end + context "when write was performed" do include_examples 'does not set database location again', true end @@ -147,24 +162,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do end end - context 'when worker cannot be constantized' do - let(:worker_class) { 'ActionMailer::MailDeliveryJob' } - let(:expected_consistency) { :always } - - include_examples 'does not pass database locations' - end - - context 'when worker class does not include ApplicationWorker' do - let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper } - let(:expected_consistency) { :always } - - include_examples 'does not pass database locations' - end - - context 'database wal location was already provided' do - include_examples 'database location was already provided' - end - context 'when worker data consistency is :always' do include_context 'data consistency worker class', :always, :load_balancing_for_test_data_consistency_worker diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb index fcb1d52fbaf..1fe348bbf2e 100644 --- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb @@ -63,10 +63,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do end shared_examples_for 'replica is up to date' do |expected_strategy| - let(:wal_locations) { { main: '0/D525E3A8' } } + let(:location) {'0/D525E3A8' } + let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } } it 'does not stick to the primary', :aggregate_failures do - expect(load_balancer).to receive(:select_up_to_date_host).with(wal_locations[:main]).and_return(true) + expect(load_balancer).to receive(:select_up_to_date_host).with(location).and_return(true) run_middleware do expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).not_to be_truthy @@ -91,7 +92,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'wal_locations' => wal_locations } } before do - allow(load_balancer).to receive(:select_up_to_date_host).with(wal_locations[:main]).and_return(true) + allow(load_balancer).to receive(:select_up_to_date_host).with(location).and_return(true) end it_behaves_like 'replica is up to date', 'replica' diff --git a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb new file mode 100644 index 00000000000..8c3d372cc55 --- /dev/null +++ b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter do + subject { described_class.import } + + it_behaves_like 'work item base types importer' +end diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb new file mode 100644 index 00000000000..ac2695977c4 --- /dev/null +++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pagination::CursorBasedKeyset do + subject { described_class } + + describe '.available_for_type?' do + it 'returns true for Group' do + expect(subject.available_for_type?(Group.all)).to be_truthy + end + + it 'return false for other types of relations' do + expect(subject.available_for_type?(User.all)).to be_falsey + end + end + + describe '.available?' do + let(:request_context) { double('request_context', params: { order_by: order_by, sort: sort }) } + let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request_context) } + + context 'with order-by name asc' do + let(:order_by) { :name } + let(:sort) { :asc } + + it 'returns true for Group' do + expect(subject.available?(cursor_based_request_context, Group.all)).to be_truthy + end + + it 'return false for other types of relations' do + expect(subject.available?(cursor_based_request_context, User.all)).to be_falsey + end + end + + context 'with other order-by columns' do + let(:order_by) { :path } + let(:sort) { :asc } + + it 'returns false for Group' do + expect(subject.available?(cursor_based_request_context, Group.all)).to be_falsey + end + + it 'return false for other types of relations' do + expect(subject.available?(cursor_based_request_context, User.all)).to be_falsey + end + end + end +end diff --git a/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb b/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb index 4998abd2186..79de6f230ec 100644 --- a/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb @@ -3,32 +3,40 @@ require 'spec_helper' RSpec.describe Gitlab::Pagination::Keyset::CursorBasedRequestContext do - let(:params) { { per_page: 2, cursor: 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==' } } - let(:request) { double('request', params: params) } + let(:params) { { per_page: 2, cursor: 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==', order_by: :name, sort: :asc } } + let(:request) { double('request', url: 'http://localhost') } + let(:request_context) { double('request_context', header: nil, params: params, request: request) } describe '#per_page' do - subject(:per_page) { described_class.new(request).per_page } + subject(:per_page) { described_class.new(request_context).per_page } it { is_expected.to eq 2 } end describe '#cursor' do - subject(:cursor) { described_class.new(request).cursor } + subject(:cursor) { described_class.new(request_context).cursor } it { is_expected.to eq 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==' } end + describe '#order_by' do + subject(:order_by) { described_class.new(request_context).order_by } + + it { is_expected.to eq({ name: :asc }) } + end + describe '#apply_headers' do - let(:request) { double('request', url: "http://#{Gitlab.config.gitlab.host}/api/v4/projects?per_page=3", params: params) } + let(:request) { double('request', url: "http://#{Gitlab.config.gitlab.host}/api/v4/projects?per_page=3") } let(:params) { { per_page: 3 } } + let(:request_context) { double('request_context', header: nil, params: params, request: request) } let(:cursor_for_next_page) { 'eyJuYW1lIjoiSDVicCIsImlkIjoiMjgiLCJfa2QiOiJuIn0=' } - subject(:apply_headers) { described_class.new(request).apply_headers(cursor_for_next_page) } + subject(:apply_headers) { described_class.new(request_context).apply_headers(cursor_for_next_page) } it 'sets Link header with same host/path as the original request' do - orig_uri = URI.parse(request.url) + orig_uri = URI.parse(request_context.request.url) - expect(request).to receive(:header).once do |name, header| + expect(request_context).to receive(:header).once do |name, header| first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures uri = URI.parse(first_link) @@ -42,9 +50,9 @@ RSpec.describe Gitlab::Pagination::Keyset::CursorBasedRequestContext do end it 'sets Link header with a cursor to the next page' do - orig_uri = URI.parse(request.url) + orig_uri = URI.parse(request_context.request.url) - expect(request).to receive(:header).once do |name, header| + expect(request_context).to receive(:header).once do |name, header| first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures query = CGI.parse(URI.parse(first_link).query) diff --git a/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb b/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb index 5ba381834ea..783e728b34c 100644 --- a/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb +++ b/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Gitlab::Pagination::Keyset::CursorPager do let(:relation) { Group.all.order(:name, :id) } let(:per_page) { 3 } let(:params) { { cursor: nil, per_page: per_page } } - let(:request) { double('request', params: params) } - let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request) } + let(:request_context) { double('request_context', params: params) } + let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request_context) } before_all do create_list(:group, 7) @@ -33,7 +33,7 @@ RSpec.describe Gitlab::Pagination::Keyset::CursorPager do it 'passes information about next page to request' do cursor_for_next_page = relation.keyset_paginate(**params).cursor_for_next_page - expect_next_instance_of(Gitlab::Pagination::Keyset::HeaderBuilder, cursor_based_request_context) do |builder| + expect_next_instance_of(Gitlab::Pagination::Keyset::HeaderBuilder, request_context) do |builder| expect(builder).to receive(:add_next_page_header).with({ cursor: cursor_for_next_page }) end diff --git a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb new file mode 100644 index 00000000000..b9cc9de88cc --- /dev/null +++ b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('upsert_base_work_item_types') + +RSpec.describe UpsertBaseWorkItemTypes, :migration do + let!(:work_item_types) { table(:work_item_types) } + + context 'when no default types exist' do + it 'creates default data' do + expect(work_item_types.count).to eq(0) + + reversible_migration do |migration| + migration.before -> { + # Depending on whether the migration has been run before, + # the size could be 4, or 0, so we don't set any expectations + # as we don't delete base types on migration reverse + } + + migration.after -> { + expect(work_item_types.count).to eq(4) + expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values) + } + end + end + end + + context 'when default types already exist' do + before do + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import + end + + it 'does not create default types again' do + expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values) + + reversible_migration do |migration| + migration.before -> { + expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values) + } + + migration.after -> { + expect(work_item_types.count).to eq(4) + expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values) + } + end + end + end +end diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb index 04fcaab4c2d..4e8d49585d0 100644 --- a/spec/models/ci/pipeline_variable_spec.rb +++ b/spec/models/ci/pipeline_variable_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Ci::PipelineVariable do it_behaves_like "CI variable" - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:pipeline_id) } + it { is_expected.to validate_presence_of(:key) } describe '#hook_attrs' do let(:variable) { create(:ci_pipeline_variable, key: 'foo', value: 'bar') } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index ff0d7ecceb5..c6b4d82bf15 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -61,6 +61,34 @@ RSpec.describe 'getting an issue list for a project' do end end + context 'filtering by my_reaction_emoji' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) } + + let(:issue_a_gid) { issue_a.to_global_id.to_s } + let(:issue_b_gid) { issue_b.to_global_id.to_s } + + where(:value, :gids) do + 'thumbsup' | lazy { [issue_a_gid] } + 'ANY' | lazy { [issue_a_gid] } + 'any' | lazy { [issue_a_gid] } + 'AnY' | lazy { [issue_a_gid] } + 'NONE' | lazy { [issue_b_gid] } + 'thumbsdown' | lazy { [] } + end + + with_them do + let(:issue_filter_params) { { my_reaction_emoji: value } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(graphql_dig_at(issues_data, :node, :id)).to eq(gids) + end + end + end + context 'when limiting the number of results' do let(:query) do <<~GQL diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 30df47ccc41..38abedde7da 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -158,6 +158,127 @@ RSpec.describe API::Groups do end end + context 'pagination strategies' do + let_it_be(:group_1) { create(:group, name: '1_group') } + let_it_be(:group_2) { create(:group, name: '2_group') } + + context 'when the user is anonymous' do + context 'offset pagination' do + context 'on making requests beyond the allowed offset pagination threshold' do + it 'returns error and suggests to use keyset pagination' do + get api('/groups'), params: { page: 3000, per_page: 25 } + + expect(response).to have_gitlab_http_status(:method_not_allowed) + expect(json_response['error']).to eq( + 'Offset pagination has a maximum allowed offset of 50000 for requests that return objects of type Group. '\ + 'Remaining records can be retrieved using keyset pagination.' + ) + end + + context 'when the feature flag `keyset_pagination_for_groups_api` is disabled' do + before do + stub_feature_flags(keyset_pagination_for_groups_api: false) + end + + it 'returns successful response' do + get api('/groups'), params: { page: 3000, per_page: 25 } + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'on making requests below the allowed offset pagination threshold' do + it 'paginates the records' do + get api('/groups'), params: { page: 1, per_page: 1 } + + expect(response).to have_gitlab_http_status(:ok) + records = json_response + expect(records.size).to eq(1) + expect(records.first['id']).to eq(group_1.id) + + # next page + + get api('/groups'), params: { page: 2, per_page: 1 } + + expect(response).to have_gitlab_http_status(:ok) + records = Gitlab::Json.parse(response.body) + expect(records.size).to eq(1) + expect(records.first['id']).to eq(group_2.id) + end + end + end + + context 'keyset pagination' do + def pagination_links(response) + link = response.headers['LINK'] + return unless link + + link.split(',').map do |link| + match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/) + break nil unless match + + { url: match[:url], rel: match[:rel] } + end.compact + end + + def params_for_next_page(response) + next_url = pagination_links(response).find { |link| link[:rel] == 'next' }[:url] + Rack::Utils.parse_query(URI.parse(next_url).query) + end + + context 'on making requests with supported ordering structure' do + it 'paginates the records correctly' do + # first page + get api('/groups'), params: { pagination: 'keyset', per_page: 1 } + + expect(response).to have_gitlab_http_status(:ok) + records = json_response + expect(records.size).to eq(1) + expect(records.first['id']).to eq(group_1.id) + + params_for_next_page = params_for_next_page(response) + expect(params_for_next_page).to include('cursor') + + get api('/groups'), params: params_for_next_page + + expect(response).to have_gitlab_http_status(:ok) + records = Gitlab::Json.parse(response.body) + expect(records.size).to eq(1) + expect(records.first['id']).to eq(group_2.id) + end + + context 'when the feature flag `keyset_pagination_for_groups_api` is disabled' do + before do + stub_feature_flags(keyset_pagination_for_groups_api: false) + end + + it 'ignores the keyset pagination params and performs offset pagination' do + get api('/groups'), params: { pagination: 'keyset', per_page: 1 } + + expect(response).to have_gitlab_http_status(:ok) + records = json_response + expect(records.size).to eq(1) + expect(records.first['id']).to eq(group_1.id) + + params_for_next_page = params_for_next_page(response) + expect(params_for_next_page).not_to include('cursor') + end + end + end + + context 'on making requests with unsupported ordering structure' do + it 'returns error' do + get api('/groups'), params: { pagination: 'keyset', per_page: 1, order_by: 'path', sort: 'desc' } + + expect(response).to have_gitlab_http_status(:method_not_allowed) + expect(json_response['error']).to eq('Keyset pagination is not yet available for this type of request') + end + end + end + end + end + context "when authenticated as admin" do it "admin: returns an array of all groups" do get api("/groups", admin) diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb new file mode 100644 index 00000000000..2c02b76d49b --- /dev/null +++ b/spec/support/shared_examples/work_item_base_types_importer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'work item base types importer' do + it 'creates all base work item types' do + expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count) + end +end diff --git a/tooling/bin/find_tests b/tooling/bin/find_tests index a82b6bb5d46..97fadf406fe 100755 --- a/tooling/bin/find_tests +++ b/tooling/bin/find_tests @@ -22,7 +22,10 @@ changed_files = mr_changes.changes.map { |change| change['new_path'] } tff = TestFileFinder::FileFinder.new(paths: changed_files).tap do |file_finder| file_finder.use TestFileFinder::MappingStrategies::PatternMatching.load('tests.yml') - file_finder.use TestFileFinder::MappingStrategies::DirectMatching.load_json(ENV['RSPEC_TESTS_MAPPING_PATH']) + + if ENV['RSPEC_TESTS_MAPPING_ENABLED'] + file_finder.use TestFileFinder::MappingStrategies::DirectMatching.load_json(ENV['RSPEC_TESTS_MAPPING_PATH']) + end end File.write(output_file, tff.test_files.uniq.join(' ')) |