diff --git a/Gemfile b/Gemfile index 6cbb1a4351f..95f39e2bd3b 100644 --- a/Gemfile +++ b/Gemfile @@ -332,7 +332,7 @@ gem 'sentry-sidekiq', '~> 5.1.1' # PostgreSQL query parsing # -gem 'pg_query', '~> 2.1.4' +gem 'pg_query', '~> 2.2' gem 'premailer-rails', '~> 1.10.3' diff --git a/Gemfile.checksum b/Gemfile.checksum index 09bbd1fa875..5fe80ce1593 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -410,7 +410,7 @@ {"name":"pg","version":"1.4.3","platform":"x64-mingw-ucrt","checksum":"9f4d1d39af5ae5eea9f3c6b1e3092cbd5d26b716ff0e1283cf71c0690c69b36c"}, {"name":"pg","version":"1.4.3","platform":"x64-mingw32","checksum":"3265afd0e00331c7c70e50d4a13eea9083e5b683ebcd808bd671af70d92b189e"}, {"name":"pg","version":"1.4.3","platform":"x86-mingw32","checksum":"08a6ef4c702e313c1a04ad6b088b1843361ca8606843c7cd607e181e0d4e5508"}, -{"name":"pg_query","version":"2.1.4","platform":"ruby","checksum":"48f1363f88cf9d86fa11d76d1b0f839ca3723b8bd397b7cbc4b578e1ca82d0bb"}, +{"name":"pg_query","version":"2.2.0","platform":"ruby","checksum":"84a37548412f540061bcc52ee2915352297832816bca60e4524c716e03f1e950"}, {"name":"plist","version":"3.6.0","platform":"ruby","checksum":"f468bcf6b72ec6d1585ed6744eb4817c1932a5bf91895ed056e69b7f12ca10f2"}, {"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"}, {"name":"po_to_json","version":"1.0.1","platform":"ruby","checksum":"6a7188aa6c42a22c9718f9b39062862ef7f3d8f6a7b4177cae058c3308b56af7"}, diff --git a/Gemfile.lock b/Gemfile.lock index 6d8244cadcb..eb3f1002b73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1034,7 +1034,7 @@ GEM peek (1.1.0) railties (>= 4.0.0) pg (1.4.3) - pg_query (2.1.4) + pg_query (2.2.0) google-protobuf (>= 3.19.2) plist (3.6.0) png_quantizator (0.2.1) @@ -1730,7 +1730,7 @@ DEPENDENCIES parslet (~> 1.8) peek (~> 1.1) pg (~> 1.4.3) - pg_query (~> 2.1.4) + pg_query (~> 2.2) png_quantizator (~> 0.2.1) premailer-rails (~> 1.10.3) prometheus-client-mmap (~> 0.16) diff --git a/app/graphql/resolvers/bulk_labels_resolver.rb b/app/graphql/resolvers/bulk_labels_resolver.rb index 7362e257fb6..d7e9564352d 100644 --- a/app/graphql/resolvers/bulk_labels_resolver.rb +++ b/app/graphql/resolvers/bulk_labels_resolver.rb @@ -9,7 +9,7 @@ module Resolvers def resolve authorize!(object) - BatchLoader::GraphQL.for(object.id).batch(cache: false) do |ids, loader, args| + BatchLoader::GraphQL.for(object.id).batch(key: object.class.name, cache: false) do |ids, loader, args| labels = Label.for_targets(object.class.id_in(ids)).group_by(&:target_id) ids.each do |id| diff --git a/app/graphql/resolvers/concerns/board_item_filterable.rb b/app/graphql/resolvers/concerns/board_item_filterable.rb index 1457a02e44f..9c0ada4f72c 100644 --- a/app/graphql/resolvers/concerns/board_item_filterable.rb +++ b/app/graphql/resolvers/concerns/board_item_filterable.rb @@ -14,6 +14,16 @@ module BoardItemFilterable set_filter_values(filters[:not]) end + if filters[:or] + if ::Feature.disabled?(:or_issuable_queries, resource_parent) + raise ::Gitlab::Graphql::Errors::ArgumentError, + "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled." + end + + rewrite_param_name(filters[:or], :author_usernames, :author_username) + rewrite_param_name(filters[:or], :assignee_usernames, :assignee_username) + end + filters end @@ -30,6 +40,14 @@ module BoardItemFilterable filters[:assignee_id] = filters.delete(:assignee_wildcard_id) end end + + def rewrite_param_name(filters, old_name, new_name) + filters[new_name] = filters.delete(old_name) if filters[old_name].present? + end + + def resource_parent + respond_to?(:board) ? board.resource_parent : list.board.resource_parent + end end ::BoardItemFilterable.prepend_mod_with('Resolvers::BoardItemFilterable') diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 8295bd58388..69a32d67330 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -67,6 +67,9 @@ module IssueResolverArguments argument :not, Types::Issues::NegatedIssueFilterInputType, description: 'Negated arguments.', required: false + argument :or, Types::Issues::UnionedIssueFilterInputType, + description: 'List of arguments with inclusive OR.', + required: false argument :crm_contact_id, GraphQL::Types::String, required: false, description: 'ID of a contact assigned to the issues.' @@ -84,7 +87,12 @@ module IssueResolverArguments end def ready?(**args) - args[:not] = args[:not].to_h if args[:not].present? + if args[:or].present? && ::Feature.disabled?(:or_issuable_queries, resource_parent) + raise ::Gitlab::Graphql::Errors::ArgumentError, "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled." + end + + args[:not] = args[:not].to_h if args[:not] + args[:or] = args[:or].to_h if args[:or] params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args) params_not_mutually_exclusive(args, mutually_exclusive_milestone_args) @@ -116,9 +124,12 @@ module IssueResolverArguments def prepare_finder_params(args) params = super(args) + params[:not] = params[:not].to_h if params[:not] + params[:or] = params[:or].to_h if params[:or] params[:iids] ||= [params.delete(:iid)].compact if params[:iid] params[:attempt_project_search_optimizations] = true if params[:search].present? + prepare_author_username_params(params) prepare_assignee_username_params(params) prepare_release_tag_params(params) @@ -132,9 +143,14 @@ module IssueResolverArguments args[:release_tag] ||= release_tag_wildcard end + def prepare_author_username_params(args) + args[:or][:author_username] = args[:or].delete(:author_usernames) if args.dig(:or, :author_usernames).present? + end + 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? + args[:or][:assignee_username] = args[:or].delete(:assignee_usernames) if args.dig(:or, :assignee_usernames).present? end def mutually_exclusive_release_tag_args diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb index 0dd7fbc87da..897e3d05948 100644 --- a/app/graphql/types/boards/board_issue_input_type.rb +++ b/app/graphql/types/boards/board_issue_input_type.rb @@ -9,6 +9,10 @@ module Types required: false, description: 'List of negated arguments.' + argument :or, Types::Issues::UnionedIssueFilterInputType, + required: false, + description: 'List of arguments with inclusive OR.' + argument :search, GraphQL::Types::String, required: false, description: 'Search query for issue title or description.' diff --git a/app/graphql/types/issues/unioned_issue_filter_input_type.rb b/app/graphql/types/issues/unioned_issue_filter_input_type.rb new file mode 100644 index 00000000000..9c7261279c7 --- /dev/null +++ b/app/graphql/types/issues/unioned_issue_filter_input_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Issues + class UnionedIssueFilterInputType < BaseInputObject + graphql_name 'UnionedIssueFilterInput' + + argument :assignee_usernames, [GraphQL::Types::String], + required: false, + description: 'Filters issues that are assigned to at least one of the given users.' + argument :author_usernames, [GraphQL::Types::String], + required: false, + description: 'Filters issues that are authored by one of the given users.' + end + end +end diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb index 735d4e4298c..e70209f1538 100644 --- a/app/models/incident_management/timeline_event.rb +++ b/app/models/incident_management/timeline_event.rb @@ -18,6 +18,8 @@ module IncidentManagement validates :project, :incident, :occurred_at, presence: true validates :action, presence: true, length: { maximum: 128 } + # `user_input` is a note filled in by a user via API. Not auto generated by GitLab + validates :note, presence: true, length: { maximum: 280 }, on: :user_input validates :note, presence: true, length: { maximum: 10_000 } validates :note_html, length: { maximum: 10_000 } diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb index bf3de5c9293..32b9d3eab7b 100644 --- a/app/services/incident_management/timeline_events/create_service.rb +++ b/app/services/incident_management/timeline_events/create_service.rb @@ -97,7 +97,7 @@ module IncidentManagement timeline_event = IncidentManagement::TimelineEvent.new(timeline_event_params) - if timeline_event.save + if timeline_event.save(context: validation_context) add_system_note(timeline_event) track_usage_event(:incident_management_timeline_event_created, user.id) @@ -122,6 +122,10 @@ module IncidentManagement SystemNoteService.add_timeline_event(timeline_event) end + + def validation_context + :user_input if !auto_created && params[:promoted_from_note].blank? + end end end end diff --git a/app/services/incident_management/timeline_events/update_service.rb b/app/services/incident_management/timeline_events/update_service.rb index 012e2f0e260..8d4e29c6857 100644 --- a/app/services/incident_management/timeline_events/update_service.rb +++ b/app/services/incident_management/timeline_events/update_service.rb @@ -8,18 +8,23 @@ module IncidentManagement # @option params [string] note # @option params [datetime] occurred_at class UpdateService < TimelineEvents::BaseService + VALIDATION_CONTEXT = :user_input + def initialize(timeline_event, user, params) @timeline_event = timeline_event @incident = timeline_event.incident @user = user @note = params[:note] @occurred_at = params[:occurred_at] + @validation_context = VALIDATION_CONTEXT end def execute return error_no_permissions unless allowed? - if timeline_event.update(update_params) + timeline_event.assign_attributes(update_params) + + if timeline_event.save(context: validation_context) add_system_note(timeline_event) track_usage_event(:incident_management_timeline_event_edited, user.id) @@ -31,7 +36,7 @@ module IncidentManagement private - attr_reader :timeline_event, :incident, :user, :note, :occurred_at + attr_reader :timeline_event, :incident, :user, :note, :occurred_at, :validation_context def update_params { updated_by_user: user, note: note, occurred_at: occurred_at }.compact diff --git a/db/post_migrate/20221019141508_add_index_to_test_reports_issue_id_created_at_and_id.rb b/db/post_migrate/20221019141508_add_index_to_test_reports_issue_id_created_at_and_id.rb new file mode 100644 index 00000000000..054512adf2e --- /dev/null +++ b/db/post_migrate/20221019141508_add_index_to_test_reports_issue_id_created_at_and_id.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddIndexToTestReportsIssueIdCreatedAtAndId < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + TABLE_NAME = 'requirements_management_test_reports' + INDEX_NAME = 'idx_test_reports_on_issue_id_created_at_and_id' + + def up + add_concurrent_index TABLE_NAME, [:issue_id, :created_at, :id], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME + end +end diff --git a/db/schema_migrations/20221019141508 b/db/schema_migrations/20221019141508 new file mode 100644 index 00000000000..2b4ecc805e4 --- /dev/null +++ b/db/schema_migrations/20221019141508 @@ -0,0 +1 @@ +527b18e3bd89316c33b099d4e3cd622617b6e8dbb482a0f0ce983386b0210f7e \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4179ff49548..691a2e29922 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -27937,6 +27937,8 @@ CREATE UNIQUE INDEX idx_serverless_domain_cluster_on_clusters_applications_knati CREATE INDEX idx_streaming_headers_on_external_audit_event_destination_id ON audit_events_streaming_headers USING btree (external_audit_event_destination_id); +CREATE INDEX idx_test_reports_on_issue_id_created_at_and_id ON requirements_management_test_reports USING btree (issue_id, created_at, id); + CREATE INDEX idx_user_details_on_provisioned_by_group_id_user_id ON user_details USING btree (provisioned_by_group_id, user_id); CREATE UNIQUE INDEX idx_vuln_signatures_on_occurrences_id_and_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, signature_sha); diff --git a/doc/administration/operations/moving_repositories.md b/doc/administration/operations/moving_repositories.md index 75078568c44..96c1fcc422d 100644 --- a/doc/administration/operations/moving_repositories.md +++ b/doc/administration/operations/moving_repositories.md @@ -376,14 +376,3 @@ sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ /home/git/repositories \ /mnt/gitlab/repositories ``` - -## Troubleshooting - -See the following for information on troubleshooting repository moves. - -### Repository move fails for archived projects - -Because of a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/363670), -[archived projects](../../user/project/settings/index.md#advanced-project-settings) fail to move even though the data is cloned -by Gitaly. Make sure archived projects are -[unarchived](../../user/project/settings/index.md#unarchive-a-project) before initiating a move. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4eca621b2da..efbddb2e774 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -13207,6 +13207,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | | `myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | `not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | +| `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | | `search` | [`String`](#string) | Search query for title or description. | | `sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. | | `state` | [`IssuableState`](#issuablestate) | Current state of this issue. | @@ -16871,6 +16872,7 @@ Returns [`Issue`](#issue). | `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | | `myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | `not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | +| `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | | `releaseTag` | [`[String!]`](#string) | Release tag associated with the issue's milestone. | | `releaseTagWildcardId` | [`ReleaseTagWildcardId`](#releasetagwildcardid) | Filter issues by release tag ID wildcard. | | `search` | [`String`](#string) | Search query for title or description. | @@ -16910,6 +16912,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype). | `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | | `myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | `not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | +| `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | | `releaseTag` | [`[String!]`](#string) | Release tag associated with the issue's milestone. | | `releaseTagWildcardId` | [`ReleaseTagWildcardId`](#releasetagwildcardid) | Filter issues by release tag ID wildcard. | | `search` | [`String`](#string) | Search query for title or description. | @@ -16956,6 +16959,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | | `myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | `not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | +| `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | | `releaseTag` | [`[String!]`](#string) | Release tag associated with the issue's milestone. | | `releaseTagWildcardId` | [`ReleaseTagWildcardId`](#releasetagwildcardid) | Filter issues by release tag ID wildcard. | | `search` | [`String`](#string) | Search query for title or description. | @@ -23541,6 +23545,7 @@ Field that are available while modifying the custom mapping attributes for an HT | `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter by milestone ID wildcard. | | `myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | | `not` | [`NegatedBoardIssueInput`](#negatedboardissueinput) | List of negated arguments. | +| `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | | `releaseTag` | [`String`](#string) | Filter by release tag. | | `search` | [`String`](#string) | Search query for issue title or description. | | `types` | [`[IssueType!]`](#issuetype) | Filter by the given issue types. | @@ -23945,6 +23950,15 @@ A time-frame defined as a closed inclusive range of two dates. | `end` | [`Date!`](#date) | End of the range. | | `start` | [`Date!`](#date) | Start of the range. | +### `UnionedIssueFilterInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `assigneeUsernames` | [`[String!]`](#string) | Filters issues that are assigned to at least one of the given users. | +| `authorUsernames` | [`[String!]`](#string) | Filters issues that are authored by one of the given users. | + ### `UpdateDiffImagePositionInput` #### Arguments diff --git a/spec/models/incident_management/timeline_event_spec.rb b/spec/models/incident_management/timeline_event_spec.rb index d288cc1a75d..036f5affb87 100644 --- a/spec/models/incident_management/timeline_event_spec.rb +++ b/spec/models/incident_management/timeline_event_spec.rb @@ -27,6 +27,7 @@ RSpec.describe IncidentManagement::TimelineEvent do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:incident) } it { is_expected.to validate_presence_of(:note) } + it { is_expected.to validate_length_of(:note).is_at_most(280).on(:user_input) } it { is_expected.to validate_length_of(:note).is_at_most(10_000) } it { is_expected.to validate_length_of(:note_html).is_at_most(10_000) } it { is_expected.to validate_presence_of(:occurred_at) } diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index a9acced1efa..9bed720c815 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -21,6 +21,7 @@ RSpec.describe 'get board lists' do let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] } let(:lists_data) { board_data['lists']['nodes'][0] } let(:issues_data) { lists_data['issues']['nodes'] } + let(:issue_params) { { filters: { label_name: label2.title, confidential: confidential }, first: 3 } } def query(list_params = params) graphql_query_for( @@ -31,7 +32,7 @@ RSpec.describe 'get board lists' do nodes { lists { nodes { - issues(filters: {labelName: "#{label2.title}", confidential: #{confidential}}, first: 3) { + issues(#{attributes_to_graphql(issue_params)}) { count nodes { #{all_graphql_fields_for('issues'.classify)} @@ -77,18 +78,23 @@ RSpec.describe 'get board lists' do end context 'when user can read the board' do - before do + before_all do board_parent.add_reporter(user) - post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) end + subject { post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) } + it 'can access the issues', :aggregate_failures do + subject + # ties for relative positions are broken by id in ascending order by default expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title]) expect(issue_relative_positions).not_to include(nil) end it 'does not set the relative positions of the issues not being returned', :aggregate_failures do + subject + expect(issue_id).not_to include(issue6.id) expect(issue3.relative_position).to be_nil end @@ -97,10 +103,36 @@ RSpec.describe 'get board lists' do let(:confidential) { true } it 'returns matching issue' do + subject + expect(issue_titles).to match_array([issue7.title]) expect(issue_relative_positions).not_to include(nil) end end + + context 'when filtering by a unioned argument' do + let(:another_user) { create(:user) } + let(:issue_params) { { filters: { or: { assignee_usernames: [user.username, another_user.username] } } } } + + it 'returns correctly filtered issues' do + issue1.assignee_ids = user.id + issue2.assignee_ids = another_user.id + + subject + + expect(issue_id).to contain_exactly(issue1.to_gid.to_s, issue2.to_gid.to_s) + end + + context 'when feature flag is disabled' do + it 'returns an error' do + stub_feature_flags(or_issuable_queries: false) + + subject + + expect_graphql_errors_to_include("'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled.") + end + end + end end end diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 6fe2e41cf35..ad7df5c9344 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -49,7 +49,7 @@ RSpec.describe 'get board lists' do end shared_examples 'group and project board lists query' do - let!(:board) { create(:board, resource_parent: board_parent) } + let_it_be(:board) { create(:board, resource_parent: board_parent) } context 'when the user does not have access to the board' do it 'returns nil' do @@ -107,16 +107,20 @@ RSpec.describe 'get board lists' do end context 'when querying for a single list' do + let_it_be(:label_list) { create(:list, board: board, label: label, position: 10) } + let_it_be(:issues) do + [ + create(:issue, project: project, labels: [label, label2]), + create(:issue, project: project, labels: [label, label2], confidential: true), + create(:issue, project: project, labels: [label]) + ] + end + before do board_parent.add_reporter(user) end it 'returns the correct list with issue count for matching issue filters' do - label_list = create(:list, board: board, label: label, position: 10) - create(:issue, project: project, labels: [label, label2]) - create(:issue, project: project, labels: [label, label2], confidential: true) - create(:issue, project: project, labels: [label]) - post_graphql( query( id: global_id_of(label_list), @@ -131,21 +135,56 @@ RSpec.describe 'get board lists' do expect(list_node['issuesCount']).to eq 1 end end + + context 'when filtering by a unioned argument' do + let_it_be(:another_user) { create(:user) } + + it 'returns correctly filtered issues' do + issues[0].assignee_ids = user.id + issues[1].assignee_ids = another_user.id + + post_graphql( + query( + id: global_id_of(label_list), + issueFilters: { or: { assignee_usernames: [user.username, another_user.username] } } + ), current_user: user + ) + + expect(lists_data[0]['node']['issuesCount']).to eq 2 + end + + context 'when feature flag is disabled' do + it 'returns an error' do + stub_feature_flags(or_issuable_queries: false) + + post_graphql( + query( + id: global_id_of(label_list), + issueFilters: { or: { assignee_usernames: [user.username, another_user.username] } } + ), current_user: user + ) + + expect_graphql_errors_to_include( + "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled." + ) + end + end + end end end describe 'for a project' do - let(:board_parent) { project } - let(:label) { project_label } - let(:label2) { project_label2 } + let_it_be(:board_parent) { project } + let_it_be(:label) { project_label } + let_it_be(:label2) { project_label2 } it_behaves_like 'group and project board lists query' end describe 'for a group' do - let(:board_parent) { group } - let(:label) { group_label } - let(:label2) { group_label2 } + let_it_be(:board_parent) { group } + let_it_be(:label) { group_label } + let_it_be(:label2) { group_label2 } before do allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb index 923e12a3c06..bac78149cf9 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -57,4 +57,11 @@ RSpec.describe 'Creating an incident timeline event' do 'occurredAt' => event_occurred_at.iso8601 ) end + + context 'when note is more than 280 characters long' do + let_it_be(:note) { 'n' * 281 } + + it_behaves_like 'timeline event mutation responds with validation error', + error_message: 'Timeline text is too long (maximum is 280 characters)' + end end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index 85eaec90f47..62eeecb3fb7 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -9,7 +9,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:incident) { create(:incident, project: project) } - let_it_be(:comment) { create(:note, project: project, noteable: incident) } + let_it_be(:comment) { create(:note, project: project, noteable: incident, note: 'a' * 281) } let(:input) { { note_id: comment.to_global_id.to_s } } let(:mutation) do diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb index 1c4439cec6f..542d51b990f 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb @@ -13,11 +13,12 @@ RSpec.describe 'Updating an incident timeline event' do end let(:occurred_at) { 1.minute.ago.iso8601 } + let(:note) { 'Updated note' } let(:variables) do { id: timeline_event.to_global_id.to_s, - note: 'Updated note', + note: note, occurred_at: occurred_at } end @@ -70,11 +71,18 @@ RSpec.describe 'Updating an incident timeline event' do 'id' => incident.to_global_id.to_s, 'title' => incident.title }, - 'note' => 'Updated note', + 'note' => note, 'noteHtml' => timeline_event.note_html, 'occurredAt' => occurred_at, 'createdAt' => timeline_event.created_at.iso8601, 'updatedAt' => timeline_event.updated_at.iso8601 ) end + + context 'when note is more than 280 characters long' do + let(:note) { 'n' * 281 } + + it_behaves_like 'timeline event mutation responds with validation error', + error_message: 'Timeline text is too long (maximum is 280 characters)' + end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 514fb6ff733..3d09206fa96 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -56,6 +56,62 @@ RSpec.describe 'getting an issue list for a project' do end end + context 'when filtering by a negated argument' do + let(:issue_filter_params) { { not: { assignee_usernames: current_user.username } } } + + it 'returns correctly filtered issues' do + issue_a.assignee_ids = current_user.id + + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue_b_gid) + end + + context 'when argument is blank' do + let(:issue_filter_params) { { not: {} } } + + it 'does not raise an error' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + end + end + end + + context 'when filtering by a unioned argument' do + let(:another_user) { create(:user) } + let(:issue_filter_params) { { or: { assignee_usernames: [current_user.username, another_user.username] } } } + + it 'returns correctly filtered issues' do + issue_a.assignee_ids = current_user.id + issue_b.assignee_ids = another_user.id + + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid) + end + + context 'when argument is blank' do + let(:issue_filter_params) { { or: {} } } + + it 'does not raise an error' do + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_be_empty + end + end + + context 'when feature flag is disabled' do + it 'returns an error' do + stub_feature_flags(or_issuable_queries: false) + + post_graphql(query, current_user: current_user) + + expect_graphql_errors_to_include("'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled.") + end + end + end + context 'filtering by my_reaction_emoji' do using RSpec::Parameterized::TableSyntax diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb index b9761234c51..6ab02ad0b0b 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -161,6 +161,38 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do it 'successfully creates a database record', :aggregate_failures do expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1) end + + context 'when note is more than 280 characters long' do + let(:args) do + { + note: 'a' * 281, + occurred_at: Time.current, + action: 'new comment', + promoted_from_note: comment, + auto_created: auto_created + } + end + + let(:auto_created) { false } + + context 'when was not promoted from note' do + let(:comment) { nil } + + context 'when auto_created is true' do + let(:auto_created) { true } + + it_behaves_like 'success response' + end + + context 'when auto_created is false' do + it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)' + end + end + + context 'when promoted from note' do + it_behaves_like 'success response' + end + end end describe 'automatically created timeline events' do diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb index 5d8518cf2ef..2373a73e108 100644 --- a/spec/services/incident_management/timeline_events/update_service_spec.rb +++ b/spec/services/incident_management/timeline_events/update_service_spec.rb @@ -87,6 +87,12 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do it_behaves_like 'error response', "Timeline text can't be blank" end + context 'when note is more than 280 characters long' do + let(:params) { { note: 'n' * 281, occurred_at: occurred_at } } + + it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)' + end + context 'when occurred_at is nil' do let(:params) { { note: 'Updated note' } } diff --git a/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb new file mode 100644 index 00000000000..fbfd1af2601 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'timeline event mutation responds with validation error' do |error_message:| + it 'responds with a validation error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to match_array([error_message]) + end +end diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index dbb24cc047e..a0cf3b1e978 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -6,12 +6,18 @@ require 'fileutils' RSpec.describe RepositoryCheck::SingleRepositoryWorker do subject(:worker) { described_class.new } + before do + allow(::Gitlab::Git::Repository).to receive(:new).and_call_original + end + it 'skips when the project has no push events' do project = create(:project, :repository, :wiki_disabled) project.events.destroy_all # rubocop: disable Cop/DestroyAll - break_project(project) - expect(worker).not_to receive(:git_fsck) + repository = instance_double(::Gitlab::Git::Repository) + allow(::Gitlab::Git::Repository).to receive(:new) + .with(project.repository_storage, "#{project.disk_path}.git", anything, anything) + .and_return(repository) worker.perform(project.id) @@ -21,7 +27,12 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do it 'fails when the project has push events and a broken repository' do project = create(:project, :repository) create_push_event(project) - break_project(project) + + repository = project.repository.raw + expect(repository).to receive(:fsck).and_raise(::Gitlab::Git::Repository::GitError) + expect(::Gitlab::Git::Repository).to receive(:new) + .with(project.repository_storage, "#{project.disk_path}.git", anything, anything) + .and_return(repository) worker.perform(project.id) @@ -32,7 +43,11 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do project = create(:project, :repository, :wiki_disabled) create_push_event(project) - expect(worker).to receive(:git_fsck).and_call_original + repository = project.repository.raw + expect(repository).to receive(:fsck).and_call_original + expect(::Gitlab::Git::Repository).to receive(:new) + .with(project.repository_storage, "#{project.disk_path}.git", anything, anything) + .and_return(repository) expect do worker.perform(project.id) @@ -50,7 +65,12 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do worker.perform(project.id) expect(project.reload.last_repository_check_failed).to eq(false) - break_wiki(project) + repository = project.wiki.repository.raw + expect(repository).to receive(:fsck).and_raise(::Gitlab::Git::Repository::GitError) + expect(::Gitlab::Git::Repository).to receive(:new) + .with(project.repository_storage, "#{project.disk_path}.wiki.git", anything, anything) + .and_return(repository) + worker.perform(project.id) expect(project.reload.last_repository_check_failed).to eq(true) @@ -59,7 +79,10 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do it 'skips wikis when disabled' do project = create(:project, :wiki_disabled) # Make sure the test would fail if the wiki repo was checked - break_wiki(project) + repository = instance_double(::Gitlab::Git::Repository) + allow(::Gitlab::Git::Repository).to receive(:new) + .with(project.repository_storage, "#{project.disk_path}.wiki.git", anything, anything) + .and_return(repository) subject.perform(project.id) @@ -88,31 +111,4 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do def create_push_event(project) project.events.create!(action: :pushed, author_id: create(:user).id) end - - def break_wiki(project) - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - break_repo(wiki_path(project)) - end - end - - def wiki_path(project) - project.wiki.repository.path_to_repo - end - - def break_project(project) - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - break_repo(project.repository.path_to_repo) - end - end - - def break_repo(repo) - # Create or replace blob ffffffffffffffffffffffffffffffffffffffff with an empty file - # This will make the repo invalid, _and_ 'git init' cannot fix it. - path = File.join(repo, 'objects', 'ff') - file = File.join(path, 'ffffffffffffffffffffffffffffffffffffff') - - FileUtils.mkdir_p(path) - FileUtils.rm_f(file) - FileUtils.touch(file) - end end