Commit af161ca2 authored by Alessio Caiazza's avatar Alessio Caiazza

Merge branch...

Merge branch '35627-api-response-for-adding-a-note-returns-http-400-for-command-only-notes' into 'master'

API requests creating command only notes should return 2xx responses

Closes #35627

See merge request gitlab-org/gitlab!19624
parents 55dcfa3e b96ca8d9
...@@ -17,57 +17,72 @@ module Notes ...@@ -17,57 +17,72 @@ module Notes
# We execute commands (extracted from `params[:note]`) on the noteable # We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands # **before** we save the note because if the note consists of commands
# only, there is no need be create a note! # only, there is no need be create a note!
quick_actions_service = QuickActionsService.new(project, current_user)
if quick_actions_service.supported?(note) execute_quick_actions(note) do |only_commands|
content, update_params, message = quick_actions_service.execute(note, quick_action_options) note.run_after_commit do
# Finish the harder work in the background
NewNoteWorker.perform_async(note.id)
end
only_commands = content.empty? note_saved = note.with_transaction_returning_status do
!only_commands && note.save
end
note.note = content when_saved(note) if note_saved
end end
note.run_after_commit do note
# Finish the harder work in the background end
NewNoteWorker.perform_async(note.id)
end
note_saved = note.with_transaction_returning_status do private
!only_commands && note.save
end
if note_saved def execute_quick_actions(note)
if note.part_of_discussion? && note.discussion.can_convert_to_discussion? return yield(false) unless quick_actions_service.supported?(note)
note.discussion.convert_to_discussion!(save: true)
end
todo_service.new_note(note, current_user) content, update_params, message = quick_actions_service.execute(note, quick_action_options)
clear_noteable_diffs_cache(note) only_commands = content.empty?
Suggestions::CreateService.new(note).execute note.note = content
increment_usage_counter(note)
if Feature.enabled?(:notes_create_service_tracking, project) yield(only_commands)
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
end
end
if quick_actions_service.commands_executed_count.to_i > 0 do_commands(note, update_params, message, only_commands)
if update_params.present? end
quick_actions_service.apply_updates(update_params, note)
note.commands_changes = update_params
end
# We must add the error after we call #save because errors are reset def quick_actions_service
# when #save is called @quick_actions_service ||= QuickActionsService.new(project, current_user)
if only_commands end
note.errors.add(:commands_only, message.presence || _('Failed to apply commands.'))
end def when_saved(note)
if note.part_of_discussion? && note.discussion.can_convert_to_discussion?
note.discussion.convert_to_discussion!(save: true)
end end
note todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
increment_usage_counter(note)
if Feature.enabled?(:notes_create_service_tracking, project)
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
end
end end
private def do_commands(note, update_params, message, only_commands)
return if quick_actions_service.commands_executed_count.to_i.zero?
if update_params.present?
quick_actions_service.apply_updates(update_params, note)
note.commands_changes = update_params
end
# We must add the error after we call #save because errors are reset
# when #save is called
if only_commands
note.errors.add(:commands_only, message.presence || _('Failed to apply commands.'))
# Allow consumers to detect problems applying commands
note.errors.add(:commands, _('Failed to apply commands.')) unless message.present?
end
end
# EE::Notes::CreateService would override this method # EE::Notes::CreateService would override this method
def quick_action_options def quick_action_options
......
...@@ -55,6 +55,8 @@ module Notes ...@@ -55,6 +55,8 @@ module Notes
# We must add the error after we call #save because errors are reset # We must add the error after we call #save because errors are reset
# when #save is called # when #save is called
note.errors.add(:commands_only, message.presence || _('Commands did not apply')) note.errors.add(:commands_only, message.presence || _('Commands did not apply'))
# Allow consumers to detect problems applying commands
note.errors.add(:commands, _('Commands did not apply')) unless message.present?
Notes::DestroyService.new(project, current_user).execute(note) Notes::DestroyService.new(project, current_user).execute(note)
end end
......
---
title: Return 202 for command only notes in REST API
merge_request: 19624
author:
type: fixed
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe API::Notes do describe API::Notes do
let(:user) { create(:user) } let!(:user) { create(:user) }
let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:project) { create(:project, :public) }
let(:private_user) { create(:user) } let(:private_user) { create(:user) }
before do before do
......
...@@ -25,6 +25,14 @@ module API ...@@ -25,6 +25,14 @@ module API
# Avoid N+1 queries as much as possible # Avoid N+1 queries as much as possible
expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) } expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
expose(:commands_changes) { |note| note.commands_changes || {} }
end
# To be returned if the note was command-only
class NoteCommands < Grape::Entity
expose(:commands_changes) { |note| note.commands_changes || {} }
expose(:summary) { |note| note.errors[:commands_only] }
end end
end end
end end
...@@ -113,6 +113,7 @@ module API ...@@ -113,6 +113,7 @@ module API
end end
def create_note(noteable, opts) def create_note(noteable, opts)
whitelist_query_limiting
authorize!(:create_note, noteable) authorize!(:create_note, noteable)
parent = noteable_parent(noteable) parent = noteable_parent(noteable)
...@@ -139,6 +140,10 @@ module API ...@@ -139,6 +140,10 @@ module API
present discussion, with: Entities::Discussion present discussion, with: Entities::Discussion
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/211538')
end
end end
end end
end end
......
...@@ -82,9 +82,13 @@ module API ...@@ -82,9 +82,13 @@ module API
note = create_note(noteable, opts) note = create_note(noteable, opts)
if note.valid? if note.errors.keys == [:commands_only]
status 202
present note, with: Entities::NoteCommands
elsif note.valid?
present note, with: Entities.const_get(note.class.name, false) present note, with: Entities.const_get(note.class.name, false)
else else
note.errors.delete(:commands_only) if note.errors.has_key?(:commands)
bad_request!("Note #{note.errors.messages}") bad_request!("Note #{note.errors.messages}")
end end
end end
......
...@@ -54,7 +54,8 @@ ...@@ -54,7 +54,8 @@
"cached_markdown_version": { "type": "integer" }, "cached_markdown_version": { "type": "integer" },
"human_access": { "type": ["string", "null"] }, "human_access": { "type": ["string", "null"] },
"toggle_award_path": { "type": "string" }, "toggle_award_path": { "type": "string" },
"path": { "type": "string" } "path": { "type": "string" },
"commands_changes": { "type": "object", "additionalProperties": true }
}, },
"required": [ "required": [
"id", "attachment", "author", "created_at", "updated_at", "id", "attachment", "author", "created_at", "updated_at",
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"commands_changes": { "type": "object", "additionalProperties": true },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"updated_at": { "type": "date" }, "updated_at": { "type": "date" },
"system": { "type": "boolean" }, "system": { "type": "boolean" },
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe API::Notes do describe API::Notes do
let(:user) { create(:user) } let!(:user) { create(:user) }
let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:project) { create(:project, :public) }
let(:private_user) { create(:user) } let(:private_user) { create(:user) }
before do before do
...@@ -226,14 +226,56 @@ describe API::Notes do ...@@ -226,14 +226,56 @@ describe API::Notes do
let(:note) { merge_request_note } let(:note) { merge_request_note }
end end
let(:request_body) { 'Hi!' }
let(:request_path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes" }
subject { post api(request_path, user), params: { body: request_body } }
context 'a command only note' do
let(:assignee) { create(:user) }
let(:request_body) { "/assign #{assignee.to_reference}" }
before do
project.add_developer(assignee)
project.add_developer(user)
end
it 'returns 202 Accepted status' do
subject
expect(response).to have_gitlab_http_status(:accepted)
end
it 'does not actually create a new note' do
expect { subject }.not_to change { Note.where(system: false).count }
end
it 'does however create a system note about the change' do
expect { subject }.to change { Note.system.count }.by(1)
end
it 'applies the commands' do
expect { subject }.to change { merge_request.reset.assignees }
end
it 'reports the changes' do
subject
expect(json_response).to include(
'commands_changes' => include(
'assignee_ids' => [Integer]
),
'summary' => include("Assigned #{assignee.to_reference}.")
)
end
end
context 'when the merge request discussion is locked' do context 'when the merge request discussion is locked' do
before do before do
merge_request.update_attribute(:discussion_locked, true) merge_request.update_attribute(:discussion_locked, true)
end end
context 'when a user is a team member' do context 'when a user is a team member' do
subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), params: { body: 'Hi!' } }
it 'returns 200 status' do it 'returns 200 status' do
subject subject
......
...@@ -172,6 +172,8 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| ...@@ -172,6 +172,8 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
if parent_type == 'projects' if parent_type == 'projects'
context 'by a project owner' do context 'by a project owner' do
let(:user) { project.owner }
it 'sets the creation time on the new note' do it 'sets the creation time on the new note' do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
......
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