901 lines
30 KiB
Ruby
901 lines
30 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe Notes::QuickActionsService, feature_category: :text_editors do
|
|
let_it_be_with_reload(:project) { create(:project, :repository) }
|
|
|
|
shared_context 'note on noteable' do
|
|
let_it_be(:maintainer) { create(:user, maintainer_of: project) }
|
|
let_it_be(:assignee) { create(:user) }
|
|
|
|
before_all do
|
|
project.add_maintainer(assignee)
|
|
end
|
|
end
|
|
|
|
shared_examples 'note on noteable that supports quick actions' do
|
|
before do
|
|
note.note = note_text
|
|
end
|
|
|
|
let!(:milestone) { create(:milestone, project: project) }
|
|
let!(:labels) { create_pair(:label, project: project) }
|
|
|
|
describe 'note with only command' do
|
|
describe '/close, /label, /assign & /milestone' do
|
|
let(:note_text) do
|
|
%(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
|
|
end
|
|
|
|
it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable).to be_closed
|
|
expect(note.noteable.labels).to match_array(labels)
|
|
expect(note.noteable.assignees).to eq([assignee])
|
|
expect(note.noteable.milestone).to eq(milestone)
|
|
end
|
|
end
|
|
|
|
context '/relate' do
|
|
let_it_be(:issue) { create(:issue, project: project) }
|
|
let_it_be(:other_issue) { create(:issue, project: project) }
|
|
|
|
let(:note_text) { "/relate #{other_issue.to_reference}" }
|
|
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
|
|
|
|
context 'user cannot relate issues', :sidekiq_inline do
|
|
before do
|
|
project.team.find_member(maintainer.id).destroy!
|
|
project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
|
|
end
|
|
|
|
it 'does not create issue relation' do
|
|
expect { execute(note) }.not_to change { IssueLink.count }
|
|
end
|
|
end
|
|
|
|
context 'user is allowed to relate issues' do
|
|
it 'creates issue relation' do
|
|
expect { execute(note) }.to change { IssueLink.count }.by(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/reopen' do
|
|
before do
|
|
note.noteable.close!
|
|
expect(note.noteable).to be_closed
|
|
end
|
|
|
|
let(:note_text) { '/reopen' }
|
|
|
|
it 'opens the noteable, and leave no note' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable).to be_open
|
|
end
|
|
end
|
|
|
|
describe '/spend' do
|
|
context 'when note is not persisted' do
|
|
let(:note_text) { '/spend 1h' }
|
|
|
|
it 'adds time to noteable, adds timelog with nil note_id and has no content' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable.time_spent).to eq(3600)
|
|
expect(Timelog.last.note_id).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when note is persisted' do
|
|
let(:note_text) { "a note \n/spend 1h" }
|
|
|
|
it 'updates the spent time and populates timelog with note_id' do
|
|
new_content, update_params = service.execute(note)
|
|
note.update!(note: new_content)
|
|
service.apply_updates(update_params, note)
|
|
|
|
expect(Timelog.last.note_id).to eq(note.id)
|
|
end
|
|
end
|
|
|
|
context 'with a timecategory' do
|
|
let!(:timelog_category) { create(:timelog_category, name: 'bob', namespace: project.root_namespace) }
|
|
let(:note_text) { "a note \n/spend 1h [timecategory:bob]" }
|
|
|
|
it 'sets the category of the new timelog' do
|
|
new_content, update_params = service.execute(note)
|
|
note.update!(note: new_content)
|
|
service.apply_updates(update_params, note)
|
|
|
|
expect(Timelog.last.timelog_category_id).to eq(timelog_category.id)
|
|
end
|
|
end
|
|
|
|
context 'adds a system note' do
|
|
context 'when not specifying a date' do
|
|
let(:note_text) { "/spend 1h" }
|
|
|
|
it 'includes the date, time and timezone' do
|
|
allow_any_instance_of(User).to receive(:timezone).and_return('Hawaii') # rubocop:disable RSpec/AnyInstanceOf -- It's not the next instance
|
|
|
|
travel_to(Time.utc(2025, 5, 1)) do
|
|
_, update_params = service.execute(note)
|
|
service.apply_updates(update_params, note)
|
|
|
|
expect(Note.last.note).to eq('added 1h of time spent at 2025-04-30 14:00:00 -1000')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when specifying a date' do
|
|
let(:note_text) { "/spend 1h 2020-01-01" }
|
|
|
|
it 'includes the date, time and timezone' do
|
|
_, update_params = service.execute(note)
|
|
service.apply_updates(update_params, note)
|
|
|
|
expect(Note.last.note).to eq('added 1h of time spent at 2020-01-01 12:00:00 UTC')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/estimate' do
|
|
before do
|
|
# reset to 10 minutes before each test
|
|
note.noteable.update!(time_estimate: 600)
|
|
end
|
|
|
|
shared_examples 'does not update time_estimate and displays the correct error message' do
|
|
it 'shows validation error message' do
|
|
content, update_params = service.execute(note)
|
|
service_response = service.apply_updates(update_params, note)
|
|
|
|
expect(content).to be_empty
|
|
expect(service_response.message).to include('Time estimate must have a valid format and be greater than or equal to zero.')
|
|
expect(note.noteable.reload.time_estimate).to eq(600)
|
|
end
|
|
end
|
|
|
|
context 'when the time estimate is valid' do
|
|
let(:note_text) { '/estimate 1h' }
|
|
|
|
it 'adds time estimate to noteable' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable.reload.time_estimate).to eq(3600)
|
|
end
|
|
end
|
|
|
|
context 'when the time estimate is 0' do
|
|
let(:note_text) { '/estimate 0' }
|
|
|
|
it 'adds time estimate to noteable' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable.reload.time_estimate).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'when the time estimate is invalid' do
|
|
let(:note_text) { '/estimate a' }
|
|
|
|
include_examples "does not update time_estimate and displays the correct error message"
|
|
end
|
|
|
|
context 'when the time estimate is partially invalid' do
|
|
let(:note_text) { '/estimate 1d 3id' }
|
|
|
|
include_examples "does not update time_estimate and displays the correct error message"
|
|
end
|
|
|
|
context 'when the time estimate is negative' do
|
|
let(:note_text) { '/estimate -1h' }
|
|
|
|
include_examples "does not update time_estimate and displays the correct error message"
|
|
end
|
|
end
|
|
|
|
describe '/confidential' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :issue, project: project) }
|
|
let_it_be(:note_text) { '/confidential' }
|
|
let(:note) { create(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
context 'when work item does not have children' do
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'marks work item as confidential' do
|
|
expect { execute(note) }.to change { noteable.reload.confidential }.from(false).to(true)
|
|
end
|
|
end
|
|
|
|
context 'when work item has children' do
|
|
before do
|
|
create(:parent_link, work_item: task, work_item_parent: noteable)
|
|
end
|
|
|
|
context 'when children are not confidential' do
|
|
let(:task) { create(:work_item, :task, project: project) }
|
|
|
|
it 'does not mark parent work item as confidential' do
|
|
expect { execute(note) }.to not_change { noteable.reload.confidential }.from(false)
|
|
expect(noteable.errors[:base]).to include('All child items must be confidential in order to turn on confidentiality.')
|
|
end
|
|
end
|
|
|
|
context 'when children are confidential' do
|
|
let(:task) { create(:work_item, :confidential, :task, project: project) }
|
|
|
|
it 'marks parent work item as confidential' do
|
|
expect { execute(note) }.to change { noteable.reload.confidential }.from(false).to(true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'note with command & text' do
|
|
describe '/close, /label, /assign & /milestone' do
|
|
let(:note_text) do
|
|
%(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}"\nWORLD)
|
|
end
|
|
|
|
it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
|
|
content = execute(note)
|
|
|
|
expect(content).to eq "HELLO\nWORLD"
|
|
expect(note.noteable).to be_closed
|
|
expect(note.noteable.labels).to match_array(labels)
|
|
expect(note.noteable.assignees).to eq([assignee])
|
|
expect(note.noteable.milestone).to eq(milestone)
|
|
end
|
|
end
|
|
|
|
describe '/reopen' do
|
|
before do
|
|
note.noteable.close
|
|
expect(note.noteable).to be_closed
|
|
end
|
|
|
|
let(:note_text) { "HELLO\n/reopen\nWORLD" }
|
|
|
|
it 'opens the noteable' do
|
|
content = execute(note)
|
|
|
|
expect(content).to eq "HELLO\nWORLD"
|
|
expect(note.noteable).to be_open
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/milestone' do
|
|
let(:issue) { create(:issue, project: project) }
|
|
let(:note_text) { %(/milestone %"#{milestone.name}") }
|
|
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
|
|
|
|
context 'on an incident' do
|
|
before do
|
|
issue.update!(work_item_type: WorkItems::Type.default_by_type(:incident))
|
|
end
|
|
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'assigns the milestone' do
|
|
expect { execute(note) }.to change { issue.reload.milestone }.from(nil).to(milestone)
|
|
end
|
|
end
|
|
|
|
context 'on a merge request' do
|
|
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
|
|
|
|
it 'leaves the note empty' do
|
|
expect(execute(note_mr)).to be_empty
|
|
end
|
|
|
|
it 'assigns the milestone' do
|
|
expect { execute(note) }.to change { issue.reload.milestone }.from(nil).to(milestone)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/remove_milestone' do
|
|
let(:issue) { create(:issue, project: project, milestone: milestone) }
|
|
let(:note_text) { '/remove_milestone' }
|
|
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
|
|
|
|
context 'on an issue' do
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'removes the milestone' do
|
|
expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
|
|
end
|
|
end
|
|
|
|
context 'on an incident' do
|
|
before do
|
|
issue.update!(work_item_type: WorkItems::Type.default_by_type(:incident))
|
|
end
|
|
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'removes the milestone' do
|
|
expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
|
|
end
|
|
end
|
|
|
|
context 'on a merge request' do
|
|
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
|
|
|
|
it 'leaves the note empty' do
|
|
expect(execute(note_mr)).to be_empty
|
|
end
|
|
|
|
it 'removes the milestone' do
|
|
expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/add_child' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
|
let_it_be(:child) { create(:work_item, :objective, project: project) }
|
|
let_it_be(:second_child) { create(:work_item, :objective, project: project) }
|
|
let_it_be(:note_text) { "/add_child #{child.to_reference}, #{second_child.to_reference}" }
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
let(:children) { [child, second_child] }
|
|
|
|
it_behaves_like 'adds child work items'
|
|
|
|
context 'when using work item full reference' do
|
|
let_it_be(:note_text) { "/add_child #{child.to_reference(full: true)}, #{second_child.to_reference(full: true)}" }
|
|
|
|
it_behaves_like 'adds child work items'
|
|
end
|
|
|
|
context 'when using work item URL' do
|
|
let_it_be(:project_path) { "#{Gitlab.config.gitlab.url}/#{project.full_path}" }
|
|
let_it_be(:url) { "#{project_path}/work_items/#{child.iid}, #{project_path}/issues/#{second_child.iid}" }
|
|
let_it_be(:note_text) { "/add_child #{url}" }
|
|
|
|
it_behaves_like 'adds child work items'
|
|
end
|
|
end
|
|
|
|
describe '/remove_child' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
|
let_it_be_with_reload(:child) { create(:work_item, :objective, project: project) }
|
|
let_it_be(:note_text) { "/remove_child #{child.to_reference}" }
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
before do
|
|
create(:parent_link, work_item_parent: noteable, work_item: child)
|
|
end
|
|
|
|
shared_examples 'removes child work item' do
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'removes child work item' do
|
|
expect { execute(note) }.to change { WorkItems::ParentLink.count }.by(-1)
|
|
|
|
expect(noteable.valid?).to be_truthy
|
|
expect(noteable.work_item_children).to be_empty
|
|
end
|
|
end
|
|
|
|
context 'when using work item reference' do
|
|
let_it_be(:note_text) { "/remove_child #{child.to_reference(full: true)}" }
|
|
|
|
it_behaves_like 'removes child work item'
|
|
end
|
|
|
|
context 'when using work item iid' do
|
|
it_behaves_like 'removes child work item'
|
|
end
|
|
|
|
context 'when using work item URL' do
|
|
let_it_be(:project_path) { "#{Gitlab.config.gitlab.url}/#{project.full_path}" }
|
|
let_it_be(:url) { "#{project_path}/work_items/#{child.iid}" }
|
|
let_it_be(:note_text) { "/remove_child #{url}" }
|
|
|
|
it_behaves_like 'removes child work item'
|
|
end
|
|
end
|
|
|
|
describe '/set_parent' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
|
let_it_be_with_reload(:parent) { create(:work_item, :objective, project: project) }
|
|
let_it_be(:note_text) { "/set_parent #{parent.to_reference}" }
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
context 'when using work item reference' do
|
|
let_it_be(:note_text) { "/set_parent #{project.full_path}#{parent.to_reference}" }
|
|
|
|
it_behaves_like 'sets work item parent'
|
|
end
|
|
|
|
context 'when using work item iid' do
|
|
let_it_be(:note_text) { "/set_parent #{parent.to_reference}" }
|
|
|
|
it_behaves_like 'sets work item parent'
|
|
end
|
|
|
|
context 'when using work item URL' do
|
|
let_it_be(:url) { "#{Gitlab.config.gitlab.url}/#{project.full_path}/work_items/#{parent.iid}" }
|
|
let_it_be(:note_text) { "/set_parent #{url}" }
|
|
|
|
it_behaves_like 'sets work item parent'
|
|
end
|
|
|
|
context 'when user has no access to the work_item' do
|
|
before do
|
|
allow(maintainer).to receive(:can?).and_call_original
|
|
allow(maintainer).to receive(:can?).with(:admin_issue_relation, noteable).and_return(false)
|
|
end
|
|
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
it 'does not assign the parent' do
|
|
expect { execute(note) }.not_to change { noteable.reload.work_item_parent }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/remove_parent' do
|
|
let_it_be_with_reload(:parent) { create(:work_item, :objective, project: project) }
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
|
let_it_be(:note_text) { "/remove_parent" }
|
|
let(:note) { create(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
before do
|
|
create(:parent_link, work_item_parent: parent, work_item: noteable)
|
|
end
|
|
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'removes work item parent' do
|
|
execute(note)
|
|
|
|
expect(noteable.valid?).to be_truthy
|
|
expect(noteable.work_item_parent).to eq(nil)
|
|
end
|
|
|
|
context 'when user has no access to the work_item' do
|
|
before do
|
|
allow(maintainer).to receive(:can?).and_call_original
|
|
allow(maintainer).to receive(:can?).with(:admin_issue_relation, noteable).and_return(false)
|
|
end
|
|
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
it 'does not assign the parent' do
|
|
expect { execute(note) }.not_to change { noteable.reload.work_item_parent }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/promote_to' do
|
|
shared_examples 'promotes work item' do |from:, to:|
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'promotes to provided type' do
|
|
expect { execute(note) }.to change { noteable.work_item_type.base_type }.from(from).to(to)
|
|
end
|
|
end
|
|
|
|
context 'when user is not allowed to promote work item' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :task, project: project) }
|
|
let_it_be(:note_text) { '/promote_to issue' }
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
before do
|
|
project.team.find_member(maintainer.id).destroy!
|
|
project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
|
|
end
|
|
|
|
it 'does not promote work item' do
|
|
expect { execute(note) }.not_to change { noteable.work_item_type.base_type }
|
|
end
|
|
end
|
|
|
|
context 'on a task' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :task, project: project) }
|
|
let_it_be(:note_text) { '/promote_to Issue' }
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
it_behaves_like 'promotes work item', from: 'task', to: 'issue'
|
|
|
|
context 'when type name is lower case' do
|
|
let_it_be(:note_text) { '/promote_to issue' }
|
|
|
|
it_behaves_like 'promotes work item', from: 'task', to: 'issue'
|
|
end
|
|
end
|
|
|
|
context 'on an issue' do
|
|
let_it_be_with_reload(:noteable) { create(:work_item, :issue, project: project) }
|
|
let_it_be(:note_text) { '/promote_to Incident' }
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
it_behaves_like 'promotes work item', from: 'issue', to: 'incident'
|
|
|
|
context 'when type name is lower case' do
|
|
let_it_be(:note_text) { '/promote_to incident' }
|
|
|
|
it_behaves_like 'promotes work item', from: 'issue', to: 'incident'
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when existing note contains quick actions' do
|
|
let(:note_text) { "foo\n/close\nbar" }
|
|
|
|
before do
|
|
note.save!
|
|
note.note = edit_note_text
|
|
end
|
|
|
|
context 'when a quick action exists in original note' do
|
|
let(:edit_note_text) { "foo\n/close\nbar\nbaz" }
|
|
|
|
it 'sanitizes/removes any quick actions and does not execute them' do
|
|
content = execute(note)
|
|
|
|
expect(content).to eq "foo\nbar\nbaz"
|
|
expect(note.noteable.open?).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'when a new quick action is used in new note' do
|
|
let(:edit_note_text) { "bar\n/react :smile:\nfoo" }
|
|
|
|
it 'executes any quick actions not in unedited note' do
|
|
content = execute(note)
|
|
|
|
expect(content).to eq "bar\nfoo"
|
|
expect(note.noteable.award_emoji.first.name).to eq 'smile'
|
|
expect(note.noteable.open?).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.supported?' do
|
|
include_context 'note on noteable'
|
|
|
|
let(:note) { create(:note_on_issue, project: project) }
|
|
|
|
context 'with a note on an issue' do
|
|
it 'returns true' do
|
|
expect(described_class.supported?(note)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'with a note on a commit' do
|
|
let(:note) { create(:note_on_commit, project: project) }
|
|
|
|
it 'returns false' do
|
|
expect(described_class.supported?(note)).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#supported?' do
|
|
include_context 'note on noteable'
|
|
|
|
it 'delegates to the class method' do
|
|
service = described_class.new(project, maintainer)
|
|
note = create(:note_on_issue, project: project)
|
|
|
|
expect(described_class).to receive(:supported?).with(note)
|
|
|
|
service.supported?(note)
|
|
end
|
|
end
|
|
|
|
describe '#execute' do
|
|
include_context 'note on noteable'
|
|
|
|
let(:service) { described_class.new(project, maintainer) }
|
|
|
|
it_behaves_like 'note on noteable that supports quick actions' do
|
|
let_it_be(:issue, reload: true) { create(:issue, project: project) }
|
|
let(:note) { build(:note_on_issue, project: project, noteable: issue) }
|
|
end
|
|
|
|
it_behaves_like 'note on noteable that supports quick actions' do
|
|
let_it_be(:incident, reload: true) { create(:incident, project: project) }
|
|
let(:note) { build(:note_on_issue, project: project, noteable: incident) }
|
|
end
|
|
|
|
it_behaves_like 'note on noteable that supports quick actions' do
|
|
let(:merge_request) { create(:merge_request, source_project: project) }
|
|
let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
|
|
end
|
|
|
|
describe '/create_merge_request' do
|
|
let(:note) { build(:note, noteable: noteable, project: project, note: note_text) }
|
|
|
|
context 'when noteable is a work item' do
|
|
let_it_be(:noteable) { create(:work_item, project: project) }
|
|
|
|
context 'when no branch name is provided' do
|
|
let(:note_text) { '/create_merge_request' }
|
|
|
|
it 'creates a merge request with default branch name', :aggregate_failures do
|
|
expect { execute(note) }.to change { MergeRequest.count }.by(1)
|
|
|
|
expect(MergeRequest.last.source_branch).to eq(noteable.to_branch_name)
|
|
end
|
|
|
|
context 'when work item type does not have the development widget' do
|
|
let_it_be(:work_item_type) { create(:work_item_type, :non_default) }
|
|
let_it_be(:noteable) { create(:work_item, project: project, work_item_type: work_item_type) }
|
|
|
|
it 'does not create a merge request' do
|
|
expect { execute(note) }.to not_change { MergeRequest.count }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when a branch name is provided' do
|
|
let(:note_text) { '/create_merge_request test-branch-1' }
|
|
|
|
it 'creates a merge request with default branch name', :aggregate_failures do
|
|
expect { execute(note) }.to change { MergeRequest.count }.by(1)
|
|
|
|
expect(MergeRequest.last.source_branch).to eq('test-branch-1')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'note on work item that supports quick actions' do
|
|
let_it_be(:work_item, reload: true) { create(:work_item, project: project) }
|
|
|
|
let(:note) { build(:note_on_work_item, project: project, noteable: work_item) }
|
|
|
|
let!(:labels) { create_pair(:label, project: project) }
|
|
|
|
before do
|
|
note.note = note_text
|
|
end
|
|
|
|
describe 'note with only command' do
|
|
describe '/close, /label & /assign' do
|
|
let(:note_text) do
|
|
%(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n)
|
|
end
|
|
|
|
it 'closes noteable, sets labels, assigns and leave no note' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable).to be_closed
|
|
expect(note.noteable.labels).to match_array(labels)
|
|
expect(note.noteable.assignees).to eq([assignee])
|
|
end
|
|
end
|
|
|
|
describe '/reopen' do
|
|
before do
|
|
note.noteable.close!
|
|
expect(note.noteable).to be_closed
|
|
end
|
|
|
|
let(:note_text) { '/reopen' }
|
|
|
|
it 'opens the noteable, and leave no note' do
|
|
content = execute(note)
|
|
|
|
expect(content).to be_empty
|
|
expect(note.noteable).to be_open
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'note with command & text' do
|
|
describe '/close, /label, /assign' do
|
|
let(:note_text) do
|
|
%(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\nWORLD)
|
|
end
|
|
|
|
it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
|
|
content = execute(note)
|
|
|
|
expect(content).to eq "HELLO\nWORLD"
|
|
expect(note.noteable).to be_closed
|
|
expect(note.noteable.labels).to match_array(labels)
|
|
expect(note.noteable.assignees).to eq([assignee])
|
|
end
|
|
end
|
|
|
|
describe '/reopen' do
|
|
before do
|
|
note.noteable.close
|
|
expect(note.noteable).to be_closed
|
|
end
|
|
|
|
let(:note_text) { "HELLO\n/reopen\nWORLD" }
|
|
|
|
it 'opens the noteable' do
|
|
content = execute(note)
|
|
|
|
expect(content).to eq "HELLO\nWORLD"
|
|
expect(note.noteable).to be_open
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '/subscribe or /unsubscribe' do
|
|
shared_examples 'when applying to work_item' do
|
|
it 'leaves the note empty' do
|
|
expect(execute(note)).to be_empty
|
|
end
|
|
|
|
it 'triggers work item updated subscription' do
|
|
expect(GraphqlTriggers).to receive(:work_item_updated).with(work_item)
|
|
|
|
execute(note)
|
|
end
|
|
end
|
|
|
|
describe '/subscribe' do
|
|
let_it_be(:note_text) { '/subscribe' }
|
|
|
|
it_behaves_like 'when applying to work_item'
|
|
end
|
|
|
|
describe '/unsubscribe' do
|
|
let_it_be(:note_text) { '/unsubscribe' }
|
|
|
|
before do
|
|
work_item.subscribe(maintainer, project)
|
|
end
|
|
|
|
it_behaves_like 'when applying to work_item'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#apply_updates' do
|
|
include_context 'note on noteable'
|
|
|
|
let_it_be_with_reload(:issue) { create(:issue, project: project) }
|
|
let_it_be_with_reload(:work_item) { create(:work_item, :issue, project: project) }
|
|
let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project) }
|
|
let_it_be_with_reload(:issue_note) { create(:note_on_issue, project: project, noteable: issue) }
|
|
let_it_be_with_reload(:work_item_note) { create(:note, project: project, noteable: work_item) }
|
|
let_it_be_with_reload(:mr_note) { create(:note_on_merge_request, project: project, noteable: merge_request) }
|
|
let_it_be_with_reload(:commit_note) { create(:note_on_commit, project: project) }
|
|
let(:update_params) { {} }
|
|
|
|
subject(:apply_updates) { described_class.new(project, maintainer).apply_updates(update_params, note) }
|
|
|
|
context 'with a note on an issue' do
|
|
let(:note) { issue_note }
|
|
|
|
it 'returns successful service response if update returned no errors' do
|
|
update_params[:confidential] = true
|
|
expect(apply_updates.success?).to be true
|
|
end
|
|
|
|
it 'returns service response with errors if update failed' do
|
|
update_params[:title] = ""
|
|
expect(apply_updates.success?).to be false
|
|
expect(apply_updates.message).to include("Title can't be blank")
|
|
end
|
|
end
|
|
|
|
context 'with a note on a merge request' do
|
|
let(:note) { mr_note }
|
|
|
|
it 'returns successful service response if update returned no errors' do
|
|
update_params[:title] = 'New title'
|
|
expect(apply_updates.success?).to be true
|
|
end
|
|
|
|
it 'returns service response with errors if update failed' do
|
|
update_params[:title] = ""
|
|
expect(apply_updates.success?).to be false
|
|
expect(apply_updates.message).to include("Title can't be blank")
|
|
end
|
|
end
|
|
|
|
context 'with a note on a work item' do
|
|
let(:note) { work_item_note }
|
|
|
|
before do
|
|
update_params[:confidential] = true
|
|
end
|
|
|
|
it 'returns successful service response if update returned no errors' do
|
|
expect(apply_updates.success?).to be true
|
|
end
|
|
|
|
it 'returns service response with errors if update failed' do
|
|
task = create(:work_item, :task, project: project)
|
|
create(:parent_link, work_item: task, work_item_parent: work_item)
|
|
|
|
expect(apply_updates.success?).to be false
|
|
expect(apply_updates.message).to include(
|
|
"A confidential issue must have only confidential children. Make any child items confidential and try again."
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'with a note on a commit' do
|
|
let(:note) { commit_note }
|
|
|
|
it 'returns successful service response if update returned no errors' do
|
|
update_params[:tag_name] = 'test'
|
|
expect(apply_updates.success?).to be true
|
|
end
|
|
|
|
it 'returns service response with errors if update failed' do
|
|
update_params[:tag_name] = '-test'
|
|
expect(apply_updates.success?).to be false
|
|
expect(apply_updates.message).to include('Tag name invalid')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'CE restriction for issue assignees' do
|
|
describe '/assign' do
|
|
let_it_be(:assignee) { create(:user) }
|
|
let_it_be(:maintainer) { create(:user) }
|
|
let(:service) { described_class.new(project, maintainer) }
|
|
let(:note) { create(:note_on_issue, note: note_text, project: project) }
|
|
|
|
let(:note_text) do
|
|
%(/assign @#{assignee.username} @#{maintainer.username}\n")
|
|
end
|
|
|
|
before_all do
|
|
project.add_maintainer(maintainer)
|
|
project.add_maintainer(assignee)
|
|
end
|
|
|
|
before do
|
|
stub_licensed_features(multiple_issue_assignees: false)
|
|
end
|
|
|
|
it 'adds only one assignee from the list' do
|
|
execute(note)
|
|
|
|
expect(note.noteable.assignees.count).to eq(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
def execute(note)
|
|
content, update_params = service.execute(note)
|
|
service.apply_updates(update_params, note)
|
|
|
|
content
|
|
end
|
|
end
|