Commit 47474d27 authored by Sean McGivern's avatar Sean McGivern

Merge branch '17461-remove-labels-on-issue-close' into 'master'

Remove specified labels on issue close

See merge request gitlab-org/gitlab!61286
parents c17f8a2f e025b2da
...@@ -20,6 +20,10 @@ module Mutations ...@@ -20,6 +20,10 @@ module Mutations
required: false, required: false,
description: 'Description of the label.' description: 'Description of the label.'
argument :remove_on_close, GraphQL::BOOLEAN_TYPE,
required: false,
description: copy_field_description(Types::LabelType, :remove_on_close)
argument :color, GraphQL::STRING_TYPE, argument :color, GraphQL::STRING_TYPE,
required: false, required: false,
default_value: Label::DEFAULT_COLOR, default_value: Label::DEFAULT_COLOR,
......
...@@ -23,5 +23,7 @@ module Types ...@@ -23,5 +23,7 @@ module Types
description: 'When this label was created.' description: 'When this label was created.'
field :updated_at, Types::TimeType, null: false, field :updated_at, Types::TimeType, null: false,
description: 'When this label was last updated.' description: 'When this label was last updated.'
field :remove_on_close, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Whether the label should be removed from an issue when the issue is closed.'
end end
end end
...@@ -11,4 +11,5 @@ class LabelLink < ApplicationRecord ...@@ -11,4 +11,5 @@ class LabelLink < ApplicationRecord
validates :label, presence: true, unless: :importing? validates :label, presence: true, unless: :importing?
scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) } scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
scope :with_remove_on_close_labels, -> { joins(:label).where(labels: { remove_on_close: true }) }
end end
...@@ -25,6 +25,7 @@ module Issues ...@@ -25,6 +25,7 @@ module Issues
end end
if project.issues_enabled? && issue.close(current_user) if project.issues_enabled? && issue.close(current_user)
remove_on_close_labels_from(issue)
event_service.close_issue(issue, current_user) event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note create_note(issue, closed_via) if system_note
...@@ -51,6 +52,18 @@ module Issues ...@@ -51,6 +52,18 @@ module Issues
private private
def remove_on_close_labels_from(issue)
old_labels = issue.labels.to_a
issue.label_links.with_remove_on_close_labels.delete_all
issue.labels.reset
Issuable::CommonSystemNotesService.new(project: project, current_user: current_user).execute(
issue,
old_labels: old_labels
)
end
def close_external_issue(issue, closed_via) def close_external_issue(issue, closed_via)
return unless project.external_issue_tracker&.support_close_issue? return unless project.external_issue_tracker&.support_close_issue?
......
---
title: Add option to remove labels on issue close in the REST and GraphQL API
merge_request: 61286
author:
type: added
# frozen_string_literal: true
class AddRemoveOnIssueCloseToLabels < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
add_column :labels, :remove_on_close, :boolean, null: false, default: false
end
end
def down
with_lock_retries do
remove_column :labels, :remove_on_close, :boolean
end
end
end
ab662ff92a4e2a7d324f0652da6f0725e1c38e387f08b89921b43816b5d35834
\ No newline at end of file
...@@ -14273,7 +14273,8 @@ CREATE TABLE labels ( ...@@ -14273,7 +14273,8 @@ CREATE TABLE labels (
description_html text, description_html text,
type character varying, type character varying,
group_id integer, group_id integer,
cached_markdown_version integer cached_markdown_version integer,
remove_on_close boolean DEFAULT false NOT NULL
); );
CREATE SEQUENCE labels_id_seq CREATE SEQUENCE labels_id_seq
...@@ -2700,6 +2700,7 @@ Input type: `LabelCreateInput` ...@@ -2700,6 +2700,7 @@ Input type: `LabelCreateInput`
| <a id="mutationlabelcreatedescription"></a>`description` | [`String`](#string) | Description of the label. | | <a id="mutationlabelcreatedescription"></a>`description` | [`String`](#string) | Description of the label. |
| <a id="mutationlabelcreategrouppath"></a>`groupPath` | [`ID`](#id) | Full path of the group with which the resource is associated. | | <a id="mutationlabelcreategrouppath"></a>`groupPath` | [`ID`](#id) | Full path of the group with which the resource is associated. |
| <a id="mutationlabelcreateprojectpath"></a>`projectPath` | [`ID`](#id) | Full path of the project with which the resource is associated. | | <a id="mutationlabelcreateprojectpath"></a>`projectPath` | [`ID`](#id) | Full path of the project with which the resource is associated. |
| <a id="mutationlabelcreateremoveonclose"></a>`removeOnClose` | [`Boolean`](#boolean) | Whether the label should be removed from an issue when the issue is closed. |
| <a id="mutationlabelcreatetitle"></a>`title` | [`String!`](#string) | Title of the label. | | <a id="mutationlabelcreatetitle"></a>`title` | [`String!`](#string) | Title of the label. |
#### Fields #### Fields
...@@ -9742,6 +9743,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -9742,6 +9743,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="labeldescription"></a>`description` | [`String`](#string) | Description of the label (Markdown rendered as HTML for caching). | | <a id="labeldescription"></a>`description` | [`String`](#string) | Description of the label (Markdown rendered as HTML for caching). |
| <a id="labeldescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. | | <a id="labeldescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
| <a id="labelid"></a>`id` | [`ID!`](#id) | Label ID. | | <a id="labelid"></a>`id` | [`ID!`](#id) | Label ID. |
| <a id="labelremoveonclose"></a>`removeOnClose` | [`Boolean!`](#boolean) | Whether the label should be removed from an issue when the issue is closed. |
| <a id="labeltextcolor"></a>`textColor` | [`String!`](#string) | Text color of the label. | | <a id="labeltextcolor"></a>`textColor` | [`String!`](#string) | Text color of the label. |
| <a id="labeltitle"></a>`title` | [`String!`](#string) | Content of the label. | | <a id="labeltitle"></a>`title` | [`String!`](#string) | Content of the label. |
| <a id="labelupdatedat"></a>`updatedAt` | [`Time!`](#time) | When this label was last updated. | | <a id="labelupdatedat"></a>`updatedAt` | [`Time!`](#time) | When this label was last updated. |
......
...@@ -46,7 +46,8 @@ Example response: ...@@ -46,7 +46,8 @@ Example response:
"open_merge_requests_count": 1, "open_merge_requests_count": 1,
"subscribed": false, "subscribed": false,
"priority": 10, "priority": 10,
"is_project_label": true "is_project_label": true,
"remove_on_close": false
}, },
{ {
"id" : 4, "id" : 4,
...@@ -60,7 +61,8 @@ Example response: ...@@ -60,7 +61,8 @@ Example response:
"open_merge_requests_count": 0, "open_merge_requests_count": 0,
"subscribed": false, "subscribed": false,
"priority": null, "priority": null,
"is_project_label": true "is_project_label": true,
"remove_on_close": false
}, },
{ {
"id" : 7, "id" : 7,
...@@ -74,7 +76,8 @@ Example response: ...@@ -74,7 +76,8 @@ Example response:
"open_merge_requests_count": 1, "open_merge_requests_count": 1,
"subscribed": false, "subscribed": false,
"priority": null, "priority": null,
"is_project_label": true "is_project_label": true,
"remove_on_close": true
}, },
{ {
"id" : 8, "id" : 8,
...@@ -88,7 +91,8 @@ Example response: ...@@ -88,7 +91,8 @@ Example response:
"open_merge_requests_count": 2, "open_merge_requests_count": 2,
"subscribed": false, "subscribed": false,
"priority": null, "priority": null,
"is_project_label": false "is_project_label": false,
"remove_on_close": false
}, },
{ {
"id" : 9, "id" : 9,
...@@ -102,7 +106,8 @@ Example response: ...@@ -102,7 +106,8 @@ Example response:
"open_merge_requests_count": 1, "open_merge_requests_count": 1,
"subscribed": true, "subscribed": true,
"priority": null, "priority": null,
"is_project_label": true "is_project_label": true,
"remove_on_close": false
} }
] ]
``` ```
...@@ -140,7 +145,8 @@ Example response: ...@@ -140,7 +145,8 @@ Example response:
"open_merge_requests_count": 1, "open_merge_requests_count": 1,
"subscribed": false, "subscribed": false,
"priority": 10, "priority": 10,
"is_project_label": true "is_project_label": true,
"remove_on_close": true
} }
``` ```
...@@ -159,6 +165,7 @@ POST /projects/:id/labels ...@@ -159,6 +165,7 @@ POST /projects/:id/labels
| `color` | string | yes | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) | | `color` | string | yes | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
| `description` | string | no | The description of the label | | `description` | string | no | The description of the label |
| `priority` | integer | no | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. | | `priority` | integer | no | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
| `remove_on_close` | boolean | no | Whether the label should be removed from an issue when the issue is closed. _([Introduced in GitLab 13.12](https://gitlab.com/gitlab-org/gitlab/-/issues/17461))_ |
```shell ```shell
curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels" curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels"
...@@ -179,7 +186,8 @@ Example response: ...@@ -179,7 +186,8 @@ Example response:
"open_merge_requests_count": 0, "open_merge_requests_count": 0,
"subscribed": false, "subscribed": false,
"priority": null, "priority": null,
"is_project_label": true "is_project_label": true,
"remove_on_close": true
} }
``` ```
...@@ -220,6 +228,7 @@ PUT /projects/:id/labels/:label_id ...@@ -220,6 +228,7 @@ PUT /projects/:id/labels/:label_id
| `color` | string | yes if `new_name` is not provided | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) | | `color` | string | yes if `new_name` is not provided | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
| `description` | string | no | The new description of the label | | `description` | string | no | The new description of the label |
| `priority` | integer | no | The new priority of the label. Must be greater or equal than zero or `null` to remove the priority. | | `priority` | integer | no | The new priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
| `remove_on_close` | boolean | no | Boolean option specifying whether the label should be removed from issues when they are closed. |
```shell ```shell
curl --request PUT --data "new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels/documentation" curl --request PUT --data "new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels/documentation"
...@@ -240,7 +249,8 @@ Example response: ...@@ -240,7 +249,8 @@ Example response:
"open_merge_requests_count": 2, "open_merge_requests_count": 2,
"subscribed": false, "subscribed": false,
"priority": null, "priority": null,
"is_project_label": true "is_project_label": true,
"remove_on_close": true
} }
``` ```
...@@ -281,7 +291,8 @@ Example response: ...@@ -281,7 +291,8 @@ Example response:
"open_issues_count": 1, "open_issues_count": 1,
"closed_issues_count": 0, "closed_issues_count": 0,
"open_merge_requests_count": 2, "open_merge_requests_count": 2,
"subscribed": false "subscribed": false,
"remove_on_close": true
} }
``` ```
...@@ -322,7 +333,8 @@ Example response: ...@@ -322,7 +333,8 @@ Example response:
"open_merge_requests_count": 1, "open_merge_requests_count": 1,
"subscribed": true, "subscribed": true,
"priority": null, "priority": null,
"is_project_label": true "is_project_label": true,
"remove_on_close": true
} }
``` ```
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module API module API
module Entities module Entities
class LabelBasic < Grape::Entity class LabelBasic < Grape::Entity
expose :id, :name, :color, :description, :description_html, :text_color expose :id, :name, :color, :description, :description_html, :text_color, :remove_on_close
end end
end end
end end
...@@ -5,27 +5,34 @@ module API ...@@ -5,27 +5,34 @@ module API
module LabelHelpers module LabelHelpers
extend Grape::API::Helpers extend Grape::API::Helpers
params :optional_label_params do
optional :description, type: String, desc: 'The description of the label'
optional :remove_on_close, type: Boolean, desc: 'Whether the label should be removed from an issue when the issue is closed'
end
params :label_create_params do params :label_create_params do
requires :name, type: String, desc: 'The name of the label to be created' requires :name, type: String, desc: 'The name of the label to be created'
requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names" requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
optional :description, type: String, desc: 'The description of label to be created'
use :optional_label_params
end end
params :label_update_params do params :label_update_params do
optional :new_name, type: String, desc: 'The new name of the label' optional :new_name, type: String, desc: 'The new name of the label'
optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names" optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
optional :description, type: String, desc: 'The new description of label'
use :optional_label_params
end end
params :project_label_update_params do params :project_label_update_params do
use :label_update_params use :label_update_params
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
at_least_one_of :new_name, :color, :description, :priority at_least_one_of :new_name, :color, :description, :priority, :remove_on_close
end end
params :group_label_update_params do params :group_label_update_params do
use :label_update_params use :label_update_params
at_least_one_of :new_name, :color, :description at_least_one_of :new_name, :color, :description, :remove_on_close
end end
def find_label(parent, id_or_title, params = { include_ancestor_groups: true }) def find_label(parent, id_or_title, params = { include_ancestor_groups: true })
......
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
"color", "color",
"description", "description",
"description_html", "description_html",
"text_color" "text_color",
"remove_on_close"
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
...@@ -20,7 +21,8 @@ ...@@ -20,7 +21,8 @@
"text_color": { "text_color": {
"type": "string", "type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$" "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
} },
"remove_on_close": { "type": "boolean" }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -69,17 +69,33 @@ RSpec.describe Mutations::Issues::Update do ...@@ -69,17 +69,33 @@ RSpec.describe Mutations::Issues::Update do
context 'when changing state' do context 'when changing state' do
let_it_be_with_refind(:issue) { create(:issue, project: project, state: :opened) } let_it_be_with_refind(:issue) { create(:issue, project: project, state: :opened) }
it 'closes issue' do before do
mutation_params[:state_event] = 'close' mutation_params[:state_event] = state_event
end
context 'when state_event is close' do
let_it_be(:removable_label) { create(:label, project: project, remove_on_close: true, issues: [issue]) }
expect { subject }.to change { issue.reload.state }.from('opened').to('closed') let(:state_event) { 'close' }
it 'closes issue' do
expect do
subject
issue.reload
end.to change(issue, :state).from('opened').to('closed').and(
change { issue.label_ids }.from([removable_label.id]).to([])
)
end
end end
it 'reopens issue' do context 'when state_event is reopen' do
issue.close let(:state_event) { 'reopen' }
mutation_params[:state_event] = 'reopen'
it 'reopens issue' do
issue.close
expect { subject }.to change { issue.reload.state }.from('closed').to('opened') expect { subject }.to change { issue.reload.state }.from('closed').to('opened')
end
end end
end end
......
...@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['Label'] do ...@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['Label'] do
:color, :color,
:text_color, :text_color,
:created_at, :created_at,
:remove_on_close,
:updated_at :updated_at
] ]
......
...@@ -102,6 +102,7 @@ ProjectLabel: ...@@ -102,6 +102,7 @@ ProjectLabel:
- template - template
- description - description
- priority - priority
- remove_on_close
Milestone: Milestone:
- id - id
- title - title
......
...@@ -13,14 +13,27 @@ RSpec.describe LabelLink do ...@@ -13,14 +13,27 @@ RSpec.describe LabelLink do
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end end
describe '.for_target' do describe 'scopes' do
it 'returns the label links for a given target' do describe '.for_target' do
label_link = create(:label_link, target: create(:merge_request)) it 'returns the label links for a given target' do
label_link = create(:label_link, target: create(:merge_request))
create(:label_link, target: create(:issue)) create(:label_link, target: create(:issue))
expect(described_class.for_target(label_link.target_id, label_link.target_type)) expect(described_class.for_target(label_link.target_id, label_link.target_type))
.to contain_exactly(label_link) .to contain_exactly(label_link)
end
end
describe '.with_remove_on_close_labels' do
it 'responds with label_links that can be removed when an issue is closed' do
issue = create(:issue)
removable_label = create(:label, project: issue.project, remove_on_close: true)
create(:label_link, target: issue)
removable_issue_label_link = create(:label_link, label: removable_label, target: issue)
expect(described_class.with_remove_on_close_labels).to contain_exactly(removable_issue_label_link)
end
end end
end end
end end
...@@ -11,7 +11,8 @@ RSpec.describe Mutations::Labels::Create do ...@@ -11,7 +11,8 @@ RSpec.describe Mutations::Labels::Create do
{ {
'title' => 'foo', 'title' => 'foo',
'description' => 'some description', 'description' => 'some description',
'color' => '#FF0000' 'color' => '#FF0000',
'removeOnClose' => true
} }
end end
......
...@@ -290,7 +290,7 @@ RSpec.describe API::GroupLabels do ...@@ -290,7 +290,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name } put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('new_name, color, description are missing, '\ expect(json_response['error']).to eq('new_name, color, description, remove_on_close are missing, '\
'at least one parameter must be provided') 'at least one parameter must be provided')
end end
end end
...@@ -337,7 +337,7 @@ RSpec.describe API::GroupLabels do ...@@ -337,7 +337,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user) put api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user)
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('new_name, color, description are missing, '\ expect(json_response['error']).to eq('new_name, color, description, remove_on_close are missing, '\
'at least one parameter must be provided') 'at least one parameter must be provided')
end end
end end
......
...@@ -402,6 +402,17 @@ RSpec.describe API::Issues do ...@@ -402,6 +402,17 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['state']).to eq 'opened' expect(json_response['state']).to eq 'opened'
end end
it 'removes labels marked to be removed on issue closed' do
removable_label = create(:label, project: project, remove_on_close: true)
create(:label_link, target: issue, label: removable_label)
put api_for_user, params: { state_event: 'close' }
expect(issue.reload.label_ids).not_to include(removable_label.id)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['state']).to eq 'closed'
end
end end
describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do
......
...@@ -57,7 +57,7 @@ RSpec.describe API::Labels do ...@@ -57,7 +57,7 @@ RSpec.describe API::Labels do
put_labels_api(route_type, user, spec_params) put_labels_api(route_type, user, spec_params)
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\ expect(json_response['error']).to eq('new_name, color, description, priority, remove_on_close are missing, '\
'at least one parameter must be provided') 'at least one parameter must be provided')
end end
...@@ -112,6 +112,14 @@ RSpec.describe API::Labels do ...@@ -112,6 +112,14 @@ RSpec.describe API::Labels do
expect(json_response['id']).to eq(expected_response_label_id) expect(json_response['id']).to eq(expected_response_label_id)
expect(json_response['priority']).to eq(10) expect(json_response['priority']).to eq(10)
end end
it "returns 200 if remove_on_close is changed (#{route_type} route)" do
put_labels_api(route_type, user, spec_params, remove_on_close: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(expected_response_label_id)
expect(json_response['remove_on_close']).to eq(true)
end
end end
it 'returns 200 if a priority is removed (deprecated route)' do it 'returns 200 if a priority is removed (deprecated route)' do
...@@ -301,7 +309,8 @@ RSpec.describe API::Labels do ...@@ -301,7 +309,8 @@ RSpec.describe API::Labels do
name: valid_label_title_2, name: valid_label_title_2,
color: '#FFAABB', color: '#FFAABB',
description: 'test', description: 'test',
priority: 2 priority: 2,
remove_on_close: true
} }
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
...@@ -309,6 +318,7 @@ RSpec.describe API::Labels do ...@@ -309,6 +318,7 @@ RSpec.describe API::Labels do
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test') expect(json_response['description']).to eq('test')
expect(json_response['priority']).to eq(2) expect(json_response['priority']).to eq(2)
expect(json_response['remove_on_close']).to eq(true)
end end
it 'returns created label when only required params' do it 'returns created label when only required params' do
......
...@@ -3,22 +3,50 @@ ...@@ -3,22 +3,50 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Issues::CloseService do RSpec.describe Issues::CloseService do
let(:project) { create(:project, :repository) } subject(:close_issue) { described_class.new(project: project, current_user: user).close_issue(issue) }
let(:user) { create(:user, email: "user@example.com") }
let(:user2) { create(:user, email: "user2@example.com") } let_it_be(:project, refind: true) { create(:project, :repository) }
let(:guest) { create(:user) } let_it_be(:label1) { create(:label, project: project) }
let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) } let_it_be(:label2) { create(:label, project: project, remove_on_close: true) }
let_it_be(:author) { create(:user) }
let_it_be(:user) { create(:user, email: "user@example.com") }
let_it_be(:user2) { create(:user, email: "user2@example.com") }
let_it_be(:guest) { create(:user) }
let_it_be(:closing_merge_request) { create(:merge_request, source_project: project) }
let(:external_issue) { ExternalIssue.new('JIRA-123', project) } let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
let(:closing_merge_request) { create(:merge_request, source_project: project) } let!(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: author) }
let(:closing_commit) { create(:commit, project: project) }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do before_all do
project.add_maintainer(user) project.add_maintainer(user)
project.add_developer(user2) project.add_developer(user2)
project.add_guest(guest) project.add_guest(guest)
end end
shared_examples 'removes labels marked for removal from issue when closed' do
before do
issue.update!(label_ids: [label1.id, label2.id])
end
it 'removes labels marked for removal' do
expect do
close_issue
end.to change { issue.reload.label_ids }.from(containing_exactly(label1.id, label2.id)).to(containing_exactly(label1.id))
end
it 'creates system notes for the removed labels' do
expect do
close_issue
end.to change(ResourceLabelEvent, :count).by(1)
expect(ResourceLabelEvent.last.slice(:action, :issue_id, :label_id)).to eq(
'action' => 'remove',
'issue_id' => issue.id,
'label_id' => label2.id
)
end
end
describe '#execute' do describe '#execute' do
let(:service) { described_class.new(project: project, current_user: user) } let(:service) { described_class.new(project: project, current_user: user) }
...@@ -121,6 +149,8 @@ RSpec.describe Issues::CloseService do ...@@ -121,6 +149,8 @@ RSpec.describe Issues::CloseService do
end end
end end
it_behaves_like 'removes labels marked for removal from issue when closed'
it 'mentions closure via a merge request' do it 'mentions closure via a merge request' do
close_issue close_issue
...@@ -184,10 +214,18 @@ RSpec.describe Issues::CloseService do ...@@ -184,10 +214,18 @@ RSpec.describe Issues::CloseService do
end end
context "closed by a commit", :sidekiq_might_not_need_inline do context "closed by a commit", :sidekiq_might_not_need_inline do
it 'mentions closure via a commit' do subject(:close_issue) do
perform_enqueued_jobs do perform_enqueued_jobs do
described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit) described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit)
end end
end
let(:closing_commit) { create(:commit, project: project) }
it_behaves_like 'removes labels marked for removal from issue when closed'
it 'mentions closure via a commit' do
close_issue
email = ActionMailer::Base.deliveries.last email = ActionMailer::Base.deliveries.last
...@@ -199,9 +237,8 @@ RSpec.describe Issues::CloseService do ...@@ -199,9 +237,8 @@ RSpec.describe Issues::CloseService do
context 'when user cannot read the commit' do context 'when user cannot read the commit' do
it 'does not mention the commit id' do it 'does not mention the commit id' do
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
perform_enqueued_jobs do
described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit) close_issue
end
email = ActionMailer::Base.deliveries.last email = ActionMailer::Base.deliveries.last
body_text = email.body.parts.map(&:body).join(" ") body_text = email.body.parts.map(&:body).join(" ")
...@@ -222,12 +259,14 @@ RSpec.describe Issues::CloseService do ...@@ -222,12 +259,14 @@ RSpec.describe Issues::CloseService do
it 'verifies the number of queries' do it 'verifies the number of queries' do
recorded = ActiveRecord::QueryRecorder.new { close_issue } recorded = ActiveRecord::QueryRecorder.new { close_issue }
expected_queries = 23 expected_queries = 32
expect(recorded.count).to be <= expected_queries expect(recorded.count).to be <= expected_queries
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
end end
it_behaves_like 'removes labels marked for removal from issue when closed'
it 'closes the issue' do it 'closes the issue' do
close_issue close_issue
...@@ -257,6 +296,8 @@ RSpec.describe Issues::CloseService do ...@@ -257,6 +296,8 @@ RSpec.describe Issues::CloseService do
end end
it 'marks todos as done' do it 'marks todos as done' do
todo = create(:todo, :assigned, user: user, project: project, target: issue, author: user2)
close_issue close_issue
expect(todo.reload).to be_done expect(todo.reload).to be_done
...@@ -321,26 +362,32 @@ RSpec.describe Issues::CloseService do ...@@ -321,26 +362,32 @@ RSpec.describe Issues::CloseService do
end end
context 'when issue is not confidential' do context 'when issue is not confidential' do
it_behaves_like 'removes labels marked for removal from issue when closed'
it 'executes issue hooks' do it 'executes issue hooks' do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
described_class.new(project: project, current_user: user).close_issue(issue) close_issue
end end
end end
context 'when issue is confidential' do context 'when issue is confidential' do
it 'executes confidential issue hooks' do let(:issue) { create(:issue, :confidential, project: project) }
issue = create(:issue, :confidential, project: project)
it_behaves_like 'removes labels marked for removal from issue when closed'
it 'executes confidential issue hooks' do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
described_class.new(project: project, current_user: user).close_issue(issue) close_issue
end end
end end
context 'internal issues disabled' do context 'internal issues disabled' do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do before do
project.issues_enabled = false project.issues_enabled = false
project.save! project.save!
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment