Skip to content

Commit 0293662

Browse files
authored
Merge pull request #195 from alphagov/2424-add-put-conversation-endpoint
Add PUT conversation endpoint
2 parents a935489 + 795ddcb commit 0293662

5 files changed

Lines changed: 131 additions & 53 deletions

File tree

app/controllers/api/v0/conversations_controller.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
class Api::V0::ConversationsController < ApplicationController
22
before_action { authorise_user!(SignonUser::Permissions::CONVERSATION_API) }
3-
before_action :find_conversation, only: %i[show answer answer_feedback]
3+
before_action :find_conversation, only: %i[show update answer answer_feedback]
44

55
def create
66
conversation = Conversation.new
7-
8-
form = Form::CreateQuestion.new(conversation:, user_question: question_params[:user_question])
7+
form = Form::CreateQuestion.new(question_params.merge(conversation:))
98

109
if form.valid?
1110
question = form.submit
1211
render json: QuestionBlueprint.render(question, view: :pending), status: :created
1312
else
14-
render json: ValidationErrorBlueprint.render(message: "Could not create question", errors: form.errors.messages), status: :unprocessable_entity
13+
render json: ValidationErrorBlueprint.render(errors: form.errors.messages), status: :unprocessable_entity
1514
end
1615
end
1716

@@ -25,6 +24,20 @@ def show
2524
)
2625
end
2726

27+
def update
28+
form = Form::CreateQuestion.new(question_params.merge(conversation: @conversation))
29+
30+
if form.valid?
31+
question = form.submit
32+
33+
render json: QuestionBlueprint.render(question, view: :pending), status: :created
34+
else
35+
render json: ValidationErrorBlueprint.render(
36+
errors: form.errors.messages,
37+
), status: :unprocessable_entity
38+
end
39+
end
40+
2841
def answer
2942
question = @conversation.questions.find(params[:question_id])
3043
answer = question.answer

app/models/form/create_question.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class Form::CreateQuestion
99
USER_QUESTION_PRESENCE_ERROR_MESSAGE = "Ask a question. For example, 'how do I register for VAT?'".freeze
1010
USER_QUESTION_LENGTH_MAXIMUM = 300
1111
USER_QUESTION_LENGTH_ERROR_MESSAGE = "Question must be %{count} characters or less".freeze
12+
USER_QUESTION_PII_ERROR_MESSAGE = "Personal data has been detected in your question. Please remove it and try asking again.".freeze
13+
QUESTION_LIMIT_REACHED_MESSAGE = "You’ve reached the message limit for the GOV.UK Chat trial. You have no messages left.".freeze
14+
UNANSWERED_QUESTION_ERROR_MESSAGE = "Previous question pending. Please wait for a response".freeze
1215

1316
before_validation :sanitise_user_question
1417

@@ -51,22 +54,20 @@ def sanitise_user_question
5154

5255
def all_questions_answered?
5356
if conversation.questions.unanswered.exists?
54-
errors.add(:base, "Previous question pending. Please wait for a response")
57+
errors.add(:base, UNANSWERED_QUESTION_ERROR_MESSAGE)
5558
end
5659
end
5760

5861
def no_pii_present?
5962
if PiiValidator.invalid?(user_question)
60-
error_message = "Personal data has been detected in your question. Please remove it and try asking again."
61-
62-
errors.add(:user_question, error_message)
63+
errors.add(:user_question, USER_QUESTION_PII_ERROR_MESSAGE)
6364
end
6465
end
6566

6667
def within_question_limit?
6768
return if conversation.user.nil?
6869
return unless conversation.user.question_limit_reached?
6970

70-
errors.add(:base, "You’ve reached the message limit for the GOV.UK Chat trial. You have no messages left.")
71+
errors.add(:base, QUESTION_LIMIT_REACHED_MESSAGE)
7172
end
7273
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
namespace :v0 do
148148
post "/conversation", to: "conversations#create", as: :create_conversation
149149
get "/conversation/:conversation_id", to: "conversations#show", as: :show_conversation
150+
put "/conversation/:conversation_id", to: "conversations#update", as: :update_conversation
150151
get "/conversation/:conversation_id/questions/:question_id/answer", to: "conversations#answer", as: :answer_question
151152
post "/conversation/:conversation_id/answers/:answer_id/feedback", to: "conversations#answer_feedback", as: :answer_feedback
152153
end

spec/models/form/create_question_spec.rb

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
)
2121
form.validate
2222

23-
expect(form.errors.messages[:user_question]).to eq(["Question must be 300 characters or less"])
23+
expect(form.errors.messages[:user_question])
24+
.to eq(
25+
[
26+
sprintf(
27+
described_class::USER_QUESTION_LENGTH_ERROR_MESSAGE,
28+
count: described_class::USER_QUESTION_LENGTH_MAXIMUM,
29+
),
30+
],
31+
)
2432
end
2533

2634
it "is invalid when user_question is blank" do
@@ -30,7 +38,7 @@
3038
)
3139
form.validate
3240

33-
expect(form.errors.messages[:user_question]).to eq(["Ask a question. For example, 'how do I register for VAT?'"])
41+
expect(form.errors.messages[:user_question]).to eq([described_class::USER_QUESTION_PRESENCE_ERROR_MESSAGE])
3442
end
3543

3644
it "is invalid when the conversation passed in has an unanswered question" do
@@ -39,22 +47,18 @@
3947
form = described_class.new(conversation:, user_question:)
4048
form.validate
4149

42-
expect(form.errors.messages[:base]).to eq(["Previous question pending. Please wait for a response"])
50+
expect(form.errors.messages[:base]).to eq([described_class::UNANSWERED_QUESTION_ERROR_MESSAGE])
4351
end
4452

4553
describe "personally identifiable information (pii) validation" do
46-
let(:pii_error_message) do
47-
"Personal data has been detected in your question. Please remove it and try asking again."
48-
end
49-
5054
it "adds an error message when pii is present" do
5155
form = described_class.new(
5256
conversation:,
5357
user_question: "My email address is email@gmail.com",
5458
)
5559
form.validate
5660

57-
expect(form.errors.messages[:user_question]).to eq([pii_error_message])
61+
expect(form.errors.messages[:user_question]).to eq([described_class::USER_QUESTION_PII_ERROR_MESSAGE])
5862
end
5963

6064
it "doesn't add an error message when no pii is present" do
@@ -68,13 +72,11 @@
6872
end
6973

7074
describe "within question limit validation" do
71-
let(:question_limit_error_message) { "You’ve reached the message limit for the GOV.UK Chat trial. You have no messages left." }
72-
7375
it "adds a error message if over the question limit" do
7476
conversation.user = build(:early_access_user, questions_count: 2, individual_question_limit: 1)
7577
form = described_class.new(conversation:, user_question:)
7678
form.validate
77-
expect(form.errors.messages[:base]).to eq([question_limit_error_message])
79+
expect(form.errors.messages[:base]).to eq([described_class::QUESTION_LIMIT_REACHED_MESSAGE])
7880
end
7981

8082
it "is valid when under the question limit" do

spec/requests/api/v0/conversations_spec.rb

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,55 @@
66
login_as(create(:signon_user, :conversation_api))
77
end
88

9-
shared_examples "responds with forbidden if user doesn't have conversation-api permission" do |path, method|
10-
let(:route_params) { {} }
11-
let(:params) { {} }
12-
13-
describe "responds with forbidden if user doesn't have conversation-api permission" do
14-
it "returns with 403 for #{path}" do
15-
login_as(create(:signon_user))
16-
process(method.to_sym, public_send(path.to_sym, *route_params), params:, as: :json)
9+
shared_examples "responds with forbidden if user doesn't have conversation-api permission" do |routes:|
10+
before { login_as(create(:signon_user)) }
11+
12+
routes.each do |route|
13+
describe "responds with forbidden if user doesn't have conversation-api permission" do
14+
it "returns with 403 for #{route[:method]} #{route[:path]}" do
15+
process(
16+
route[:method.to_sym],
17+
public_send(route[:path].to_sym, *(route[:route_params] || [])),
18+
params: route[:params] || {},
19+
as: :json,
20+
)
1721

18-
expect(response).to have_http_status(:forbidden)
22+
expect(response).to have_http_status(:forbidden)
23+
end
1924
end
2025
end
2126
end
2227

2328
it_behaves_like "responds with forbidden if user doesn't have conversation-api permission",
24-
:api_v0_create_conversation_path,
25-
:post do
26-
let(:route_params) { [] }
27-
let(:params) { { user_question: "" } }
28-
end
29-
30-
it_behaves_like "responds with forbidden if user doesn't have conversation-api permission",
31-
:api_v0_show_conversation_path,
32-
:get do
33-
let(:route_params) { [SecureRandom.uuid] }
34-
end
35-
36-
it_behaves_like "responds with forbidden if user doesn't have conversation-api permission",
37-
:api_v0_answer_question_path,
38-
:get do
39-
let(:route_params) { [SecureRandom.uuid, SecureRandom.uuid] }
40-
end
41-
42-
it_behaves_like "responds with forbidden if user doesn't have conversation-api permission",
43-
:api_v0_answer_feedback_path,
44-
:post do
45-
let(:route_params) { [SecureRandom.uuid, SecureRandom.uuid] }
46-
let(:params) { { useful: true } }
47-
end
29+
routes: [
30+
{
31+
path: :api_v0_create_conversation_path,
32+
method: :post,
33+
params: { user_question: "question" },
34+
},
35+
{
36+
path: :api_v0_show_conversation_path,
37+
method: :get,
38+
route_params: [SecureRandom.uuid],
39+
},
40+
{
41+
path: :api_v0_update_conversation_path,
42+
method: :put,
43+
route_params: [SecureRandom.uuid],
44+
params: { user_question: "question" },
45+
},
46+
{
47+
path: :api_v0_answer_question_path,
48+
method: :get,
49+
route_params: [SecureRandom.uuid, SecureRandom.uuid],
50+
},
51+
{
52+
path: :api_v0_answer_feedback_path,
53+
method: :post,
54+
route_params: [SecureRandom.uuid, SecureRandom.uuid],
55+
params: { useful: true },
56+
},
57+
]
4858

4959
describe "middleware ensures adherance to the OpenAPI specification" do
5060
context "when the response returned does not conform to the OpenAPI specification" do
@@ -141,6 +151,57 @@
141151
end
142152
end
143153

154+
describe "PUT :update" do
155+
context "when the params are valid" do
156+
let(:user_question) { "What is the capital of France?" }
157+
let(:params) { { user_question: } }
158+
159+
it "returns a created status" do
160+
put api_v0_update_conversation_path(conversation), params:, as: :json
161+
expect(response).to have_http_status(:created)
162+
end
163+
164+
it "creates a question on the conversation" do
165+
expect {
166+
put api_v0_update_conversation_path(conversation), params:, as: :json
167+
}.to change(conversation.questions, :count).by(1)
168+
expect(conversation.questions.strict_loading(false).last.message)
169+
.to eq(user_question)
170+
end
171+
172+
it "returns the expected JSON" do
173+
put api_v0_update_conversation_path(conversation), params:, as: :json
174+
175+
expected_response = QuestionBlueprint.render_as_json(
176+
conversation.questions.strict_loading(false).last,
177+
view: :pending,
178+
)
179+
expect(JSON.parse(response.body)).to eq(expected_response)
180+
end
181+
end
182+
183+
context "when the params are invalid" do
184+
let(:params) { { user_question: "" } }
185+
186+
it "returns an unprocessable_entity status" do
187+
put api_v0_update_conversation_path(conversation), params:, as: :json
188+
expect(response).to have_http_status(:unprocessable_entity)
189+
end
190+
191+
it "returns the correct expected JSON" do
192+
put api_v0_update_conversation_path(conversation), params:, as: :json
193+
194+
expect(JSON.parse(response.body))
195+
.to eq(
196+
{
197+
"message" => "Unprocessable entity",
198+
"errors" => { "user_question" => [Form::CreateQuestion::USER_QUESTION_PRESENCE_ERROR_MESSAGE] },
199+
},
200+
)
201+
end
202+
end
203+
end
204+
144205
describe "GET :answer" do
145206
context "when an answer has been generated for the question" do
146207
let!(:answer) { create(:answer, question:) }

0 commit comments

Comments
 (0)