Skip to content

Commit 78fe9e0

Browse files
committed
Implement ProfileApiClient.create_school_student
TODO: Better support for 422 responses from Profile API - at the very least for the `ERR_USER_EXISTS` error.
1 parent 977fdbd commit 78fe9e0

File tree

7 files changed

+117
-16
lines changed

7 files changed

+117
-16
lines changed

lib/concepts/school_student/create.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ def call(school:, school_student_params:, token:)
1616
private
1717

1818
def create_student(school, school_student_params, token)
19-
organisation_id = school.id
2019
username = school_student_params.fetch(:username)
2120
password = school_student_params.fetch(:password)
2221
name = school_student_params.fetch(:name)
2322

2423
validate(school:, username:, password:, name:)
2524

26-
ProfileApiClient.create_school_student(token:, username:, password:, name:, organisation_id:)
25+
response = ProfileApiClient.create_school_student(token:, username:, password:, name:, school:)
26+
response[:created].each do |user_id|
27+
Role.student.create!(school:, user_id:)
28+
end
2729
end
2830

2931
def validate(school:, username:, password:, name:)

lib/concepts/school_student/create_batch.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def create_batch(school, uploaded_file, token)
2323
validate(school:, sheet:)
2424

2525
non_header_rows_with_content(sheet:).each do |name, username, password|
26-
ProfileApiClient.create_school_student(token:, username:, password:, name:, organisation_id: school.id)
26+
ProfileApiClient.create_school_student(token:, username:, password:, name:, school:)
2727
end
2828
end
2929

lib/profile_api_client.rb

+20-9
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,30 @@ def list_school_students(token:, organisation_id:)
147147
# The API should respond:
148148
# - 404 Not Found if the user doesn't exist
149149
# - 422 Unprocessable if the constraints are not met
150-
def create_school_student(token:, username:, password:, name:, organisation_id:)
150+
# rubocop:disable Metrics/AbcSize
151+
def create_school_student(token:, username:, password:, name:, school:)
151152
return nil if token.blank?
152153

153-
_ = username
154-
_ = password
155-
_ = name
156-
_ = organisation_id
154+
response = connection.post("/api/v1/schools/#{school.id}/students") do |request|
155+
apply_default_headers(request, token)
156+
request.body = [{
157+
name:,
158+
username:,
159+
password:
160+
}].to_json
161+
end
157162

158-
# TODO: We should make Faraday raise a Ruby error for a non-2xx status
159-
# code so that SchoolStudent::Create propagates the error in the response.
160-
response = {}
161-
response.deep_symbolize_keys
163+
if response.status == 422
164+
error_code = JSON.parse(response.body)['errors'].first['error']
165+
message = error_code == 'ERR_USER_EXISTS' ? 'Username already exists' : "Unknown error code: #{error_code}"
166+
raise "Student not created in Profile API. HTTP response code 422. #{message}"
167+
end
168+
169+
raise "Student not created in Profile API. HTTP response code: #{response.status}" unless response.status == 201
170+
171+
JSON.parse(response.body).deep_symbolize_keys
162172
end
173+
# rubocop:enable Metrics/AbcSize
163174

164175
# The API should enforce these constraints:
165176
# - The token has the school-owner or school-teacher role for the given organisation ID

spec/concepts/school_student/create_batch_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121

2222
# TODO: Replace with WebMock assertion once the profile API has been built.
2323
expect(ProfileApiClient).to have_received(:create_school_student)
24-
.with(token:, username: 'jane123', password: 'secret123', name: 'Jane Doe', organisation_id: school.id)
24+
.with(token:, username: 'jane123', password: 'secret123', name: 'Jane Doe', school:)
2525
end
2626

2727
it "makes a profile API call to create John Doe's account" do
2828
described_class.call(school:, uploaded_file: file, token:)
2929

3030
# TODO: Replace with WebMock assertion once the profile API has been built.
3131
expect(ProfileApiClient).to have_received(:create_school_student)
32-
.with(token:, username: 'john123', password: 'secret456', name: 'John Doe', organisation_id: school.id)
32+
.with(token:, username: 'john123', password: 'secret456', name: 'John Doe', school:)
3333
end
3434

3535
context 'when an .xlsx file is provided' do

spec/concepts/school_student/create_spec.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@
2828

2929
# TODO: Replace with WebMock assertion once the profile API has been built.
3030
expect(ProfileApiClient).to have_received(:create_school_student)
31-
.with(token:, username: 'student-to-create', password: 'at-least-8-characters', name: 'School Student', organisation_id: school.id)
31+
.with(token:, username: 'student-to-create', password: 'at-least-8-characters', name: 'School Student', school:)
32+
end
33+
34+
it 'creates a role associating the student with the school' do
35+
user_id = SecureRandom.uuid
36+
allow(ProfileApiClient).to receive(:create_school_student).and_return({ created: [user_id] })
37+
described_class.call(school:, school_student_params:, token:)
38+
expect(Role.student.where(school:, user_id:)).to exist
3239
end
3340

3441
context 'when creation fails' do

spec/lib/profile_api_client_spec.rb

+81
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,85 @@ def delete_safeguarding_flag
292292
described_class.delete_safeguarding_flag(token:, flag:)
293293
end
294294
end
295+
296+
describe '.create_school_student' do
297+
let(:username) { 'username' }
298+
let(:password) { 'password' }
299+
let(:name) { 'name' }
300+
let(:school) { build(:school, id: SecureRandom.uuid) }
301+
let(:create_students_url) { "#{api_url}/api/v1/schools/#{school.id}/students" }
302+
303+
before do
304+
stub_request(:post, create_students_url).to_return(status: 201, body: '{}')
305+
end
306+
307+
it 'makes a request to the profile api host' do
308+
create_school_student
309+
expect(WebMock).to have_requested(:post, create_students_url)
310+
end
311+
312+
it 'includes token in the authorization request header' do
313+
create_school_student
314+
expect(WebMock).to have_requested(:post, create_students_url).with(headers: { authorization: "Bearer #{token}" })
315+
end
316+
317+
it 'includes the profile api key in the x-api-key request header' do
318+
create_school_student
319+
expect(WebMock).to have_requested(:post, create_students_url).with(headers: { 'x-api-key' => api_key })
320+
end
321+
322+
it 'sets content-type of request to json' do
323+
create_school_student
324+
expect(WebMock).to have_requested(:post, create_students_url).with(headers: { 'content-type' => 'application/json' })
325+
end
326+
327+
it 'sets accept header to json' do
328+
create_school_student
329+
expect(WebMock).to have_requested(:post, create_students_url).with(headers: { 'accept' => 'application/json' })
330+
end
331+
332+
it 'sends the student details in the request body' do
333+
create_school_student
334+
expect(WebMock).to have_requested(:post, create_students_url).with(body: [{ name:, username:, password: }].to_json)
335+
end
336+
337+
it 'returns the id of the created student(s) if successful' do
338+
response = { created: ['student-id'] }
339+
stub_request(:post, create_students_url)
340+
.to_return(status: 201, body: response.to_json)
341+
expect(create_school_student).to eq(response)
342+
end
343+
344+
it 'raises exception with details of the error if 422 response indicates that the user already exists' do
345+
response = { 'errors' => [{ 'username' => 'jdoe', 'error' => 'ERR_USER_EXISTS' }] }
346+
stub_request(:post, create_students_url)
347+
.to_return(status: 422, body: response.to_json)
348+
expect { create_school_student }.to raise_error('Student not created in Profile API. HTTP response code 422. Username already exists')
349+
end
350+
351+
it 'raises exception including the error code if 422 response indicates that some other error occurred' do
352+
response = { 'errors' => [{ 'username' => 'jdoe', 'error' => 'ERR_UNKNOWN' }] }
353+
stub_request(:post, create_students_url)
354+
.to_return(status: 422, body: response.to_json)
355+
expect { create_school_student }.to raise_error('Student not created in Profile API. HTTP response code 422. Unknown error code: ERR_UNKNOWN')
356+
end
357+
358+
it 'raises exception if anything other than a 201 status code is returned' do
359+
stub_request(:post, create_students_url)
360+
.to_return(status: 200)
361+
362+
expect { create_school_student }.to raise_error(RuntimeError)
363+
end
364+
365+
it 'includes details of underlying response when exception is raised' do
366+
stub_request(:post, create_students_url)
367+
.to_return(status: 401)
368+
369+
expect { create_school_student }.to raise_error('Student not created in Profile API. HTTP response code: 401')
370+
end
371+
372+
def create_school_student
373+
described_class.create_school_student(token:, username:, password:, name:, school:)
374+
end
375+
end
295376
end

spec/support/profile_api_mock.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def stub_profile_api_list_school_students(user_id:)
2828
end
2929

3030
def stub_profile_api_create_school_student
31-
allow(ProfileApiClient).to receive(:create_school_student)
31+
allow(ProfileApiClient).to receive(:create_school_student).and_return(created: [])
3232
end
3333

3434
def stub_profile_api_update_school_student

0 commit comments

Comments
 (0)