Skip to content

Commit 068faee

Browse files
committed
Implement ProfileApiClient.create_school_student
1 parent 69b6be2 commit 068faee

File tree

7 files changed

+138
-21
lines changed

7 files changed

+138
-21
lines changed

lib/concepts/school_student/create.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class Create
55
class << self
66
def call(school:, school_student_params:, token:)
77
response = OperationResponse.new
8-
create_student(school, school_student_params, token)
8+
response[:student_id] = create_student(school, school_student_params, token)
99
response
1010
rescue StandardError => e
1111
Sentry.capture_exception(e)
@@ -16,14 +16,17 @@ def call(school:, school_student_params:, token:)
1616
private
1717

1818
def create_student(school, school_student_params, token)
19-
organisation_id = school.id
19+
school_id = school.id
2020
username = school_student_params.fetch(:username)
2121
password = school_student_params.fetch(:password)
2222
name = school_student_params.fetch(:name)
2323

2424
validate(school:, username:, password:, name:)
2525

26-
ProfileApiClient.create_school_student(token:, username:, password:, name:, organisation_id:)
26+
response = ProfileApiClient.create_school_student(token:, username:, password:, name:, school_id:)
27+
user_id = response[:created].first
28+
Role.student.create!(school:, user_id:)
29+
user_id
2730
end
2831

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

lib/concepts/school_student/create_batch.rb

Lines changed: 1 addition & 1 deletion
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_id: school.id)
2727
end
2828
end
2929

lib/profile_api_client.rb

Lines changed: 20 additions & 9 deletions
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_id:)
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: name.strip,
158+
username: username.strip,
159+
password: password.strip
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

Lines changed: 2 additions & 2 deletions
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_id: school.id)
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_id: school.id)
3333
end
3434

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

spec/concepts/school_student/create_spec.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
RSpec.describe SchoolStudent::Create, type: :unit do
66
let(:token) { UserProfileMock::TOKEN }
77
let(:school) { create(:verified_school) }
8+
let(:user_id) { SecureRandom.uuid }
89

910
let(:school_student_params) do
1011
{
@@ -15,7 +16,7 @@
1516
end
1617

1718
before do
18-
stub_profile_api_create_school_student
19+
stub_profile_api_create_school_student(user_id:)
1920
end
2021

2122
it 'returns a successful operation response' do
@@ -28,13 +29,23 @@
2829

2930
# TODO: Replace with WebMock assertion once the profile API has been built.
3031
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)
32+
.with(token:, username: 'student-to-create', password: 'at-least-8-characters', name: 'School Student', school_id: school.id)
33+
end
34+
35+
it 'creates a role associating the student with the school' do
36+
described_class.call(school:, school_student_params:, token:)
37+
expect(Role.student.where(school:, user_id:)).to exist
38+
end
39+
40+
it 'returns the ID of the student created in Profile API' do
41+
response = described_class.call(school:, school_student_params:, token:)
42+
expect(response[:student_id]).to eq(user_id)
3243
end
3344

3445
context 'when creation fails' do
3546
let(:school_student_params) do
3647
{
37-
username: ' ',
48+
username: '',
3849
password: 'at-least-8-characters',
3950
name: 'School Student'
4051
}
@@ -56,7 +67,7 @@
5667

5768
it 'returns the error message in the operation response' do
5869
response = described_class.call(school:, school_student_params:, token:)
59-
expect(response[:error]).to match(/username ' ' is invalid/)
70+
expect(response[:error]).to match(/username '' is invalid/)
6071
end
6172

6273
it 'sent the exception to Sentry' do

spec/lib/profile_api_client_spec.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,96 @@ 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+
context 'when there are extraneous leading and trailing spaces in the student params' do
373+
let(:username) { ' username ' }
374+
let(:password) { ' password ' }
375+
let(:name) { ' name ' }
376+
377+
it 'strips the extraneous spaces' do
378+
create_school_student
379+
expect(WebMock).to have_requested(:post, create_students_url).with(body: [{ name: 'name', username: 'username', password: 'password' }].to_json)
380+
end
381+
end
382+
383+
def create_school_student
384+
described_class.create_school_student(token:, username:, password:, name:, school_id: school.id)
385+
end
386+
end
295387
end

spec/support/profile_api_mock.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def stub_profile_api_list_school_students(user_id:)
2727
allow(ProfileApiClient).to receive(:list_school_students).and_return(ids: [user_id])
2828
end
2929

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

3434
def stub_profile_api_update_school_student

0 commit comments

Comments
 (0)