From 2977cf67ec27f8ab014bfee852d0ae7b56585242 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 27 Jul 2022 15:09:42 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/rspec/context_wording.yml | 1 - .../pages/projects/tags/releases/index.js | 6 - .../vue_shared/components/markdown/header.vue | 7 +- .../projects/tags/releases_controller.rb | 39 ------- .../mutations/work_items/update_arguments.rb | 3 + app/graphql/mutations/timelogs/base.rb | 18 +++ app/graphql/mutations/timelogs/create.rb | 48 ++++++++ app/graphql/mutations/timelogs/delete.rb | 13 +-- app/graphql/mutations/work_items/create.rb | 3 + app/graphql/types/mutation_type.rb | 1 + app/policies/issuable_policy.rb | 4 + app/services/releases/create_service.rb | 4 - app/services/system_note_service.rb | 18 +++ .../system_notes/time_tracking_service.rb | 26 +++++ app/services/timelogs/base_service.rb | 21 +++- app/services/timelogs/create_service.rb | 45 ++++++++ app/services/timelogs/delete_service.rb | 16 ++- .../tags/_edit_release_button.html.haml | 18 ++- .../projects/tags/releases/edit.html.haml | 19 ---- .../shared/blob/_markdown_buttons.html.haml | 2 +- ...dit_tag_release_notes_via_release_page.yml | 8 -- config/routes/repository.rb | 5 +- .../14-7-deprecate-artifacts-keyword.yml | 2 +- .../15_0/15-0-removal-artifacts-keyword.yml | 2 +- .../postgresql_versions.md | 4 +- doc/api/graphql/reference/index.md | 27 ++++- doc/update/deprecations.md | 6 +- doc/update/removals.md | 4 + locale/gitlab.pot | 15 ++- .../projects/tags/releases_controller_spec.rb | 103 ------------------ .../projects/tags/user_edits_tags_spec.rb | 74 ++----------- .../tags/developer_updates_tag_spec.rb | 56 ---------- .../components/markdown/header_spec.js | 8 +- spec/policies/issuable_policy_spec.rb | 40 +++++++ .../graphql/mutations/timelogs/create_spec.rb | 48 ++++++++ .../mutations/work_items/create_spec.rb | 2 + .../mutations/work_items/update_spec.rb | 91 ++++++++++++---- spec/services/releases/create_service_spec.rb | 8 -- spec/services/system_note_service_spec.rb | 13 +++ .../time_tracking_service_spec.rb | 24 ++++ spec/services/timelogs/create_service_spec.rb | 47 ++++++++ spec/services/timelogs/delete_service_spec.rb | 10 +- .../timelogs/create_shared_examples.rb | 97 +++++++++++++++++ .../create_service_shared_examples.rb | 89 +++++++++++++++ 44 files changed, 712 insertions(+), 383 deletions(-) delete mode 100644 app/assets/javascripts/pages/projects/tags/releases/index.js delete mode 100644 app/controllers/projects/tags/releases_controller.rb create mode 100644 app/graphql/mutations/timelogs/base.rb create mode 100644 app/graphql/mutations/timelogs/create.rb create mode 100644 app/services/timelogs/create_service.rb delete mode 100644 app/views/projects/tags/releases/edit.html.haml delete mode 100644 config/feature_flags/development/edit_tag_release_notes_via_release_page.yml delete mode 100644 spec/controllers/projects/tags/releases_controller_spec.rb delete mode 100644 spec/features/tags/developer_updates_tag_spec.rb create mode 100644 spec/requests/api/graphql/mutations/timelogs/create_spec.rb create mode 100644 spec/services/timelogs/create_service_spec.rb create mode 100644 spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb create mode 100644 spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 797f03573d5..5a889b5866d 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -1507,7 +1507,6 @@ RSpec/ContextWording: - 'spec/features/snippets/explore_spec.rb' - 'spec/features/tags/developer_creates_tag_spec.rb' - 'spec/features/tags/developer_deletes_tag_spec.rb' - - 'spec/features/tags/developer_updates_tag_spec.rb' - 'spec/features/tags/maintainer_deletes_protected_tag_spec.rb' - 'spec/features/uploads/user_uploads_file_to_note_spec.rb' - 'spec/features/user_can_display_performance_bar_spec.rb' diff --git a/app/assets/javascripts/pages/projects/tags/releases/index.js b/app/assets/javascripts/pages/projects/tags/releases/index.js deleted file mode 100644 index cafd880b4be..00000000000 --- a/app/assets/javascripts/pages/projects/tags/releases/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import $ from 'jquery'; -import GLForm from '~/gl_form'; -import ZenMode from '~/zen_mode'; - -new ZenMode(); // eslint-disable-line no-new -new GLForm($('.release-form')); // eslint-disable-line no-new diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 46cd35aa0b7..480d412f220 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -332,11 +332,12 @@ export default { :button-title="__('Add a table')" icon="table" /> - diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb deleted file mode 100644 index adeadf2133e..00000000000 --- a/app/controllers/projects/tags/releases_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# TODO: remove this file together with FF https://gitlab.com/gitlab-org/gitlab/-/issues/366244 -# also delete view/routes -class Projects::Tags::ReleasesController < Projects::ApplicationController - # Authorize - before_action :require_non_empty_project - before_action :authorize_download_code! - before_action :authorize_push_code! - before_action :tag - before_action :release - - feature_category :release_evidence - urgency :low - - def edit - end - - def update - release.update(release_params) if release.persisted? || release_params[:description].present? - - redirect_to project_tag_path(@project, tag.name) - end - - private - - def tag - @tag ||= @repository.find_tag(params[:tag_id]) - end - - def release - @release ||= Releases::CreateService.new(project, current_user, tag: @tag.name) - .find_or_build_release - end - - def release_params - params.require(:release).permit(:description) - end -end diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb index c50f1ce006e..a2670bd628f 100644 --- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb +++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb @@ -15,6 +15,9 @@ module Mutations argument :title, GraphQL::Types::String, required: false, description: copy_field_description(Types::WorkItemType, :title) + argument :confidential, GraphQL::Types::Boolean, + required: false, + description: 'Sets the work item confidentiality.' argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType, required: false, description: 'Input for description widget.' diff --git a/app/graphql/mutations/timelogs/base.rb b/app/graphql/mutations/timelogs/base.rb new file mode 100644 index 00000000000..9859f0e7d79 --- /dev/null +++ b/app/graphql/mutations/timelogs/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module Timelogs + class Base < Mutations::BaseMutation + field :timelog, + Types::TimelogType, + null: true, + description: 'Timelog.' + + private + + def response(result) + { timelog: result.payload[:timelog], errors: result.errors } + end + end + end +end diff --git a/app/graphql/mutations/timelogs/create.rb b/app/graphql/mutations/timelogs/create.rb new file mode 100644 index 00000000000..bab7508454e --- /dev/null +++ b/app/graphql/mutations/timelogs/create.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Mutations + module Timelogs + class Create < Base + graphql_name 'TimelogCreate' + + argument :time_spent, + GraphQL::Types::String, + required: true, + description: 'Amount of time spent.' + + argument :spent_at, + Types::DateType, + required: true, + description: 'When the time was spent.' + + argument :summary, + GraphQL::Types::String, + required: true, + description: 'Summary of time spent.' + + argument :issuable_id, + ::Types::GlobalIDType[::Issuable], + required: true, + description: 'Global ID of the issuable (Issue, WorkItem or MergeRequest).' + + authorize :create_timelog + + def resolve(issuable_id:, time_spent:, spent_at:, summary:, **args) + issuable = authorized_find!(id: issuable_id) + parsed_time_spent = Gitlab::TimeTrackingFormatter.parse(time_spent) + + result = ::Timelogs::CreateService.new( + issuable, parsed_time_spent, spent_at, summary, current_user + ).execute + + response(result) + end + + private + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: [::Issue, ::WorkItem, ::MergeRequest]).sync + end + end + end +end diff --git a/app/graphql/mutations/timelogs/delete.rb b/app/graphql/mutations/timelogs/delete.rb index 8fd41c27b88..61588d839a7 100644 --- a/app/graphql/mutations/timelogs/delete.rb +++ b/app/graphql/mutations/timelogs/delete.rb @@ -2,14 +2,9 @@ module Mutations module Timelogs - class Delete < Mutations::BaseMutation + class Delete < Base graphql_name 'TimelogDelete' - field :timelog, - Types::TimelogType, - null: true, - description: 'Deleted timelog.' - argument :id, ::Types::GlobalIDType[::Timelog], required: true, @@ -22,11 +17,13 @@ module Mutations result = ::Timelogs::DeleteService.new(timelog, current_user).execute # Return the result payload, not the loaded timelog, so that it returns null in case of unauthorized access - { timelog: result.payload, errors: result.errors } + response(result) end + private + def find_object(id:) - GitlabSchema.find_by_gid(id) + GitlabSchema.object_from_id(id, expected_type: ::Timelog).sync end end end diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index 350153eaf19..ece00e04ed9 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -13,6 +13,9 @@ module Mutations authorize :create_work_item + argument :confidential, GraphQL::Types::Boolean, + required: false, + description: 'Sets the work item confidentiality.' argument :description, GraphQL::Types::String, required: false, description: copy_field_description(Types::WorkItemType, :description) diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 49424cb43ba..3907b096c2c 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -94,6 +94,7 @@ module Types mount_mutation Mutations::Terraform::State::Delete mount_mutation Mutations::Terraform::State::Lock mount_mutation Mutations::Terraform::State::Unlock + mount_mutation Mutations::Timelogs::Create mount_mutation Mutations::Timelogs::Delete mount_mutation Mutations::Todos::Create mount_mutation Mutations::Todos::MarkDone diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index f1efcb25331..3c5e1020c8a 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -44,6 +44,10 @@ class IssuablePolicy < BasePolicy rule { can?(:read_issue) & can?(:developer_access) }.policy do enable :admin_incident_management_timeline_event end + + rule { can?(:reporter_access) }.policy do + enable :create_timelog + end end IssuablePolicy.prepend_mod_with('IssuablePolicy') diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index e3134070231..2588d2187a5 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -19,10 +19,6 @@ module Releases create_release(tag, evidence_pipeline) end - def find_or_build_release - release || build_release(existing_tag) - end - private def ensure_tag diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index fb24ace9fdb..332333d2fc3 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -111,6 +111,24 @@ module SystemNoteService ::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).change_time_spent end + # Called when a timelog is added to an issuable + # + # issuable - Issuable object (Issue, WorkItem or MergeRequest) + # project - Project owning the issuable + # author - User performing the change + # timelog - Created timelog + # + # Example Note text: + # + # "subtracted 1h 15m of time spent" + # + # "added 2h 30m of time spent" + # + # Returns the created Note object + def created_timelog(issuable, project, author, timelog) + ::SystemNotes::TimeTrackingService.new(noteable: issuable, project: project, author: author).created_timelog(timelog) + end + # Called when a timelog is removed from a Noteable # # noteable - Noteable object diff --git a/app/services/system_notes/time_tracking_service.rb b/app/services/system_notes/time_tracking_service.rb index a9b1f6d3d37..f99c7c9c0bb 100644 --- a/app/services/system_notes/time_tracking_service.rb +++ b/app/services/system_notes/time_tracking_service.rb @@ -76,6 +76,32 @@ module SystemNotes create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) end + # Called when a timelog is added to an issuable + # + # timelog - Added timelog + # + # Example Note text: + # + # "subtracted 1h 15m of time spent" + # + # "added 2h 30m of time spent" + # + # Returns the created Note object + def created_timelog(timelog) + time_spent = timelog.time_spent + spent_at = timelog.spent_at&.to_date + parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) + action = time_spent > 0 ? 'added' : 'subtracted' + + text_parts = ["#{action} #{parsed_time} of time spent"] + text_parts << "at #{spent_at}" if spent_at && spent_at != DateTime.current.to_date + body = text_parts.join(' ') + + issue_activity_counter.track_issue_time_spent_changed_action(author: author) if noteable.is_a?(Issue) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) + end + def remove_timelog(timelog) time_spent = timelog.time_spent spent_at = timelog.spent_at&.to_date diff --git a/app/services/timelogs/base_service.rb b/app/services/timelogs/base_service.rb index be46c26e047..e09264864fd 100644 --- a/app/services/timelogs/base_service.rb +++ b/app/services/timelogs/base_service.rb @@ -5,11 +5,26 @@ module Timelogs include BaseServiceUtility include Gitlab::Utils::StrongMemoize - attr_accessor :timelog, :current_user + attr_accessor :current_user - def initialize(timelog, user) - @timelog = timelog + def initialize(user) @current_user = user end + + def success(timelog) + ServiceResponse.success(payload: { + timelog: timelog + }) + end + + def error(message, http_status = nil) + ServiceResponse.error(message: message, http_status: http_status) + end + + def error_in_save(timelog) + return error(_("Failed to save timelog")) if timelog.errors.empty? + + error(timelog.errors.full_messages.to_sentence) + end end end diff --git a/app/services/timelogs/create_service.rb b/app/services/timelogs/create_service.rb new file mode 100644 index 00000000000..12181cec20a --- /dev/null +++ b/app/services/timelogs/create_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Timelogs + class CreateService < Timelogs::BaseService + attr_accessor :issuable, :time_spent, :spent_at, :summary + + def initialize(issuable, time_spent, spent_at, summary, user) + super(user) + + @issuable = issuable + @time_spent = time_spent + @spent_at = spent_at + @summary = summary + end + + def execute + unless can?(current_user, :create_timelog, issuable) + return error( + _("%{issuable_class_name} doesn't exist or you don't have permission to add timelog to it.") % { + issuable_class_name: issuable.nil? ? 'Issuable' : issuable.base_class_name + }, 404) + end + + issue = issuable if issuable.is_a?(Issue) + merge_request = issuable if issuable.is_a?(MergeRequest) + + timelog = Timelog.new( + time_spent: time_spent, + spent_at: spent_at, + summary: summary, + user: current_user, + issue: issue, + merge_request: merge_request, + note: nil + ) + + if !timelog.save + error_in_save(timelog) + else + SystemNoteService.created_timelog(issuable, issuable.project, current_user, timelog) + success(timelog) + end + end + end +end diff --git a/app/services/timelogs/delete_service.rb b/app/services/timelogs/delete_service.rb index 0df888a3706..e72dfd98494 100644 --- a/app/services/timelogs/delete_service.rb +++ b/app/services/timelogs/delete_service.rb @@ -2,11 +2,17 @@ module Timelogs class DeleteService < Timelogs::BaseService + attr_accessor :timelog + + def initialize(timelog, user) + super(user) + + @timelog = timelog + end + def execute unless can?(current_user, :admin_timelog, timelog) - return ServiceResponse.error( - message: "Timelog doesn't exist or you don't have permission to delete it", - http_status: 404) + return error(_("Timelog doesn't exist or you don't have permission to delete it"), 404) end if timelog.destroy @@ -17,9 +23,9 @@ module Timelogs SystemNoteService.remove_timelog(issuable, issuable.project, current_user, timelog) end - ServiceResponse.success(payload: timelog) + success(timelog) else - ServiceResponse.error(message: 'Failed to remove timelog', http_status: 400) + error(_('Failed to remove timelog'), 400) end end end diff --git a/app/views/projects/tags/_edit_release_button.html.haml b/app/views/projects/tags/_edit_release_button.html.haml index 5bdf1c7896c..05e0d0f0fa8 100644 --- a/app/views/projects/tags/_edit_release_button.html.haml +++ b/app/views/projects/tags/_edit_release_button.html.haml @@ -1,11 +1,7 @@ -- if Feature.enabled?(:edit_tag_release_notes_via_release_page, project) - - release_btn_text = s_('TagsPage|Create release') - - release_btn_path = new_project_release_path(project, tag_name: tag.name) - - if release - - release_btn_text = s_('TagsPage|Edit release') - - release_btn_path = edit_project_release_path(project, release) - = link_to release_btn_path, class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: release_btn_text, data: { container: "body" } do - = sprite_icon('pencil', css_class: 'gl-icon') -- else - = link_to edit_project_tag_release_path(project, tag.name), class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do - = sprite_icon('pencil', css_class: 'gl-icon') +- release_btn_text = s_('TagsPage|Create release') +- release_btn_path = new_project_release_path(project, tag_name: tag.name) +- if release + - release_btn_text = s_('TagsPage|Edit release') + - release_btn_path = edit_project_release_path(project, release) += link_to release_btn_path, class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: release_btn_text, data: { container: "body" } do + = sprite_icon('pencil', css_class: 'gl-icon') diff --git a/app/views/projects/tags/releases/edit.html.haml b/app/views/projects/tags/releases/edit.html.haml deleted file mode 100644 index c99f146ea7a..00000000000 --- a/app/views/projects/tags/releases/edit.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -- add_to_breadcrumbs _("Tags"), project_tags_path(@project) -- breadcrumb_title @tag.name -- page_title _("Edit"), @tag.name, _("Tags") - -.sub-header-block.no-bottom-space - .oneline - .title - Release notes for tag - %strong= @tag.name - -= form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), - html: { class: 'common-note-form release-form js-quick-submit' }) do |f| - = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do - = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…" - = render 'shared/notes/hints' - .error-alert - .gl-mt-5.gl-display-flex - = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3' - = link_to _('Cancel'), project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel" diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml index f7f4115bed2..5caa7a05640 100644 --- a/app/views/shared/blob/_markdown_buttons.html.haml +++ b/app/views/shared/blob/_markdown_buttons.html.haml @@ -28,7 +28,7 @@ title: _("Add a collapsible section") }) = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") }) = markdown_toolbar_button({ icon: "paperclip", - data: { "md-tag" => "", "md-prepend" => true, "testid" => "button-attach-file" }, + data: { "testid" => "button-attach-file" }, css_class: 'js-attach-file-button markdown-selector', title: _("Attach a file or image") }) - if show_fullscreen_button diff --git a/config/feature_flags/development/edit_tag_release_notes_via_release_page.yml b/config/feature_flags/development/edit_tag_release_notes_via_release_page.yml deleted file mode 100644 index 1f67eafc06b..00000000000 --- a/config/feature_flags/development/edit_tag_release_notes_via_release_page.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: edit_tag_release_notes_via_release_page -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88832 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366244 -milestone: '15.2' -type: development -group: group::release -default_enabled: false diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 74e72927699..0202eb80b23 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -51,10 +51,7 @@ scope format: false do end delete :merged_branches, controller: 'branches', action: :destroy_all_merged - resources :tags, only: [:index, :show, :new, :create, :destroy] do - resource :release, controller: 'tags/releases', only: [:edit, :update] - end - + resources :tags, only: [:index, :show, :new, :create, :destroy] resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::PathRegex.git_reference_regex } resources :protected_tags, only: [:index, :show, :create, :update, :destroy] end diff --git a/data/deprecations/14-7-deprecate-artifacts-keyword.yml b/data/deprecations/14-7-deprecate-artifacts-keyword.yml index 29b5ec39193..26fd6b485df 100644 --- a/data/deprecations/14-7-deprecate-artifacts-keyword.yml +++ b/data/deprecations/14-7-deprecate-artifacts-keyword.yml @@ -3,7 +3,7 @@ announcement_date: "2022-01-22" removal_milestone: "15.0" removal_date: "2022-05-22" - breaking_change: false + breaking_change: true body: | Currently, test coverage visualizations in GitLab only support Cobertura reports. Starting 15.0, the `artifacts:reports:cobertura` keyword will be replaced by diff --git a/data/removals/15_0/15-0-removal-artifacts-keyword.yml b/data/removals/15_0/15-0-removal-artifacts-keyword.yml index 29edd922eae..15c9a5ee27a 100644 --- a/data/removals/15_0/15-0-removal-artifacts-keyword.yml +++ b/data/removals/15_0/15-0-removal-artifacts-keyword.yml @@ -3,7 +3,7 @@ announcement_date: "2022-02-22" removal_milestone: "15.0" removal_date: "2022-05-22" - breaking_change: false + breaking_change: true body: | As of GitLab 15.0, the [`artifacts:reports:cobertura`](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscobertura-removed) keyword has been [replaced](https://gitlab.com/gitlab-org/gitlab/-/issues/344533) by diff --git a/doc/administration/package_information/postgresql_versions.md b/doc/administration/package_information/postgresql_versions.md index afdc63c4851..e208f9dc935 100644 --- a/doc/administration/package_information/postgresql_versions.md +++ b/doc/administration/package_information/postgresql_versions.md @@ -11,7 +11,9 @@ This table lists only GitLab versions where a significant change happened in the package regarding PostgreSQL versions, not all. Usually, PostgreSQL versions change with major or minor GitLab releases. However, patch versions -of Omnibus GitLab sometimes update the patch level of PostgreSQL. +of Omnibus GitLab sometimes update the patch level of PostgreSQL. We've established a +[yearly cadence for PostgreSQL upgrades](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/database/postgresql-upgrade-cadence.html) +and trigger automatic database upgrades in the release before the new version is required. For example: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index bb0a021e169..c68272582be 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4815,6 +4815,28 @@ Input type: `TimelineEventUpdateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. | +### `Mutation.timelogCreate` + +Input type: `TimelogCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `issuableId` | [`IssuableID!`](#issuableid) | Global ID of the issuable (Issue, WorkItem or MergeRequest). | +| `spentAt` | [`Date!`](#date) | When the time was spent. | +| `summary` | [`String!`](#string) | Summary of time spent. | +| `timeSpent` | [`String!`](#string) | Amount of time spent. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `timelog` | [`Timelog`](#timelog) | Timelog. | + ### `Mutation.timelogDelete` Input type: `TimelogDeleteInput` @@ -4832,7 +4854,7 @@ Input type: `TimelogDeleteInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | -| `timelog` | [`Timelog`](#timelog) | Deleted timelog. | +| `timelog` | [`Timelog`](#timelog) | Timelog. | ### `Mutation.todoCreate` @@ -5587,6 +5609,7 @@ Input type: `WorkItemCreateInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. | | `description` | [`String`](#string) | Description of the work item. | | `hierarchyWidget` | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | Input for hierarchy widget. | | `projectPath` | [`ID!`](#id) | Full path of the project the work item is associated with. | @@ -5695,6 +5718,7 @@ Input type: `WorkItemUpdateInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. | | `descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. | | `hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. | | `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | @@ -22394,6 +22418,7 @@ A time-frame defined as a closed inclusive range of two dates. | Name | Type | Description | | ---- | ---- | ----------- | +| `confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. | | `descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. | | `hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. | | `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index a99ea1a3ef4..39ed17067e3 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -1496,12 +1496,16 @@ Tracing in GitLab is an integration with Jaeger, an open-source end-to-end distr -
+
### `artifacts:reports:cobertura` keyword Planned removal: GitLab 15.0 (2022-05-22) +WARNING: +This is a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Review the details carefully before upgrading. + Currently, test coverage visualizations in GitLab only support Cobertura reports. Starting 15.0, the `artifacts:reports:cobertura` keyword will be replaced by [`artifacts:reports:coverage_report`](https://gitlab.com/gitlab-org/gitlab/-/issues/344533). Cobertura will be the diff --git a/doc/update/removals.md b/doc/update/removals.md index fa5c016d3ab..841b96f59b8 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -621,6 +621,10 @@ The `Managed-Cluster-Applications.gitlab-ci.yml` CI/CD template is being removed ### `artifacts:reports:cobertura` keyword +WARNING: +This is a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Review the details carefully before upgrading. + As of GitLab 15.0, the [`artifacts:reports:cobertura`](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscobertura-removed) keyword has been [replaced](https://gitlab.com/gitlab-org/gitlab/-/issues/344533) by [`artifacts:reports:coverage_report`](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscoverage_report). diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5ed1d6aba94..4ebb4219a24 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -694,6 +694,9 @@ msgstr "" msgid "%{issuableType} will be removed! Are you sure?" msgstr "" +msgid "%{issuable_class_name} doesn't exist or you don't have permission to add timelog to it." +msgstr "" + msgid "%{issuable}(s) already assigned" msgstr "" @@ -15946,6 +15949,9 @@ msgstr "" msgid "Failed to remove the pipeline schedule" msgstr "" +msgid "Failed to remove timelog" +msgstr "" + msgid "Failed to remove user identity." msgstr "" @@ -15970,6 +15976,9 @@ msgstr "" msgid "Failed to save preferences." msgstr "" +msgid "Failed to save timelog" +msgstr "" + msgid "Failed to set due date because the date format is invalid." msgstr "" @@ -38174,9 +38183,6 @@ msgstr "" msgid "TagsPage|Edit release" msgstr "" -msgid "TagsPage|Edit release notes" -msgstr "" - msgid "TagsPage|Existing branch name, tag, or commit SHA" msgstr "" @@ -40403,6 +40409,9 @@ msgstr "" msgid "Timeline|Turn recent updates view on" msgstr "" +msgid "Timelog doesn't exist or you don't have permission to delete it" +msgstr "" + msgid "Timeout" msgstr "" diff --git a/spec/controllers/projects/tags/releases_controller_spec.rb b/spec/controllers/projects/tags/releases_controller_spec.rb deleted file mode 100644 index 1d2385f54f9..00000000000 --- a/spec/controllers/projects/tags/releases_controller_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::Tags::ReleasesController do - let!(:project) { create(:project, :repository) } - let!(:user) { create(:user) } - let!(:release) { create(:release, project: project, tag: "v1.1.0") } - let!(:tag) { release.tag } - - before do - project.add_developer(user) - sign_in(user) - end - - describe 'GET #edit' do - it 'initializes a new release' do - tag_id = release.tag - project.releases.destroy_all # rubocop: disable Cop/DestroyAll - - response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id } - - release = assigns(:release) - expect(release).not_to be_nil - expect(release).not_to be_persisted - expect(response).to have_gitlab_http_status(:ok) - end - - it 'retrieves an existing release' do - response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag } - - release = assigns(:release) - expect(release).not_to be_nil - expect(release).to be_persisted - expect(response).to have_gitlab_http_status(:ok) - end - end - - describe 'PUT #update' do - it 'updates release note description' do - response = update_release(release.tag, "description updated") - - release = project.releases.find_by(tag: tag) - expect(release.description).to eq("description updated") - expect(response).to have_gitlab_http_status(:found) - end - - it 'creates a release if one does not exist' do - tag_without_release = create_new_tag - - expect do - update_release(tag_without_release.name, "a new release") - end.to change { project.releases.count }.by(1) - - expect(response).to have_gitlab_http_status(:found) - end - - it 'sets the release name, sha, and author for a new release' do - tag_without_release = create_new_tag - - response = update_release(tag_without_release.name, "a new release") - - release = project.releases.find_by(tag: tag_without_release.name) - expect(release.name).to eq(tag_without_release.name) - expect(release.sha).to eq(tag_without_release.target_commit.sha) - expect(release.author.id).to eq(user.id) - expect(response).to have_gitlab_http_status(:found) - end - - it 'does not delete release when description is empty' do - expect do - update_release(tag, "") - end.not_to change { project.releases.count } - - expect(release.reload.description).to eq("") - - expect(response).to have_gitlab_http_status(:found) - end - - it 'does nothing when description is empty and the tag does not have a release' do - tag_without_release = create_new_tag - - expect do - update_release(tag_without_release.name, "") - end.not_to change { project.releases.count } - - expect(response).to have_gitlab_http_status(:found) - end - end - - def create_new_tag - project.repository.add_tag(user, 'mytag', 'master') - end - - def update_release(tag_id, description) - put :update, params: { - namespace_id: project.namespace.to_param, - project_id: project, - tag_id: tag_id, - release: { description: description } - } - end -end diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb index af7ffa2c1ec..857d0696659 100644 --- a/spec/features/projects/tags/user_edits_tags_spec.rb +++ b/spec/features/projects/tags/user_edits_tags_spec.rb @@ -15,6 +15,13 @@ RSpec.describe 'Project > Tags', :js do end shared_examples "can create and update release" do + it 'shows tag information' do + visit page_url + + expect(page).to have_content 'v1.1.0' + expect(page).to have_content 'Version 1.1.0' + end + it 'can create new release' do visit page_url page.find("a[href=\"#{new_project_release_path(project, tag_name: 'v1.1.0')}\"]").click @@ -52,71 +59,4 @@ RSpec.describe 'Project > Tags', :js do include_examples "can create and update release" end - - # TODO: remove most of these together with FF https://gitlab.com/gitlab-org/gitlab/-/issues/366244 - describe 'when opening project tags' do - before do - stub_feature_flags(edit_tag_release_notes_via_release_page: false) - visit project_tags_path(project) - end - - context 'page with tags list' do - it 'shows tag name' do - expect(page).to have_content 'v1.1.0' - expect(page).to have_content 'Version 1.1.0' - end - - it 'shows tag edit button' do - page.within '.tags > .content-list' do - edit_btn = page.find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']") - - expect(edit_btn['href']).to end_with("/#{project.full_path}/-/tags/v1.1.0/release/edit") - end - end - end - - context 'edit tag release notes' do - before do - page.find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']").click - end - - it 'shows tag name header' do - page.within('.content') do - expect(page.find('.sub-header-block')).to have_content 'Release notes for tag v1.1.0' - end - end - - it 'shows release notes form' do - page.within('.content') do - expect(page).to have_selector('form.release-form') - end - end - - it 'toolbar buttons on release notes form are functional' do - page.within('.content form.release-form') do - note_textarea = page.find('.js-gfm-input') - - # Click on Bold button - page.find('.md-header-toolbar button:first-child').click - - expect(note_textarea.value).to eq('****') - end - end - - it 'release notes form shows "Attach a file or image" button', :js do - page.within('.content form.release-form') do - expect(page).to have_selector('[data-testid="button-attach-file"]') - expect(page).not_to have_selector('.uploading-progress-container', visible: true) - end - end - - it 'shows "Attaching a file" message on uploading 1 file', :js, :capybara_ignore_server_errors do - slow_requests do - dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) - - expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') - end - end - end - end end diff --git a/spec/features/tags/developer_updates_tag_spec.rb b/spec/features/tags/developer_updates_tag_spec.rb deleted file mode 100644 index 531ed91c057..00000000000 --- a/spec/features/tags/developer_updates_tag_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# TODO: remove this file together with FF https://gitlab.com/gitlab-org/gitlab/-/issues/366244 -RSpec.describe 'Developer updates tag' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, :repository, namespace: group) } - - before do - project.add_developer(user) - sign_in(user) - stub_feature_flags(edit_tag_release_notes_via_release_page: false) - visit project_tags_path(project) - end - - context 'from the tags list page' do - it 'updates the release notes' do - find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']").click - - fill_in 'release_description', with: 'Awesome release notes' - click_button 'Save changes' - - expect(page).to have_current_path( - project_tag_path(project, 'v1.1.0'), ignore_query: true) - expect(page).to have_content 'v1.1.0' - expect(page).to have_content 'Awesome release notes' - end - - it 'description has emoji autocomplete', :js do - page.within(first('.content-list .controls')) do - click_link 'Edit release notes' - end - - find('#release_description').native.send_keys('') - fill_in 'release_description', with: ':' - - expect(page).to have_selector('.atwho-view') - end - end - - context 'from a specific tag page' do - it 'updates the release notes' do - click_on 'v1.1.0' - click_link 'Edit release notes' - fill_in 'release_description', with: 'Awesome release notes' - click_button 'Save changes' - - expect(page).to have_current_path( - project_tag_path(project, 'v1.1.0'), ignore_query: true) - expect(page).to have_content 'v1.1.0' - expect(page).to have_content 'Awesome release notes' - end - end -end diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js index 12972fff58e..8a9af812c89 100644 --- a/spec/frontend/vue_shared/components/markdown/header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/header_spec.js @@ -56,7 +56,6 @@ describe('Markdown field header component', () => { 'Add a task list', 'Add a collapsible section', 'Add a table', - 'Attach a file or image', 'Go full screen', ]; const elements = findToolbarButtons(); @@ -66,6 +65,13 @@ describe('Markdown field header component', () => { }); }); + it('renders "Attach a file or image" button using gl-button', () => { + const button = wrapper.findByTestId('button-attach-file'); + + expect(button.element.tagName).toBe('GL-BUTTON-STUB'); + expect(button.attributes('title')).toBe('Attach a file or image'); + }); + describe('when the user is on a non-Mac', () => { beforeEach(() => { delete window.gl.client.isMac; diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb index 5e2a307e959..706570babd5 100644 --- a/spec/policies/issuable_policy_spec.rb +++ b/spec/policies/issuable_policy_spec.rb @@ -113,5 +113,45 @@ RSpec.describe IssuablePolicy, models: true do end end end + + context 'when user is anonymous' do + it 'does not allow timelogs creation' do + expect(permissions(nil, issue)).to be_disallowed(:create_timelog) + end + end + + context 'when user is not a member of the project' do + it 'does not allow timelogs creation' do + expect(policies).to be_disallowed(:create_timelog) + end + end + + context 'when user is not a member of the project but the author of the issuable' do + let(:issue) { create(:issue, project: project, author: user) } + + it 'does not allow timelogs creation' do + expect(policies).to be_disallowed(:create_timelog) + end + end + + context 'when user is a guest member of the project' do + it 'does not allow timelogs creation' do + expect(permissions(guest, issue)).to be_disallowed(:create_timelog) + end + end + + context 'when user is a guest member of the project and the author of the issuable' do + let(:issue) { create(:issue, project: project, author: guest) } + + it 'does not allow timelogs creation' do + expect(permissions(guest, issue)).to be_disallowed(:create_timelog) + end + end + + context 'when user is at least reporter of the project' do + it 'allows timelogs creation' do + expect(permissions(reporter, issue)).to be_allowed(:create_timelog) + end + end end end diff --git a/spec/requests/api/graphql/mutations/timelogs/create_spec.rb b/spec/requests/api/graphql/mutations/timelogs/create_spec.rb new file mode 100644 index 00000000000..eea04b89783 --- /dev/null +++ b/spec/requests/api/graphql/mutations/timelogs/create_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a timelog' do + include GraphqlHelpers + + let_it_be(:author) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:time_spent) { '1h' } + + let(:current_user) { nil } + let(:users_container) { project } + let(:mutation) do + graphql_mutation(:timelogCreate, { + 'time_spent' => time_spent, + 'spent_at' => '2022-07-08', + 'summary' => 'Test summary', + 'issuable_id' => issuable.to_global_id.to_s + }) + end + + let(:mutation_response) { graphql_mutation_response(:timelog_create) } + + context 'when issuable is an Issue' do + let_it_be(:issuable) { create(:issue, project: project) } + + it_behaves_like 'issuable supports timelog creation mutation' + end + + context 'when issuable is a MergeRequest' do + let_it_be(:issuable) { create(:merge_request, source_project: project) } + + it_behaves_like 'issuable supports timelog creation mutation' + end + + context 'when issuable is a WorkItem' do + let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem') } + + it_behaves_like 'issuable supports timelog creation mutation' + end + + context 'when issuable is an Incident' do + let_it_be(:issuable) { create(:incident, project: project) } + + it_behaves_like 'issuable supports timelog creation mutation' + end +end diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 911568bc39f..2a542cef5de 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Create a work item' do { 'title' => 'new title', 'description' => 'new description', + 'confidential' => true, 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } end @@ -38,6 +39,7 @@ RSpec.describe 'Create a work item' do expect(response).to have_gitlab_http_status(:success) expect(created_work_item.issue_type).to eq('task') + expect(created_work_item).to be_confidential expect(created_work_item.work_item_type.base_type).to eq('task') expect(mutation_response['workItem']).to include( input.except('workItemTypeId').merge( diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index c18b28dd93e..bd6ec4b9535 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -34,6 +34,10 @@ RSpec.describe 'Update a work item' do context 'when user has permissions to update a work item' do let(:current_user) { developer } + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::WorkItems::Update } + end + context 'when the work item is open' do it 'closes and updates the work item' do expect do @@ -71,36 +75,48 @@ RSpec.describe 'Update a work item' do end end - context 'when unsupported widget input is sent' do - let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') } - let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } - - let(:input) do - { - 'hierarchyWidget' => {} + context 'when updating confidentiality' do + let(:fields) do + <<~FIELDS + workItem { + confidential } + errors + FIELDS end - it_behaves_like 'a mutation that returns top-level errors', - errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"] - end + shared_examples 'toggling confidentiality' do + it 'successfully updates work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :confidential).from(values[:old]).to(values[:new]) - it_behaves_like 'has spam protection' do - let(:mutation_class) { ::Mutations::WorkItems::Update } - end - - context 'when the work_items feature flag is disabled' do - before do - stub_feature_flags(work_items: false) + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']).to include( + 'confidential' => values[:new] + ) + end end - it 'does not update the work item and returns and error' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to not_change(work_item, :title) + context 'when setting as confidential' do + let(:input) { { 'confidential' => true } } - expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + it_behaves_like 'toggling confidentiality' do + let(:values) { { old: false, new: true }} + end + end + + context 'when setting as non-confidential' do + let(:input) { { 'confidential' => false } } + + before do + work_item.update!(confidential: true) + end + + it_behaves_like 'toggling confidentiality' do + let(:values) { { old: true, new: false }} + end end end @@ -322,5 +338,34 @@ RSpec.describe 'Update a work item' do end end end + + context 'when unsupported widget input is sent' do + let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') } + let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } + + let(:input) do + { + 'hierarchyWidget' => {} + } + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"] + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'does not update the work item and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :title) + + expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + end + end end end diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index 566d73a3b75..2421fab0eec 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -111,14 +111,6 @@ RSpec.describe Releases::CreateService do expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}") end end - end - - describe '#find_or_build_release' do - it 'does not save the built release' do - service.find_or_build_release - - expect(project.releases.count).to eq(0) - end context 'when existing milestone is passed in' do let(:title) { 'v1.0' } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index aa3829d9ea5..2a6adef2155 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -432,6 +432,19 @@ RSpec.describe SystemNoteService do end end + describe '.created_timelog' do + let(:issue) { create(:issue, project: project) } + let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)} + + it 'calls TimeTrackingService' do + expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service| + expect(service).to receive(:created_timelog) + end + + described_class.created_timelog(noteable, project, author, timelog) + end + end + describe '.remove_timelog' do let(:issue) { create(:issue, project: project) } let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)} diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb index fdf18f4f29a..65e8632b3ba 100644 --- a/spec/services/system_notes/time_tracking_service_spec.rb +++ b/spec/services/system_notes/time_tracking_service_spec.rb @@ -106,6 +106,30 @@ RSpec.describe ::SystemNotes::TimeTrackingService do end end + describe '#create_timelog' do + subject { described_class.new(noteable: noteable, project: project, author: author).created_timelog(timelog) } + + context 'when the timelog has a positive time spent value' do + let_it_be(:noteable, reload: true) { create(:issue, project: project) } + + let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z')} + + it 'sets the note text' do + expect(subject.note).to eq "added 30m of time spent at 2022-03-30" + end + end + + context 'when the timelog has a negative time spent value' do + let_it_be(:noteable, reload: true) { create(:issue, project: project) } + + let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z')} + + it 'sets the note text' do + expect(subject.note).to eq "subtracted 30m of time spent at 2022-03-30" + end + end + end + describe '#remove_timelog' do subject { described_class.new(noteable: noteable, project: project, author: author).remove_timelog(timelog) } diff --git a/spec/services/timelogs/create_service_spec.rb b/spec/services/timelogs/create_service_spec.rb new file mode 100644 index 00000000000..b5ed4a005c7 --- /dev/null +++ b/spec/services/timelogs/create_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Timelogs::CreateService do + let_it_be(:author) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:time_spent) { 3600 } + let_it_be(:spent_at) { "2022-07-08" } + let_it_be(:summary) { "Test summary" } + + let(:issuable) { nil } + let(:users_container) { project } + let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) } + + describe '#execute' do + subject { service.execute } + + context 'when issuable is an Issue' do + let_it_be(:issuable) { create(:issue, project: project) } + let_it_be(:note_noteable) { create(:issue, project: project) } + + it_behaves_like 'issuable supports timelog creation service' + end + + context 'when issuable is a MergeRequest' do + let_it_be(:issuable) { create(:merge_request, source_project: project, source_branch: 'branch-1') } + let_it_be(:note_noteable) { create(:merge_request, source_project: project, source_branch: 'branch-2') } + + it_behaves_like 'issuable supports timelog creation service' + end + + context 'when issuable is a WorkItem' do + let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem-1') } + let_it_be(:note_noteable) { create(:work_item, project: project, title: 'WorkItem-2') } + + it_behaves_like 'issuable supports timelog creation service' + end + + context 'when issuable is an Incident' do + let_it_be(:issuable) { create(:incident, project: project) } + let_it_be(:note_noteable) { create(:incident, project: project) } + + it_behaves_like 'issuable supports timelog creation service' + end + end +end diff --git a/spec/services/timelogs/delete_service_spec.rb b/spec/services/timelogs/delete_service_spec.rb index c52cebdc5bf..6f70bf0b68d 100644 --- a/spec/services/timelogs/delete_service_spec.rb +++ b/spec/services/timelogs/delete_service_spec.rb @@ -21,8 +21,8 @@ RSpec.describe Timelogs::DeleteService do end it 'returns the removed timelog' do - expect(subject).to be_success - expect(subject.payload).to eq(timelog) + is_expected.to be_success + expect(subject.payload[:timelog]).to eq(timelog) end end @@ -31,7 +31,7 @@ RSpec.describe Timelogs::DeleteService do let!(:timelog) { nil } it 'returns an error' do - expect(subject).to be_error + is_expected.to be_error expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it') expect(subject.http_status).to eq(404) end @@ -41,7 +41,7 @@ RSpec.describe Timelogs::DeleteService do let(:user) { create(:user) } it 'returns an error' do - expect(subject).to be_error + is_expected.to be_error expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it') expect(subject.http_status).to eq(404) end @@ -56,7 +56,7 @@ RSpec.describe Timelogs::DeleteService do end it 'returns an error' do - expect(subject).to be_error + is_expected.to be_error expect(subject.message).to eq('Failed to remove timelog') expect(subject.http_status).to eq(400) end diff --git a/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb new file mode 100644 index 00000000000..f28348fb945 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable supports timelog creation mutation' do + context 'when the user is anonymous' do + before do + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is a guest member of the namespace' do + let(:current_user) { create(:user) } + + before do + users_container.add_guest(current_user) + + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to create a timelog' do + let(:current_user) { author } + + before do + users_container.add_reporter(current_user) + end + + context 'with valid data' do + it 'creates the timelog' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Timelog, :count).by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(mutation_response['timelog']).to include( + 'timeSpent' => 3600, + 'spentAt' => '2022-07-08T00:00:00Z', + 'summary' => 'Test summary' + ) + end + end + + context 'with invalid time_spent' do + let(:time_spent) { '3h e' } + + it 'returns an error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Timelog, :count).by(0) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to match_array(['Time spent can\'t be blank']) + expect(mutation_response['timelog']).to be_nil + end + end + end +end + +RSpec.shared_examples 'issuable does not support timelog creation mutation' do + context 'when the user is anonymous' do + before do + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is a guest member of the namespace' do + let(:current_user) { create(:user) } + + before do + users_container.add_guest(current_user) + + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { contain_exactly(include('is not a valid ID for')) } + end + end + + context 'when user has permissions to create a timelog' do + let(:current_user) { author } + + before do + users_container.add_reporter(current_user) + end + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { contain_exactly(include('is not a valid ID for')) } + end + end +end diff --git a/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb b/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb new file mode 100644 index 00000000000..53c42ec0e00 --- /dev/null +++ b/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable supports timelog creation service' do + shared_examples 'success_response' do + it 'sucessfully saves the timelog' do + is_expected.to be_success + + timelog = subject.payload[:timelog] + + expect(timelog).to be_persisted + expect(timelog.time_spent).to eq(time_spent) + expect(timelog.spent_at).to eq('Fri, 08 Jul 2022 00:00:00.000000000 UTC +00:00') + expect(timelog.summary).to eq(summary) + expect(timelog.issuable).to eq(issuable) + end + end + + context 'when the user does not have permission' do + let(:user) { create(:user) } + + it 'returns an error' do + is_expected.to be_error + + expect(subject.message).to eq( + "#{issuable.base_class_name} doesn't exist or you don't have permission to add timelog to it.") + expect(subject.http_status).to eq(404) + end + end + + context 'when the user has permissions' do + let(:user) { author } + + before do + users_container.add_reporter(user) + end + + context 'when the timelog save fails' do + before do + allow_next_instance_of(Timelog) do |timelog| + allow(timelog).to receive(:save).and_return(false) + end + end + + it 'returns an error' do + is_expected.to be_error + expect(subject.message).to eq('Failed to save timelog') + end + end + + context 'when the creation completes sucessfully' do + it_behaves_like 'success_response' + end + end +end + +RSpec.shared_examples 'issuable does not support timelog creation service' do + shared_examples 'error_response' do + it 'returns an error' do + is_expected.to be_error + + issuable_type = if issuable.nil? + 'Issuable' + else + issuable.base_class_name + end + + expect(subject.message).to eq( + "#{issuable_type} doesn't exist or you don't have permission to add timelog to it." + ) + expect(subject.http_status).to eq(404) + end + end + + context 'when the user does not have permission' do + let(:user) { create(:user) } + + it_behaves_like 'error_response' + end + + context 'when the user has permissions' do + let(:user) { author } + + before do + users_container.add_reporter(user) + end + + it_behaves_like 'error_response' + end +end