Skip to content

Commit b8dc482

Browse files
committed
Implement ProfileApiClient.update_school_student
I've chosen to make ProfileApiClient responsible for stripping leading and trailing spaces from name, username and password, and for not sending them to the API unless they're defined. API docs: https://my.raspberrypi.org/api/v1/documentation/#/Students/patch_schools__schoolId__students__studentId_
1 parent 481ffcc commit b8dc482

File tree

5 files changed

+138
-19
lines changed

5 files changed

+138
-19
lines changed

app/controllers/api/school_students_controller.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ def create_batch
3939
end
4040

4141
def update
42-
result = SchoolStudent::Update.call(school: @school, school_student_params:, token: current_user.token)
42+
result = SchoolStudent::Update.call(
43+
school: @school, student_id: params[:id], school_student_params:, token: current_user.token
44+
)
4345

4446
if result.success?
4547
head :no_content

lib/concepts/school_student/update.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
module SchoolStudent
44
class Update
55
class << self
6-
def call(school:, school_student_params:, token:)
6+
def call(school:, student_id:, school_student_params:, token:)
77
response = OperationResponse.new
8-
update_student(school, school_student_params, token)
8+
update_student(school, student_id, school_student_params, token)
99
response
1010
rescue StandardError => e
1111
Sentry.capture_exception(e)
@@ -15,17 +15,16 @@ def call(school:, school_student_params:, token:)
1515

1616
private
1717

18-
def update_student(school, school_student_params, token)
18+
def update_student(school, student_id, school_student_params, token)
1919
username = school_student_params.fetch(:username, nil)
2020
password = school_student_params.fetch(:password, nil)
2121
name = school_student_params.fetch(:name, nil)
2222

2323
validate(username:, password:, name:)
2424

25-
attributes_to_update = { username:, password:, name: }.compact
26-
return if attributes_to_update.empty?
27-
28-
ProfileApiClient.update_school_student(token:, attributes_to_update:, organisation_id: school.id)
25+
ProfileApiClient.update_school_student(
26+
token:, school_id: school.id, student_id:, username:, password:, name:
27+
)
2928
end
3029

3130
def validate(username:, password:, name:)

lib/profile_api_client.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,25 @@ def create_school_student(token:, username:, password:, name:, school_id:)
109109
raise Student422Error, JSON.parse(e.response_body)['errors'].first
110110
end
111111

112-
def update_school_student(token:, attributes_to_update:, organisation_id:)
112+
# rubocop:disable Metrics/AbcSize
113+
def update_school_student(token:, school_id:, student_id:, name: nil, username: nil, password: nil) # rubocop:disable Metrics/ParameterLists
113114
return nil if token.blank?
114115

115-
_ = attributes_to_update
116-
_ = organisation_id
116+
response = connection(token).patch("/api/v1/schools/#{school_id}/students/#{student_id}") do |request|
117+
request.body = {
118+
name: name&.strip,
119+
username: username&.strip,
120+
password: password&.strip
121+
}.compact
122+
end
117123

118-
{}
124+
raise UnexpectedResponse, response unless response.status == 200
125+
126+
Student.new(**response.body)
127+
rescue Faraday::UnprocessableEntityError => e
128+
raise Student422Error, JSON.parse(e.response_body)['errors'].first
119129
end
130+
# rubocop:enable Metrics/AbcSize
120131

121132
def delete_school_student(token:, student_id:, organisation_id:)
122133
return nil if token.blank?

spec/concepts/school_student/update_spec.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@
2020
end
2121

2222
it 'returns a successful operation response' do
23-
response = described_class.call(school:, school_student_params:, token:)
23+
response = described_class.call(school:, student_id:, school_student_params:, token:)
2424
expect(response.success?).to be(true)
2525
end
2626

2727
it 'makes a profile API call' do
28-
described_class.call(school:, school_student_params:, token:)
28+
described_class.call(school:, student_id:, school_student_params:, token:)
2929

3030
# TODO: Replace with WebMock assertion once the profile API has been built.
3131
expect(ProfileApiClient).to have_received(:update_school_student)
32-
.with(token:, attributes_to_update: school_student_params, organisation_id: school.id)
32+
.with(token:, username: 'new-username', password: 'new-password', name: 'New Name', school_id: school.id, student_id:)
3333
end
3434

3535
context 'when updating fails' do
@@ -46,22 +46,22 @@
4646
end
4747

4848
it 'does not make a profile API request' do
49-
described_class.call(school:, school_student_params:, token:)
49+
described_class.call(school:, student_id:, school_student_params:, token:)
5050
expect(ProfileApiClient).not_to have_received(:update_school_student)
5151
end
5252

5353
it 'returns a failed operation response' do
54-
response = described_class.call(school:, school_student_params:, token:)
54+
response = described_class.call(school:, student_id:, school_student_params:, token:)
5555
expect(response.failure?).to be(true)
5656
end
5757

5858
it 'returns the error message in the operation response' do
59-
response = described_class.call(school:, school_student_params:, token:)
59+
response = described_class.call(school:, student_id:, school_student_params:, token:)
6060
expect(response[:error]).to match(/username ' ' is invalid/)
6161
end
6262

6363
it 'sent the exception to Sentry' do
64-
described_class.call(school:, school_student_params:, token:)
64+
described_class.call(school:, student_id:, school_student_params:, token:)
6565
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
6666
end
6767
end

spec/lib/profile_api_client_spec.rb

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,4 +495,111 @@ def list_school_students
495495
described_class.list_school_students(token:, school_id: school.id, student_ids:)
496496
end
497497
end
498+
499+
# rubocop:disable RSpec/MultipleMemoizedHelpers
500+
describe '.update_school_student' do
501+
let(:username) { 'username' }
502+
let(:password) { 'password' }
503+
let(:name) { 'name' }
504+
let(:school) { build(:school, id: SecureRandom.uuid) }
505+
let(:student) { create(:student, school:) }
506+
let(:update_student_url) { "#{api_url}/api/v1/schools/#{school.id}/students/#{student.id}" }
507+
508+
before do
509+
stub_request(:patch, update_student_url)
510+
.to_return(
511+
status: 200,
512+
body: '{"id":"","schoolId":"","name":"","username":"","createdAt":"","updatedAt":"","discardedAt":""}',
513+
headers: { 'content-type' => 'application/json' }
514+
)
515+
end
516+
517+
it 'makes a request to the profile api host' do
518+
update_school_student
519+
expect(WebMock).to have_requested(:patch, update_student_url)
520+
end
521+
522+
it 'includes token in the authorization request header' do
523+
update_school_student
524+
expect(WebMock).to have_requested(:patch, update_student_url).with(headers: { authorization: "Bearer #{token}" })
525+
end
526+
527+
it 'includes the profile api key in the x-api-key request header' do
528+
update_school_student
529+
expect(WebMock).to have_requested(:patch, update_student_url).with(headers: { 'x-api-key' => api_key })
530+
end
531+
532+
it 'sets content-type of request to json' do
533+
update_school_student
534+
expect(WebMock).to have_requested(:patch, update_student_url).with(headers: { 'content-type' => 'application/json' })
535+
end
536+
537+
it 'sets accept header to json' do
538+
update_school_student
539+
expect(WebMock).to have_requested(:patch, update_student_url).with(headers: { 'accept' => 'application/json' })
540+
end
541+
542+
it 'sends the student details in the request body' do
543+
update_school_student
544+
expect(WebMock).to have_requested(:patch, update_student_url).with(body: { name:, username:, password: }.to_json)
545+
end
546+
547+
it 'returns the updated student if successful' do
548+
response = { id: 'id', schoolId: 'school-id', name: 'new-name', username: 'new-username', createdAt: '', updatedAt: '', discardedAt: '' }
549+
expected = ProfileApiClient::Student.new(**response)
550+
stub_request(:patch, update_student_url)
551+
.to_return(status: 200, body: response.to_json, headers: { 'content-type' => 'application/json' })
552+
expect(update_school_student).to eq(expected)
553+
end
554+
555+
it 'raises 422 exception if 422 status code is returned' do
556+
response = { errors: [username: 'username', error: 'ERR_USER_EXISTS'] }
557+
stub_request(:patch, update_student_url)
558+
.to_return(status: 422, body: response.to_json, headers: { 'content-type' => 'application/json' })
559+
560+
expect { update_school_student }.to raise_error(ProfileApiClient::Student422Error)
561+
.with_message("Student not saved in Profile API (status code 422, username 'username', error 'username has already been taken')")
562+
end
563+
564+
it 'raises exception if anything other that 200 status code is returned' do
565+
stub_request(:patch, update_student_url)
566+
.to_return(status: 201)
567+
568+
expect { update_school_student }.to raise_error(ProfileApiClient::UnexpectedResponse)
569+
end
570+
571+
it 'raises faraday exception for 4xx and 5xx responses' do
572+
stub_request(:patch, update_student_url)
573+
.to_return(status: 401)
574+
575+
expect { update_school_student }.to raise_error(Faraday::Error)
576+
end
577+
578+
context 'when there are extraneous leading and trailing spaces in the student params' do
579+
let(:username) { ' username ' }
580+
let(:password) { ' password ' }
581+
let(:name) { ' name ' }
582+
583+
it 'strips the extraneous spaces' do
584+
update_school_student
585+
expect(WebMock).to have_requested(:patch, update_student_url).with(body: { name: 'name', username: 'username', password: 'password' }.to_json)
586+
end
587+
end
588+
589+
context 'when optional values are nil' do
590+
let(:username) { nil }
591+
let(:password) { nil }
592+
let(:name) { nil }
593+
594+
it 'does not send empty values' do
595+
update_school_student
596+
expect(WebMock).to have_requested(:patch, update_student_url).with(body: {}.to_json)
597+
end
598+
end
599+
600+
def update_school_student
601+
described_class.update_school_student(token:, username:, password:, name:, school_id: school.id, student_id: student.id)
602+
end
603+
end
604+
# rubocop:enable RSpec/MultipleMemoizedHelpers
498605
end

0 commit comments

Comments
 (0)