diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index a31910e3090..68a3493670d 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -8,6 +8,7 @@ gl-emoji { } .user-status-emoji { + margin-left: $gl-padding-4; margin-right: $gl-padding-4; gl-emoji { diff --git a/app/finders/incident_management/timeline_event_tags_finder.rb b/app/finders/incident_management/timeline_event_tags_finder.rb new file mode 100644 index 00000000000..71820bf7dcb --- /dev/null +++ b/app/finders/incident_management/timeline_event_tags_finder.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module IncidentManagement + class TimelineEventTagsFinder + def initialize(user, timeline_event, params = {}) + @user = user + @timeline_event = timeline_event + @params = params + end + + def execute + return ::IncidentManagement::TimelineEventTag.none unless allowed? + + timeline_event.timeline_event_tags + end + + private + + attr_reader :user, :timeline_event, :params + + def allowed? + Ability.allowed?(user, :read_incident_management_timeline_event_tag, timeline_event) + end + end +end diff --git a/app/graphql/mutations/incident_management/timeline_event/create.rb b/app/graphql/mutations/incident_management/timeline_event/create.rb index 1907954cada..419b814dc8c 100644 --- a/app/graphql/mutations/incident_management/timeline_event/create.rb +++ b/app/graphql/mutations/incident_management/timeline_event/create.rb @@ -18,6 +18,10 @@ module Mutations required: true, description: 'Timestamp of when the event occurred.' + argument :timeline_event_tag_names, [GraphQL::Types::String], + required: false, + description: copy_field_description(Types::IncidentManagement::TimelineEventType, :timeline_event_tags) + def resolve(incident_id:, **args) incident = authorized_find!(id: incident_id) diff --git a/app/graphql/resolvers/incident_management/timeline_event_tags_resolver.rb b/app/graphql/resolvers/incident_management/timeline_event_tags_resolver.rb new file mode 100644 index 00000000000..ac6577d119b --- /dev/null +++ b/app/graphql/resolvers/incident_management/timeline_event_tags_resolver.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Resolvers + module IncidentManagement + class TimelineEventTagsResolver < BaseResolver + include LooksAhead + + type ::Types::IncidentManagement::TimelineEventTagType.connection_type, null: true + + def resolve(**args) + apply_lookahead(::IncidentManagement::TimelineEventTagsFinder.new(current_user, object, args).execute) + end + end + end +end diff --git a/app/graphql/resolvers/incident_management/timeline_events_resolver.rb b/app/graphql/resolvers/incident_management/timeline_events_resolver.rb index b9978259e6b..0d46b1387b0 100644 --- a/app/graphql/resolvers/incident_management/timeline_events_resolver.rb +++ b/app/graphql/resolvers/incident_management/timeline_events_resolver.rb @@ -22,11 +22,17 @@ module Resolvers prepare: ->(id, ctx) { id.model_id } end - def resolve(**args) + def resolve_with_lookahead(**args) incident = args[:incident_id].find apply_lookahead(::IncidentManagement::TimelineEventsFinder.new(current_user, incident, args).execute) end + + def preloads + { + timeline_event_tags: [:timeline_event_tags] + } + end end end end diff --git a/app/graphql/types/incident_management/timeline_event_tag_type.rb b/app/graphql/types/incident_management/timeline_event_tag_type.rb new file mode 100644 index 00000000000..452294d4797 --- /dev/null +++ b/app/graphql/types/incident_management/timeline_event_tag_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + module IncidentManagement + class TimelineEventTagType < BaseObject + graphql_name 'TimelineEventTagType' + + description 'Describes a tag on an incident management timeline event.' + + authorize :read_incident_management_timeline_event_tag + + field :id, + Types::GlobalIDType[::IncidentManagement::TimelineEventTag], + null: false, + description: 'ID of the timeline event tag.' + + field :name, + GraphQL::Types::String, + null: false, + description: 'Name of the timeline event tag.' + end + end +end diff --git a/app/graphql/types/incident_management/timeline_event_type.rb b/app/graphql/types/incident_management/timeline_event_type.rb index 690facc8732..939dd9f09e5 100644 --- a/app/graphql/types/incident_management/timeline_event_type.rb +++ b/app/graphql/types/incident_management/timeline_event_type.rb @@ -53,6 +53,13 @@ module Types null: false, description: 'Timestamp when the event occurred.' + field :timeline_event_tags, + ::Types::IncidentManagement::TimelineEventTagType.connection_type, + null: true, + description: 'Tags for the incident timeline event.', + extras: [:lookahead], + resolver: Resolvers::IncidentManagement::TimelineEventTagsResolver + field :created_at, Types::TimeType, null: false, diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index e4b705245ed..efaf6c9c816 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -47,7 +47,7 @@ module Types mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update mount_mutation Mutations::DependencyProxy::GroupSettings::Update mount_mutation Mutations::Environments::CanaryIngress::Update - mount_mutation Mutations::IncidentManagement::TimelineEvent::Create + mount_mutation Mutations::IncidentManagement::TimelineEvent::Create, alpha: { milestone: '15.6' } mount_mutation Mutations::IncidentManagement::TimelineEvent::PromoteFromNote mount_mutation Mutations::IncidentManagement::TimelineEvent::Update mount_mutation Mutations::IncidentManagement::TimelineEvent::Destroy diff --git a/app/models/incident_management/timeline_event_tag.rb b/app/models/incident_management/timeline_event_tag.rb index ad1ec04e1e7..2064ccc8c5e 100644 --- a/app/models/incident_management/timeline_event_tag.rb +++ b/app/models/incident_management/timeline_event_tag.rb @@ -20,6 +20,8 @@ module IncidentManagement validates :name, uniqueness: { scope: :project_id } validates :name, length: { maximum: 255 } + scope :by_names, -> (tag_names) { where(name: tag_names) } + def self.pluck_names pluck(:name) end diff --git a/app/policies/incident_management/timeline_event_tag_policy.rb b/app/policies/incident_management/timeline_event_tag_policy.rb new file mode 100644 index 00000000000..e2268d917b4 --- /dev/null +++ b/app/policies/incident_management/timeline_event_tag_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module IncidentManagement + class TimelineEventTagPolicy < ::BasePolicy + delegate { @subject.project } + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index c71b26987a0..b0818d1de6c 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -254,7 +254,6 @@ class ProjectPolicy < BasePolicy enable :change_namespace enable :change_visibility_level - enable :rename_project enable :remove_project enable :archive_project enable :remove_fork_project @@ -497,6 +496,7 @@ class ProjectPolicy < BasePolicy enable :push_to_delete_protected_branch enable :update_snippet enable :admin_snippet + enable :rename_project enable :admin_project_member enable :admin_note enable :admin_wiki @@ -846,6 +846,10 @@ class ProjectPolicy < BasePolicy enable :view_package_registry_project_settings end + rule { can?(:read_project) }.policy do + enable :read_incident_management_timeline_event_tag + end + private def user_is_user? diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb index 32b9d3eab7b..c625b0d15bd 100644 --- a/app/services/incident_management/timeline_events/create_service.rb +++ b/app/services/incident_management/timeline_events/create_service.rb @@ -99,6 +99,9 @@ module IncidentManagement if timeline_event.save(context: validation_context) add_system_note(timeline_event) + + create_timeline_event_tag_links(timeline_event, params[:timeline_event_tag_names]) + track_usage_event(:incident_management_timeline_event_created, user.id) success(timeline_event) @@ -126,6 +129,22 @@ module IncidentManagement def validation_context :user_input if !auto_created && params[:promoted_from_note].blank? end + + def create_timeline_event_tag_links(timeline_event, tag_names) + return unless params[:timeline_event_tag_names] + + tags = project.incident_management_timeline_event_tags.by_names(tag_names) + + tag_links = tags.select(:id).map do |tag| + { + timeline_event_id: timeline_event.id, + timeline_event_tag_id: tag.id, + created_at: DateTime.current + } + end + + IncidentManagement::TimelineEventTagLink.insert_all(tag_links) if tag_links.any? + end end end end diff --git a/config/feature_flags/development/allow_audit_event_type_filtering.yml b/config/feature_flags/development/allow_audit_event_type_filtering.yml new file mode 100644 index 00000000000..e5cbd2fddcf --- /dev/null +++ b/config/feature_flags/development/allow_audit_event_type_filtering.yml @@ -0,0 +1,8 @@ +--- +name: allow_audit_event_type_filtering +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102502 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373833 +milestone: '15.6' +type: development +group: group::compliance +default_enabled: false diff --git a/db/migrate/20221018092552_add_file_name_index_to_packages_rpm_repository_files.rb b/db/migrate/20221018092552_add_file_name_index_to_packages_rpm_repository_files.rb new file mode 100644 index 00000000000..fcec3a6800d --- /dev/null +++ b/db/migrate/20221018092552_add_file_name_index_to_packages_rpm_repository_files.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddFileNameIndexToPackagesRpmRepositoryFiles < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + NEW_INDEX_NAME = 'index_packages_rpm_repository_files_on_project_id_and_file_name' + OLD_INDEX_NAME = 'index_packages_rpm_repository_files_on_project_id' + + def up + add_concurrent_index :packages_rpm_repository_files, %i[project_id file_name], name: NEW_INDEX_NAME + remove_concurrent_index :packages_rpm_repository_files, :project_id, name: OLD_INDEX_NAME + end + + def down + add_concurrent_index :packages_rpm_repository_files, :project_id, name: OLD_INDEX_NAME + remove_concurrent_index :packages_rpm_repository_files, %i[project_id file_name], name: NEW_INDEX_NAME + end +end diff --git a/db/schema_migrations/20221018092552 b/db/schema_migrations/20221018092552 new file mode 100644 index 00000000000..8416f7d72a3 --- /dev/null +++ b/db/schema_migrations/20221018092552 @@ -0,0 +1 @@ +d7ec9ab32c5f58805bec64bea9bd32aedbd80f678d6b8e8c6914aa26523dcc95 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 691a2e29922..c84c6ebc749 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29943,7 +29943,7 @@ CREATE INDEX index_packages_project_id_name_partial_for_nuget ON packages_packag CREATE INDEX index_packages_rpm_metadata_on_package_id ON packages_rpm_metadata USING btree (package_id); -CREATE INDEX index_packages_rpm_repository_files_on_project_id ON packages_rpm_repository_files USING btree (project_id); +CREATE INDEX index_packages_rpm_repository_files_on_project_id_and_file_name ON packages_rpm_repository_files USING btree (project_id, file_name); CREATE INDEX index_packages_tags_on_package_id ON packages_tags USING btree (package_id); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index efbddb2e774..a750e5e7b11 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4882,6 +4882,10 @@ Input type: `TerraformStateUnlockInput` ### `Mutation.timelineEventCreate` +WARNING: +**Introduced** in 15.6. +This feature is in Alpha. It can be changed or removed at any time. + Input type: `TimelineEventCreateInput` #### Arguments @@ -4892,6 +4896,7 @@ Input type: `TimelineEventCreateInput` | `incidentId` | [`IssueID!`](#issueid) | Incident ID of the timeline event. | | `note` | [`String!`](#string) | Text note of the timeline event. | | `occurredAt` | [`Time!`](#time) | Timestamp of when the event occurred. | +| `timelineEventTagNames` | [`[String!]`](#string) | Tags for the incident timeline event. | #### Fields @@ -9320,6 +9325,29 @@ The edge type for [`TimeTrackingTimelogCategory`](#timetrackingtimelogcategory). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`TimeTrackingTimelogCategory`](#timetrackingtimelogcategory) | The item at the end of the edge. | +#### `TimelineEventTagTypeConnection` + +The connection type for [`TimelineEventTagType`](#timelineeventtagtype). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[TimelineEventTagTypeEdge]`](#timelineeventtagtypeedge) | A list of edges. | +| `nodes` | [`[TimelineEventTagType]`](#timelineeventtagtype) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `TimelineEventTagTypeEdge` + +The edge type for [`TimelineEventTagType`](#timelineeventtagtype). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`TimelineEventTagType`](#timelineeventtagtype) | The item at the end of the edge. | + #### `TimelineEventTypeConnection` The connection type for [`TimelineEventType`](#timelineeventtype). @@ -18924,6 +18952,17 @@ Explains why we could not generate a timebox report. | `code` | [`TimeboxReportErrorReason`](#timeboxreporterrorreason) | Machine readable code, categorizing the error. | | `message` | [`String`](#string) | Human readable message explaining what happened. | +### `TimelineEventTagType` + +Describes a tag on an incident management timeline event. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `id` | [`IncidentManagementTimelineEventTagID!`](#incidentmanagementtimelineeventtagid) | ID of the timeline event tag. | +| `name` | [`String!`](#string) | Name of the timeline event tag. | + ### `TimelineEventType` Describes an incident management timeline event. @@ -18942,6 +18981,7 @@ Describes an incident management timeline event. | `noteHtml` | [`String`](#string) | HTML note of the timeline event. | | `occurredAt` | [`Time!`](#time) | Timestamp when the event occurred. | | `promotedFromNote` | [`Note`](#note) | Note from which the timeline event was created. | +| `timelineEventTags` | [`TimelineEventTagTypeConnection`](#timelineeventtagtypeconnection) | Tags for the incident timeline event. (see [Connections](#connections)) | | `updatedAt` | [`Time!`](#time) | Timestamp when the event updated. | | `updatedByUser` | [`UserCore`](#usercore) | User that updated the timeline event. | @@ -22533,6 +22573,12 @@ A `IncidentManagementTimelineEventID` is a global ID. It is encoded as a string. An example `IncidentManagementTimelineEventID` is: `"gid://gitlab/IncidentManagement::TimelineEvent/1"`. +### `IncidentManagementTimelineEventTagID` + +A `IncidentManagementTimelineEventTagID` is a global ID. It is encoded as a string. + +An example `IncidentManagementTimelineEventTagID` is: `"gid://gitlab/IncidentManagement::TimelineEventTag/1"`. + ### `Int` Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. diff --git a/doc/api/group_iterations.md b/doc/api/group_iterations.md index 92333de701c..988986d8965 100644 --- a/doc/api/group_iterations.md +++ b/doc/api/group_iterations.md @@ -20,8 +20,8 @@ Returns a list of group iterations. GET /groups/:id/iterations GET /groups/:id/iterations?state=opened GET /groups/:id/iterations?state=closed -GET /groups/:id/iterations?title=1.0 GET /groups/:id/iterations?search=version +GET /groups/:id/iterations?include_ancestors=false ``` | Attribute | Type | Required | Description | diff --git a/doc/api/iterations.md b/doc/api/iterations.md index 5704bcd3418..4997a917a5a 100644 --- a/doc/api/iterations.md +++ b/doc/api/iterations.md @@ -22,8 +22,8 @@ Returns a list of project iterations. GET /projects/:id/iterations GET /projects/:id/iterations?state=opened GET /projects/:id/iterations?state=closed -GET /projects/:id/iterations?title=1.0 GET /projects/:id/iterations?search=version +GET /projects/:id/iterations?include_ancestors=false ``` | Attribute | Type | Required | Description | diff --git a/doc/development/gitlab_flavored_markdown/specification_guide/index.md b/doc/development/gitlab_flavored_markdown/specification_guide/index.md index e64c145b3a3..17afebcf6ee 100644 --- a/doc/development/gitlab_flavored_markdown/specification_guide/index.md +++ b/doc/development/gitlab_flavored_markdown/specification_guide/index.md @@ -1144,7 +1144,7 @@ The `output_example_snapshots` directory contains files which are generated by t `glfm_specification/input` directory. The `output-specification.rb` script generates -`output_snapshot_examples/glfm_snapshot_spec.md` and `output_snapshot_examples/glfm_snapshot_spec.html`. +`output_snapshot_examples/snapshot_spec.md` and `output_snapshot_examples/snapshot_spec.html`. These files are Markdown specification files containing examples generated based on input files, similar to the `output_spec/spec.txt` and `output_spec/spec.html`, with the following differences: @@ -1166,9 +1166,9 @@ key in `glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.ym can be used to disable automatic generation of some examples. They can instead be manually edited as necessary to help drive the implementations. -##### `glfm_snapshot_spec.md` +##### `snapshot_spec.md` -[`glfm_specification/output_example_snapshots/glfm_snapshot_spec.md`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/output_example_snapshots/glfm_snapshot_spec.md) +[`glfm_specification/output_example_snapshots/snapshot_spec.md`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/output_example_snapshots/snapshot_spec.md) is a Markdown file, containing standard Markdown + canonical HTML examples like [`spec.txt`](#spectxt). It is generated or updated by the `update-specification.rb` script, using the @@ -1180,26 +1180,26 @@ scripts such as `update-example-snapshots.rb`. It is similar to [`spec.txt`](#spectxt), with the following differences: 1. [`spec.txt`](#spectxt) contains only examples for GitLab Flavored Markdown, but - `glfm_snapshot_spec.md` also contains the full superset of examples from the + `snapshot_spec.md` also contains the full superset of examples from the "GitHub Flavored Markdown" (GFM)[specification](https://github.github.com/gfm/) and the [CommonMark specification](https://spec.commonmark.org/0.30/) specifications. 1. [`spec.txt`](#spectxt) represents the full GLFM specification, including additional header sections - containing only explanatory prose and no examples, but `glfm_snapshot_spec.md` consists of only + containing only explanatory prose and no examples, but `snapshot_spec.md` consists of only header sections which contain examples. This is because its purpose is to serve as input for the other [`output example snapshot files`](#output-example-snapshot-files) - it is not intended to serve as an actual [specification file](#output-specification-files) like [`spec.txt`](#spectxt) or [`spec.html`](#spechtml). -##### `glfm_snapshot_spec.html` +##### `snapshot_spec.html` -[`glfm_specification/output_snapshot_examples/glfm_snapshot_spec.html`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/output_snapshot_examples/glfm_snapshot_spec.html) -is an HTML file, rendered based on `glfm_snapshot_spec.md`. It is +[`glfm_specification/output_snapshot_examples/snapshot_spec.html`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/output_snapshot_examples/snapshot_spec.html) +is an HTML file, rendered based on `snapshot_spec.md`. It is generated (or updated) by the `update-specification.rb` script at the same time as -`glfm_snapshot_spec.md`. +`snapshot_spec.md`. NOTE: The formatting of this HTML is currently not identical to the GFM and CommonMark -HTML-rendered specification. It is only the raw output of running `glfm_snapshot_spec.md` through +HTML-rendered specification. It is only the raw output of running `snapshot_spec.md` through the GitLab Markdown renderer. Properly formatting the HTML will require duplicating or reusing the Lua script and template from the CommonMark project: [CommonMark Makefile](https://github.com/commonmark/commonmark-spec/blob/master/Makefile#L11) diff --git a/lib/api/rpm_project_packages.rb b/lib/api/rpm_project_packages.rb index 1eb7361d356..13bbdfc0e6c 100644 --- a/lib/api/rpm_project_packages.rb +++ b/lib/api/rpm_project_packages.rb @@ -30,7 +30,14 @@ module API requires :file_name, type: String, desc: 'Repository metadata file name' end get 'repodata/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do - not_found! + authorize_read_package!(authorized_user_project) + + repository_file = Packages::Rpm::RepositoryFile.find_by_project_id_and_file_name!( + authorized_user_project.id, + "#{params['file_name']}.#{params['format']}" + ) + + present_carrierwave_file!(repository_file.file) end desc 'Download RPM package files' diff --git a/spec/factories/packages/rpm/rpm_repository_files.rb b/spec/factories/packages/rpm/rpm_repository_files.rb index 079d32b3995..00755f49d98 100644 --- a/spec/factories/packages/rpm/rpm_repository_files.rb +++ b/spec/factories/packages/rpm/rpm_repository_files.rb @@ -4,9 +4,10 @@ FactoryBot.define do factory :rpm_repository_file, class: 'Packages::Rpm::RepositoryFile' do project - file_name { 'repomd.xml' } + file_name { '364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml' } file_sha1 { 'efae869b4e95d54796a46481f3a211d6a88d0323' } file_md5 { 'ddf8a75330c896a8d7709e75f8b5982a' } + file_sha256 { '364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3' } size { 3127.kilobytes } status { :default } @@ -15,7 +16,11 @@ FactoryBot.define do end transient do - file_fixture { 'spec/fixtures/packages/rpm/repodata/repomd.xml' } + file_fixture do + # rubocop:disable Layout/LineLength + 'spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml' + # rubocop:enable Layout/LineLength + end end after(:build) do |package_file, evaluator| diff --git a/spec/finders/incident_management/timeline_event_tags_finder_spec.rb b/spec/finders/incident_management/timeline_event_tags_finder_spec.rb new file mode 100644 index 00000000000..5bdb356ff62 --- /dev/null +++ b/spec/finders/incident_management/timeline_event_tags_finder_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IncidentManagement::TimelineEventTagsFinder do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:incident) { create(:incident, project: project) } + let_it_be(:timeline_event) do + create(:incident_management_timeline_event, project: project, incident: incident, occurred_at: Time.current) + end + + let_it_be(:timeline_event_tag) do + create(:incident_management_timeline_event_tag, project: project) + end + + let_it_be(:timeline_event_tag_link) do + create(:incident_management_timeline_event_tag_link, + timeline_event: timeline_event, + timeline_event_tag: timeline_event_tag) + end + + let(:params) { {} } + + describe '#execute' do + subject(:execute) { described_class.new(user, timeline_event, params).execute } + + context 'when user has permissions' do + before do + project.add_guest(user) + end + + it 'returns tags on the event' do + is_expected.to match_array([timeline_event_tag]) + end + + context 'when event does not have tags' do + let(:timeline_event) do + create(:incident_management_timeline_event, project: project, incident: incident, occurred_at: Time.current) + end + + it 'returns empty result' do + is_expected.to match_array([]) + end + end + + context 'when timeline event is nil' do + let(:timeline_event) { nil } + + it { is_expected.to eq(IncidentManagement::TimelineEventTag.none) } + end + end + + context 'when user does not have permissions' do + it { is_expected.to eq(IncidentManagement::TimelineEventTag.none) } + end + end +end diff --git a/spec/fixtures/packages/rpm/repodata/repomd.xml b/spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml similarity index 82% rename from spec/fixtures/packages/rpm/repodata/repomd.xml rename to spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml index 4554ee9a6d0..177a9be4723 100644 --- a/spec/fixtures/packages/rpm/repodata/repomd.xml +++ b/spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml @@ -1,4 +1,7 @@ - + 1644602779 6503673de76312406ff8ecb06d9733c32b546a65abae4d4170d9b51fb75bf253 diff --git a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb index 9254d84b29c..61fb0d9458b 100644 --- a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -6,6 +6,9 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:incident) { create(:incident, project: project) } + let_it_be(:timeline_event_tag) do + create(:incident_management_timeline_event_tag, project: project) + end let(:args) { { note: 'note', occurred_at: Time.current } } @@ -39,6 +42,18 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do it_behaves_like 'responding with an incident timeline errors', errors: ["Occurred at can't be blank and Timeline text can't be blank"] end + + context 'when timeline event tags are passed' do + let(:args) do + { + note: 'note', + occurred_at: Time.current, + timeline_event_tag_names: [timeline_event_tag.name.to_s] + } + end + + it_behaves_like 'creating an incident timeline event' + end end it_behaves_like 'failing to create an incident timeline event' diff --git a/spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb b/spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb new file mode 100644 index 00000000000..8ab34e05e52 --- /dev/null +++ b/spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::IncidentManagement::TimelineEventTagsResolver do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:incident) { create(:incident, project: project) } + + let_it_be(:timeline_event) do + create(:incident_management_timeline_event, project: project, incident: incident) + end + + let_it_be(:timeline_event_with_no_tags) do + create(:incident_management_timeline_event, project: project, incident: incident) + end + + let_it_be(:timeline_event_tag) do + create(:incident_management_timeline_event_tag, project: project) + end + + let_it_be(:timeline_event_tag2) do + create(:incident_management_timeline_event_tag, project: project, name: 'Test tag 2') + end + + let_it_be(:timeline_event_tag_link) do + create(:incident_management_timeline_event_tag_link, + timeline_event: timeline_event, + timeline_event_tag: timeline_event_tag) + end + + let(:resolver) { described_class } + + subject(:resolved_timeline_event_tags) do + sync(resolve_timeline_event_tags(timeline_event, current_user: current_user).to_a) + end + + before do + project.add_guest(current_user) + end + + specify do + expect(resolver).to have_nullable_graphql_type( + Types::IncidentManagement::TimelineEventTagType.connection_type + ) + end + + it 'returns timeline event tags', :aggregate_failures do + expect(resolved_timeline_event_tags.length).to eq(1) + expect(resolved_timeline_event_tags.first).to be_a(::IncidentManagement::TimelineEventTag) + end + + context 'when timeline event is nil' do + subject(:resolved_timeline_event_tags) do + sync(resolve_timeline_event_tags(nil, current_user: current_user).to_a) + end + + it 'returns no timeline event tags' do + expect(resolved_timeline_event_tags).to be_empty + end + end + + context 'when there is no timeline event tag link' do + subject(:resolved_timeline_event_tags) do + sync(resolve_timeline_event_tags(timeline_event_with_no_tags, current_user: current_user).to_a) + end + + it 'returns no timeline event tags' do + expect(resolved_timeline_event_tags).to be_empty + end + end + + context 'when user does not have permissions' do + let(:non_member) { create(:user) } + + subject(:resolved_timeline_event_tags) do + sync(resolve_timeline_event_tags(timeline_event, current_user: non_member).to_a) + end + + it 'returns no timeline event tags' do + expect(resolved_timeline_event_tags).to be_empty + end + end + + private + + def resolve_timeline_event_tags(obj, context = { current_user: current_user }) + resolve(resolver, obj: obj, args: {}, ctx: context, arg_style: :internal_prepared) + end +end diff --git a/spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb b/spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb new file mode 100644 index 00000000000..831a598ab66 --- /dev/null +++ b/spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['TimelineEventTagType'] do + specify { expect(described_class.graphql_name).to eq('TimelineEventTagType') } + + specify { expect(described_class).to require_graphql_authorizations(:read_incident_management_timeline_event_tag) } + + it 'exposes the expected fields' do + expected_fields = %i[ + id + name + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/incident_management/timeline_event_type_spec.rb b/spec/graphql/types/incident_management/timeline_event_type_spec.rb index 5a6bc461f20..6805e0cdc9b 100644 --- a/spec/graphql/types/incident_management/timeline_event_type_spec.rb +++ b/spec/graphql/types/incident_management/timeline_event_type_spec.rb @@ -21,6 +21,7 @@ RSpec.describe GitlabSchema.types['TimelineEventType'] do occurred_at created_at updated_at + timeline_event_tags ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/models/incident_management/timeline_event_tag_spec.rb b/spec/models/incident_management/timeline_event_tag_spec.rb index 9dbb78a6d82..66cc885d8b8 100644 --- a/spec/models/incident_management/timeline_event_tag_spec.rb +++ b/spec/models/incident_management/timeline_event_tag_spec.rb @@ -39,4 +39,21 @@ RSpec.describe IncidentManagement::TimelineEventTag do it { expect(described_class::START_TIME_TAG_NAME).to eq('Start time') } it { expect(described_class::END_TIME_TAG_NAME).to eq('End time') } end + + describe '#by_names scope' do + let_it_be(:project) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:tag1) { create(:incident_management_timeline_event_tag, name: 'Test tag 1', project: project) } + let_it_be(:tag2) { create(:incident_management_timeline_event_tag, name: 'Test tag 2', project: project) } + let_it_be(:tag3) { create(:incident_management_timeline_event_tag, name: 'Test tag 3', project: project2) } + + it 'returns two matching tags' do + expect(described_class.by_names(['Test tag 1', 'Test tag 2'])).to contain_exactly(tag1, tag2) + end + + it 'returns tags on the project' do + expect(project2.incident_management_timeline_event_tags.by_names(['Test tag 1', + 'Test tag 3'])).to contain_exactly(tag3) + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 1024d4f0b4a..0ee9c24ee9b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -533,6 +533,24 @@ RSpec.describe ProjectPolicy do end end + context 'with timeline event tags' do + context 'when user is member of the project' do + it 'allows access to timeline event tags' do + expect(described_class.new(owner, project)).to be_allowed(:read_incident_management_timeline_event_tag) + expect(described_class.new(developer, project)).to be_allowed(:read_incident_management_timeline_event_tag) + expect(described_class.new(admin, project)).to be_allowed(:read_incident_management_timeline_event_tag) + end + end + + context 'when user is not a member of the project' do + let(:project) { private_project } + + it 'disallows access to the timeline event tags' do + expect(described_class.new(non_member, project)).to be_disallowed(:read_incident_management_timeline_event_tag) + end + end + end + context 'reading a project' do it 'allows access when a user has read access to the repo' do expect(described_class.new(owner, project)).to be_allowed(:read_project) 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 bac78149cf9..fc3b666dd3d 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 @@ -10,8 +10,16 @@ RSpec.describe 'Creating an incident timeline event' do let_it_be(:incident) { create(:incident, project: project) } let_it_be(:event_occurred_at) { Time.current } let_it_be(:note) { 'demo note' } + let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') } + let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') } + + let(:input) do + { incident_id: incident.to_global_id.to_s, + note: note, + occurred_at: event_occurred_at, + timeline_event_tag_names: [tag1.name] } + end - let(:input) { { incident_id: incident.to_global_id.to_s, note: note, occurred_at: event_occurred_at } } let(:mutation) do graphql_mutation(:timeline_event_create, input) do <<~QL @@ -22,6 +30,7 @@ RSpec.describe 'Creating an incident timeline event' do author { id username } incident { id title } note + timelineEventTags { nodes { name } } editable action occurredAt @@ -64,4 +73,18 @@ RSpec.describe 'Creating an incident timeline event' do it_behaves_like 'timeline event mutation responds with validation error', error_message: 'Timeline text is too long (maximum is 280 characters)' end + + context 'when timeline event tags are passed' do + it 'creates incident timeline event with tags', :aggregate_failures do + post_graphql_mutation(mutation, current_user: user) + + timeline_event_response = mutation_response['timelineEvent'] + tag_names = timeline_event_response['timelineEventTags']['nodes'] + + expect(response).to have_gitlab_http_status(:success) + expect(timeline_event_response).to include( + 'timelineEventTags' => { 'nodes' => tag_names } + ) + end + end end diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb index bcbb1f11d43..544d2d7bd95 100644 --- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb +++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb @@ -48,6 +48,7 @@ RSpec.describe 'getting incident timeline events' do note noteHtml promotedFromNote { id body } + timelineEventTags { nodes { name } } editable action occurredAt @@ -100,6 +101,7 @@ RSpec.describe 'getting incident timeline events' do 'id' => promoted_from_note.to_global_id.to_s, 'body' => promoted_from_note.note }, + 'timelineEventTags' => { 'nodes' => [] }, 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, @@ -108,6 +110,47 @@ RSpec.describe 'getting incident timeline events' do ) end + context 'when timelineEvent tags are linked' do + let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') } + let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') } + let_it_be(:timeline_event_tag_link) do + create(:incident_management_timeline_event_tag_link, + timeline_event: timeline_event, + timeline_event_tag: tag1) + end + + it_behaves_like 'a working graphql query' + + it 'returns the set tags' do + expect(timeline_events.first['timelineEventTags']['nodes'].first['name']).to eq(tag1.name) + end + + context 'when different timeline events are loaded' do + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: current_user) + end + + new_event = create(:incident_management_timeline_event, + incident: incident, + project: project, + updated_by_user: updated_by_user, + promoted_from_note: promoted_from_note, + note: "Referencing #{issue.to_reference(full: true)} - Full URL #{issue_url}" + ) + + create(:incident_management_timeline_event_tag_link, + timeline_event: new_event, + timeline_event_tag: tag2 + ) + + expect(incident.incident_management_timeline_events.length).to eq(3) + expect(post_graphql(query, current_user: current_user)).not_to exceed_query_limit(control) + expect(timeline_events.count).to eq(3) + end + end + end + context 'when filtering by id' do let(:params) { { incident_id: incident.to_global_id.to_s, id: timeline_event.to_global_id.to_s } } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a6b2ef5cd86..746fe83e1a4 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -3426,18 +3426,6 @@ RSpec.describe API::Projects do end context 'when authenticated as project owner' do - it 'updates name' do - project_param = { name: 'bar' } - - put api("/projects/#{project.id}", user), params: project_param - - expect(response).to have_gitlab_http_status(:ok) - - project_param.each_pair do |k, v| - expect(json_response[k.to_s]).to eq(v) - end - end - it 'updates visibility_level' do project_param = { visibility: 'public' } @@ -3795,10 +3783,16 @@ RSpec.describe API::Projects do expect(json_response['message']['path']).to eq(['has already been taken']) end - it 'does not update name' do + it 'updates name' do project_param = { name: 'bar' } - put api("/projects/#{project3.id}", user4), params: project_param - expect(response).to have_gitlab_http_status(:forbidden) + + put api("/projects/#{project.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(:ok) + + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end end it 'does not update visibility_level' do diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb index dd029a3ccf0..68511795c94 100644 --- a/spec/requests/api/rpm_project_packages_spec.rb +++ b/spec/requests/api/rpm_project_packages_spec.rb @@ -37,7 +37,7 @@ RSpec.describe API::RpmProjectPackages do it_behaves_like 'returning response status', status end - shared_examples 'a deploy token for RPM requests' do + shared_examples 'a deploy token for RPM requests' do |success_status = :not_found| context 'with deploy token headers' do before do project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) @@ -46,7 +46,7 @@ RSpec.describe API::RpmProjectPackages do let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } context 'when token is valid' do - it_behaves_like 'returning response status', :not_found + it_behaves_like 'returning response status', success_status end context 'when token is invalid' do @@ -57,7 +57,7 @@ RSpec.describe API::RpmProjectPackages do end end - shared_examples 'a job token for RPM requests' do + shared_examples 'a job token for RPM requests' do |success_status = :not_found| context 'with job token headers' do let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) } @@ -67,7 +67,7 @@ RSpec.describe API::RpmProjectPackages do end context 'with valid token' do - it_behaves_like 'returning response status', :not_found + it_behaves_like 'returning response status', success_status end context 'with invalid token' do @@ -84,10 +84,10 @@ RSpec.describe API::RpmProjectPackages do end end - shared_examples 'a user token for RPM requests' do + shared_examples 'a user token for RPM requests' do |success_status = :not_found| context 'with valid project' do where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | success_status 'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized @@ -96,7 +96,7 @@ RSpec.describe API::RpmProjectPackages do 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized 'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found + 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | success_status 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized @@ -124,13 +124,15 @@ RSpec.describe API::RpmProjectPackages do end describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do - let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" } + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } } + let(:repository_file) { create(:rpm_repository_file, project: project) } + let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{repository_file.file_name}" } subject { get api(url), headers: headers } - it_behaves_like 'a job token for RPM requests' - it_behaves_like 'a deploy token for RPM requests' - it_behaves_like 'a user token for RPM requests' + it_behaves_like 'a job token for RPM requests', :success + it_behaves_like 'a deploy token for RPM requests', :success + it_behaves_like 'a user token for RPM requests', :success end describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do 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 6ab02ad0b0b..4e9cc4fa09c 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -8,6 +8,9 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do let_it_be(:project) { create(:project) } let_it_be_with_refind(:incident) { create(:incident, project: project) } let_it_be(:comment) { create(:note, project: project, noteable: incident) } + let_it_be(:timeline_event_tag) do + create(:incident_management_timeline_event_tag, name: 'Test tag 1', project: project) + end let(:args) do { @@ -134,6 +137,25 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do end end + context 'when timeline event tag names are passed' do + let(:args) do + { + note: 'note', + occurred_at: Time.current, + action: 'new comment', + promoted_from_note: comment, + timeline_event_tag_names: ['Test tag 1'] + } + end + + it_behaves_like 'success response' + + it 'matches the tag name' do + result = execute.payload[:timeline_event] + expect(result.timeline_event_tags.first).to eq(timeline_event_tag) + end + end + context 'with editable param' do let(:args) do { diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 9f99a66e2e1..6e2caa853f8 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -68,7 +68,7 @@ RSpec.shared_context 'ProjectPolicy context' do admin_project admin_project_member admin_snippet admin_terraform_state admin_wiki create_deploy_token destroy_deploy_token push_to_delete_protected_branch read_deploy_token update_snippet - destroy_upload admin_member_access_request + destroy_upload admin_member_access_request rename_project ] end @@ -83,7 +83,7 @@ RSpec.shared_context 'ProjectPolicy context' do let(:base_owner_permissions) do %i[ archive_project change_namespace change_visibility_level destroy_issue - destroy_merge_request manage_owners remove_fork_project remove_project rename_project + destroy_merge_request manage_owners remove_fork_project remove_project set_issue_created_at set_issue_iid set_issue_updated_at set_note_created_at ]