Skip to content

Commit f1423de

Browse files
committed
Validate requests with Committee middleware
Committee provides schema validation for requests. When invalid it returns a 400 with an error message outlining the issue with the request body. Here's the example response shown in the Committee documentation: { "id":"invalid_response", "message":"Missing keys in response: archived_at, owner:email, owner:id" } There is an option to build your own validation errors https://github.com/interagent/committee?tab=readme-ov-file#validation-errors. This commit adds the Committee middleware to the API, and configures it to use the OpenAPI schema. It also adds a custom error class for request which simply strips the id from the JSON in the response body. It also adds a test to ensure that the middleware is working as expected for the POST feedback endpoint.
1 parent 2a59987 commit f1423de

4 files changed

Lines changed: 73 additions & 49 deletions

File tree

config/initializers/committee.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
require "committee"
2+
require "api/request_validation_error"
3+
4+
Rails.application.config.middleware.use Committee::Middleware::RequestValidation,
5+
schema_path: "docs/api_openapi_specification.yml",
6+
coerce_date_times: true,
7+
prefix: "/api/v0",
8+
strict_reference_validation: true,
9+
error_class: Api::RequestValidationError
210

311
Rails.application.config.middleware.use Committee::Middleware::ResponseValidation,
412
schema_path: "docs/api_openapi_specification.yml",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Api
2+
class RequestValidationError < Committee::ValidationError
3+
def error_body
4+
GenericErrorBlueprint.render_as_hash(message:)
5+
end
6+
end
7+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
RSpec.describe Api::RequestValidationError do
2+
describe "#render" do
3+
it "returns an array which uses the GenericErrorBlueprint for the error body" do
4+
request = double
5+
error_message = "Bad request"
6+
generic_error_blueprint = GenericErrorBlueprint.render(message: error_message)
7+
8+
error = described_class.new(400, :bad_request, error_message, request)
9+
expect(error.render).to eq([400, { "Content-Type" => "application/json" }, [generic_error_blueprint]])
10+
end
11+
end
12+
end

spec/requests/api/v0/conversations_spec.rb

Lines changed: 46 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@
5050
expect(JSON.parse(response.body)).to eq({ "message" => "Internal server error" })
5151
end
5252
end
53+
54+
context "when the request does not conform to the OpenAPI specification" do
55+
it "returns a bad request status code" do
56+
post api_v0_answer_feedback_path(conversation_id: conversation.id, answer_id: question.id),
57+
params: { useful: "not a boolean" },
58+
as: :json
59+
60+
expect(response).to have_http_status(:bad_request)
61+
end
62+
63+
it "returns the correct JSON in the body" do
64+
post api_v0_answer_feedback_path(conversation_id: conversation.id, answer_id: question.id),
65+
params: { useful: "not a boolean" },
66+
as: :json
67+
expect(JSON.parse(response.body)).to match({ "message" => /useful expected boolean, but received String: "not a boolean"/ })
68+
end
69+
end
5370
end
5471

5572
describe "GET :show" do
@@ -66,7 +83,7 @@
6683
end
6784

6885
it "returns a 404 if the conversation cannot be found" do
69-
get api_v0_show_conversation_path(-1)
86+
get api_v0_show_conversation_path(SecureRandom.uuid)
7087

7188
expect(response).to have_http_status(:not_found)
7289
end
@@ -77,12 +94,12 @@
7794
let!(:answer) { create(:answer, question:) }
7895

7996
it "returns a success status" do
80-
get api_v0_answer_question_path(conversation, question)
97+
get api_v0_answer_question_path(conversation, question), as: :json
8198
expect(response).to have_http_status(:ok)
8299
end
83100

84101
it "returns the expected JSON" do
85-
get api_v0_answer_question_path(conversation, question)
102+
get api_v0_answer_question_path(conversation, question), as: :json
86103

87104
eager_loaded_answer = Answer.includes(:sources, :feedback).find(answer.id)
88105
expected_response = AnswerBlueprint.render_as_json(eager_loaded_answer)
@@ -92,7 +109,7 @@
92109
it "returns the correct JSON for answer sources" do
93110
source = create(:answer_source, answer:)
94111

95-
get api_v0_answer_question_path(conversation, question)
112+
get api_v0_answer_question_path(conversation, question), as: :json
96113

97114
expect(JSON.parse(response.body)["sources"])
98115
.to eq([{ url: source.url, title: "#{source.title}: #{source.heading}" }.as_json])
@@ -101,81 +118,61 @@
101118

102119
context "when an answer has not been generated for the question" do
103120
it "returns an accepted status" do
104-
get api_v0_answer_question_path(conversation, question)
121+
get api_v0_answer_question_path(conversation, question), as: :json
105122
expect(response).to have_http_status(:accepted)
106123
end
107124

108125
it "returns an empty JSON response" do
109-
get api_v0_answer_question_path(conversation, question)
126+
get api_v0_answer_question_path(conversation, question), as: :json
110127
expect(JSON.parse(response.body)).to eq({})
111128
end
112129
end
113130
end
114131

115132
describe "POST :answer_feedback" do
116-
context "when the params are valid" do
117-
let!(:answer) { create(:answer, question:) }
133+
let!(:answer) { create(:answer, question:) }
118134

119-
context "and the answer has no feedback" do
120-
it "returns a created status" do
121-
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }
122-
expect(response).to have_http_status(:created)
123-
end
124-
125-
it "returns an empty JSON" do
126-
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }
127-
128-
expect(JSON.parse(response.body)).to eq({})
129-
end
130-
131-
it "creates feedback for the answer" do
132-
expect {
133-
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }
134-
}.to change(AnswerFeedback, :count).by(1)
135-
136-
answer_feedback = AnswerFeedback.includes(:answer).last
137-
expect(answer_feedback.answer).to eq(answer)
138-
expect(answer_feedback.useful).to be true
139-
end
135+
context "when the answer has no feedback" do
136+
it "returns a created status" do
137+
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }, as: :json
138+
expect(response).to have_http_status(:created)
140139
end
141140

142-
context "and an answer already has feedback" do
143-
before do
144-
create(:answer_feedback, answer:)
145-
end
141+
it "returns an empty JSON" do
142+
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }, as: :json
146143

147-
it "returns an unprocessable_entity status" do
148-
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }, as: :json
149-
expect(response).to have_http_status(:unprocessable_entity)
150-
end
144+
expect(JSON.parse(response.body)).to eq({})
145+
end
151146

152-
it "returns the correct expected JSON" do
147+
it "creates feedback for the answer" do
148+
expect {
153149
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }, as: :json
150+
}.to change(AnswerFeedback, :count).by(1)
154151

155-
expect(JSON.parse(response.body))
156-
.to eq({ "message" => "Unprocessable entity", "errors" => { "base" => ["Feedback already provided for this answer"] } })
157-
end
152+
answer_feedback = AnswerFeedback.includes(:answer).last
153+
expect(answer_feedback.answer).to eq(answer)
154+
expect(answer_feedback.useful).to be true
158155
end
159156
end
160157

161-
context "when the params are invalid" do
162-
it "returns an unprocessable_entity status" do
163-
answer = create(:answer, question:)
158+
context "when an answer already has feedback" do
159+
before do
160+
create(:answer_feedback, answer:)
161+
end
164162

165-
post api_v0_answer_feedback_path(conversation, answer)
163+
it "returns an unprocessable_entity status" do
164+
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }, as: :json
166165
expect(response).to have_http_status(:unprocessable_entity)
167166
end
168167

169168
it "returns the correct expected JSON" do
170-
answer = create(:answer, question:)
171-
172-
post api_v0_answer_feedback_path(conversation, answer)
169+
post api_v0_answer_feedback_path(conversation, answer), params: { useful: true }, as: :json
173170

174171
expect(JSON.parse(response.body))
175172
.to eq(
176173
{
177174
"message" => "Unprocessable entity",
178-
"errors" => { "useful" => ["Useful must be true or false"] },
175+
"errors" => { "base" => ["Feedback already provided for this answer"] },
179176
},
180177
)
181178
end

0 commit comments

Comments
 (0)