-
Notifications
You must be signed in to change notification settings - Fork 5
Cancel ownership transfer #1032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
64fb019
feat: add endpoint for the owner to cancel a pending ownership transfer
DNR500 25be4cd
feat: email the nominee when their ownership nomination is cancelled
DNR500 7acbcd9
fix: guard against a blank user-info API response crashing mailer jobs
DNR500 768f7e3
fix: guard against blank names in the ownership transfer request email
DNR500 fe1e75a
docs: reword comment to be less jargony
DNR500 7166cd9
refactor: use first! so not-found and unauthorized share one rescue path
DNR500 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
app/views/school_ownership_mailer/cancel_ownership_transfer.text.erb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| Hi <%= @nominee_name %>, | ||
|
|
||
| The nomination for you to become the owner of the Code Classroom account for <%= @school.name %> has been cancelled by the school owner. | ||
|
|
||
| No action is needed from you, and ownership remains unchanged. If you were expecting to take over this Code Classroom, please reach out to the school owner directly. | ||
|
|
||
| If you have any questions, please contact us at websupport@raspberrypi.org. | ||
|
|
||
| Kind Regards, | ||
| The Code Editor team |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
spec/features/ownership_transfer/cancelling_an_ownership_transfer_spec.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'rails_helper' | ||
|
|
||
| RSpec.describe 'Cancelling an ownership transfer', type: :request do | ||
| include ActionMailer::TestHelper | ||
|
|
||
| include_context 'with a school owner and nominated teacher' | ||
|
|
||
| it 'responds 401 Unauthorized when no token is given' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel") | ||
| expect(response).to have_http_status(:unauthorized) | ||
| end | ||
|
|
||
| it 'responds 403 Forbidden when the user is a school-student' do | ||
| student = create(:student, school:) | ||
| authenticated_in_hydra_as(student) | ||
|
|
||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:forbidden) | ||
| end | ||
|
|
||
| it 'responds 403 Forbidden when the user is the owner of a different school' do | ||
| other_owner = create(:owner, school: create(:verified_school)) | ||
| authenticated_in_hydra_as(other_owner) | ||
|
|
||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:forbidden) | ||
| end | ||
|
|
||
| context 'when the school has never had an ownership transfer' do | ||
| before { authenticated_in_hydra_as(owner) } | ||
|
|
||
| it 'responds 404 Not Found' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:not_found) | ||
| end | ||
| end | ||
|
|
||
| context 'when there is a pending transfer for the school' do | ||
| let!(:ownership_transfer) do | ||
| create( | ||
| :ownership_transfer, | ||
| school:, | ||
| nominated_user_id: nominee.id, | ||
| requested_by_user_id: owner.id, | ||
| email_address: nominee.email | ||
| ) | ||
| end | ||
|
|
||
| context 'when the current user is the owner who requested the transfer' do | ||
| before { authenticated_in_hydra_as(owner) } | ||
|
|
||
| it 'responds 200 OK' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:ok) | ||
| end | ||
|
|
||
| it 'marks the transfer as cancelled' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(ownership_transfer.reload.status).to eq('cancelled') | ||
| end | ||
|
|
||
| it 'sends the cancellation email' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
|
|
||
| assert_enqueued_email_with( | ||
| SchoolOwnershipMailer, :cancel_ownership_transfer, params: { ownership_transfer: } | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when the current user is a different owner of the same school' do | ||
| let(:other_owner) { create(:owner, school:) } | ||
|
|
||
| before { authenticated_in_hydra_as(other_owner) } | ||
|
|
||
| it 'responds 200 OK, since any current owner can cancel, not only the one who requested it' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:ok) | ||
| end | ||
| end | ||
|
|
||
| context 'when the current user is the nominee' do | ||
| before { authenticated_in_hydra_as(nominee) } | ||
|
|
||
| it 'responds 404 Not Found, since only an owner of the school can cancel' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:not_found) | ||
| end | ||
|
|
||
| it 'does not change the transfer status' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(ownership_transfer.reload.status).to eq('pending') | ||
| end | ||
| end | ||
|
|
||
| context 'when the current user is a different teacher at the school' do | ||
| let(:other_teacher) { create(:teacher, school:) } | ||
|
|
||
| before { authenticated_in_hydra_as(other_teacher) } | ||
|
|
||
| it 'responds 404 Not Found' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:not_found) | ||
| end | ||
| end | ||
|
|
||
| context 'when the transfer is no longer pending' do | ||
| before do | ||
| ownership_transfer.update!(status: :completed) | ||
| authenticated_in_hydra_as(owner) | ||
| end | ||
|
|
||
| it 'responds 404 Not Found' do | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| expect(response).to have_http_status(:not_found) | ||
| end | ||
| end | ||
|
|
||
| context 'when the transfer has already been cancelled' do | ||
| before do | ||
| authenticated_in_hydra_as(owner) | ||
| put("/api/schools/#{school.id}/ownership_transfer/cancel", headers:) | ||
| end | ||
|
|
||
| it 'allows the owner to start a new transfer' do | ||
| other_teacher = create(:teacher, school:) | ||
| stub_user_info_api_for(other_teacher) | ||
|
|
||
| post( | ||
| "/api/schools/#{school.id}/ownership_transfer", | ||
| params: { ownership_transfer: { nominated_user_id: other_teacher.id } }, | ||
| headers: | ||
| ) | ||
|
|
||
| expect(response).to have_http_status(:created) | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what requests does the User Info response return an empty body?
I just wanted to check it was equivalent to no users found, and not a different error state we should handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Any real failure from the API (like a 500) already gets turned into an error before we reach this line, so an empty body here isn't a hidden failure - it only happens on an otherwise successful response. The case I had in mind is looking up a user who's been deleted since the transfer was created - no match, so nothing comes back.
I also double checked this. I looked at how the JSON library we use handles an empty response, and it looks like it turns a blank body into either nil or an empty string - neither of which works with the code we call it with. So without this line, that case could crash instead of just returning no users.
For anyone who wants to check this themselves:
conn(lib/user_info_api_client.rb:51-61) setsf.response :raise_errorbeforef.response :json.raise_errorraises on any 4xx/5xx, so by the time we reach line 16, the response is guaranteed 2xx - an empty body here can't be masking a failed request.faradaygem, v2.14.3,lib/faraday/response/json.rb) handles an empty body one of two ways depending on whether the response has a JSON content-type:parsemethod explicitly doesreturn if body.strip.empty?(line 32), soresponse.bodybecomesnil.parse_response?(line 39) returns false, parsing is skipped entirely, andresponse.bodystays whatever the raw HTTP body was - an empty string for a zero-content response.response.bodyends up asnilor"", and.fetch('users', [])isn't defined on either, so line 18 would raise without the guard on line 16.I confirmed this locally by temporarily removing line 16 and running
spec/lib/user_info_api_client_spec.rb- it fails withNoMethodError: undefined method 'fetch' for an instance of String.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I talked to @DNR500 about this to be clear why this change has been made.
It's not actually needed as we know no cases where the UserInfoAPI returns an empty body on successful responses.
Instead the purpose for this change is to prevent errors just in case one day the user info API starts returning blank responses.