-
-
Notifications
You must be signed in to change notification settings - Fork 512
Fix invitation token form submission #6625
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class Users::InvitationsController < Devise::InvitationsController | ||
| # GET /users/invitation/accept?invitation_token=abcdef123456 | ||
| def edit | ||
| set_minimum_password_length | ||
| # Ensure the invitation_token is set on the resource from the URL parameter | ||
| resource.invitation_token = params[:invitation_token] | ||
|
|
||
| # Removed logging of invitation tokens for security reasons | ||
|
|
||
|
|
||
| render :edit | ||
| end | ||
|
|
||
| # PUT /users/invitation | ||
| def update | ||
| # Removed logging of invitation tokens for security reasons | ||
| super | ||
| end | ||
|
|
||
| protected | ||
|
|
||
| # Permit the invitation_token parameter | ||
| def update_resource_params | ||
| params.require(resource_name).permit(:invitation_token, :password, :password_confirmation) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "rails_helper" | ||
|
|
||
| RSpec.describe "Users::InvitationsController", type: :request do | ||
| let(:casa_org) { create(:casa_org) } | ||
| let(:volunteer) { create(:volunteer, casa_org: casa_org) } | ||
|
|
||
| describe "GET /users/invitation/accept" do | ||
| context "with valid invitation_token" do | ||
| before do | ||
| volunteer.invite! | ||
| end | ||
|
|
||
| it "sets invitation_token on the resource" do | ||
| get accept_user_invitation_path(invitation_token: volunteer.raw_invitation_token) | ||
|
|
||
| expect(response).to have_http_status(:success) | ||
| expect(response.body).to include("Set my password") | ||
| expect(response.body).to include(volunteer.raw_invitation_token) | ||
| end | ||
|
|
||
| it "renders the edit template" do | ||
| get accept_user_invitation_path(invitation_token: volunteer.raw_invitation_token) | ||
|
|
||
| expect(response).to render_template(:edit) | ||
| end | ||
| end | ||
|
|
||
| context "without invitation_token" do | ||
| it "redirects away" do | ||
| get accept_user_invitation_path | ||
|
|
||
| # Devise may redirect to root or sign_in depending on configuration | ||
| expect(response).to have_http_status(:redirect) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "PUT /users/invitation" do | ||
| let(:valid_password) { "Password123!" } | ||
|
|
||
| context "with valid invitation_token and password" do | ||
| before do | ||
| volunteer.invite! | ||
| end | ||
|
|
||
| let(:params) do | ||
| { | ||
| user: { | ||
| invitation_token: volunteer.raw_invitation_token, | ||
| password: valid_password, | ||
| password_confirmation: valid_password | ||
| } | ||
| } | ||
| end | ||
|
|
||
| it "accepts the invitation" do | ||
| expect { | ||
| put user_invitation_path, params: params | ||
| }.to change { volunteer.reload.invitation_accepted_at }.from(nil) | ||
| end | ||
|
|
||
| it "sets the password" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| volunteer.reload | ||
| expect(volunteer.valid_password?(valid_password)).to be true | ||
| end | ||
|
|
||
| it "signs in the user" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(controller.current_user).to eq(volunteer) | ||
| end | ||
|
|
||
| it "redirects after acceptance" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(response).to redirect_to(authenticated_user_root_path) | ||
| end | ||
| end | ||
|
|
||
| context "with invalid invitation_token" do | ||
| let(:params) do | ||
| { | ||
| user: { | ||
| invitation_token: "invalid_token", | ||
| password: valid_password, | ||
| password_confirmation: valid_password | ||
| } | ||
| } | ||
| end | ||
|
|
||
| it "does not accept the invitation" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(response).to have_http_status(:success) | ||
| expect(response.body).to include("Invitation token is invalid") | ||
| end | ||
| end | ||
|
|
||
| context "with blank invitation_token" do | ||
| let(:params) do | ||
| { | ||
| user: { | ||
| invitation_token: "", | ||
| password: valid_password, | ||
| password_confirmation: valid_password | ||
| } | ||
| } | ||
| end | ||
|
|
||
| it "shows validation error" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(response).to have_http_status(:success) | ||
| expect(response.body).to include("Invitation token") | ||
| end | ||
| end | ||
|
|
||
| context "with mismatched passwords" do | ||
| before do | ||
| volunteer.invite! | ||
| end | ||
|
|
||
| let(:params) do | ||
| { | ||
| user: { | ||
| invitation_token: volunteer.raw_invitation_token, | ||
| password: valid_password, | ||
| password_confirmation: "DifferentPassword123!" | ||
| } | ||
| } | ||
| end | ||
|
|
||
| it "does not accept the invitation" do | ||
| expect { | ||
| put user_invitation_path, params: params | ||
| }.not_to change { volunteer.reload.invitation_accepted_at } | ||
| end | ||
|
|
||
| it "shows validation error" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(response).to have_http_status(:success) | ||
| expect(response.body).to include("Password confirmation") | ||
| end | ||
| end | ||
|
|
||
| context "with password too short" do | ||
| before do | ||
| volunteer.invite! | ||
| end | ||
|
|
||
| let(:params) do | ||
| { | ||
| user: { | ||
| invitation_token: volunteer.raw_invitation_token, | ||
| password: "short", | ||
| password_confirmation: "short" | ||
| } | ||
| } | ||
| end | ||
|
|
||
| it "does not accept the invitation" do | ||
| expect { | ||
| put user_invitation_path, params: params | ||
| }.not_to change { volunteer.reload.invitation_accepted_at } | ||
| end | ||
|
|
||
| it "shows validation error" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(response).to have_http_status(:success) | ||
| expect(response.body).to include("Password is too short") | ||
| end | ||
| end | ||
|
|
||
| context "with expired invitation" do | ||
| before do | ||
| volunteer.invite! | ||
| travel_to 2.years.from_now | ||
| end | ||
|
|
||
| after do | ||
| travel_back | ||
| end | ||
|
|
||
| let(:params) do | ||
| { | ||
| user: { | ||
| invitation_token: volunteer.raw_invitation_token, | ||
| password: valid_password, | ||
| password_confirmation: valid_password | ||
| } | ||
| } | ||
| end | ||
|
|
||
| it "does not accept the invitation" do | ||
| expect { | ||
| put user_invitation_path, params: params | ||
| }.not_to change { volunteer.reload.invitation_accepted_at } | ||
| end | ||
|
|
||
| it "shows validation error" do | ||
| put user_invitation_path, params: params | ||
|
|
||
| expect(response).to have_http_status(:success) | ||
| expect(response.body).to include("Invitation token is invalid") | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "parameter sanitization" do | ||
| before do | ||
| volunteer.invite! | ||
| end | ||
|
|
||
| it "permits invitation_token in update" do | ||
| params = { | ||
| user: { | ||
| invitation_token: volunteer.raw_invitation_token, | ||
| password: "Password123!", | ||
| password_confirmation: "Password123!", | ||
| extra_param: "should_not_be_permitted" | ||
| } | ||
| } | ||
|
|
||
| put user_invitation_path, params: params | ||
|
|
||
| # If the invitation_token was properly permitted, the invitation should be accepted | ||
| expect(volunteer.reload.invitation_accepted_at).to be_present | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,55 @@ | ||||||
| # frozen_string_literal: true | ||||||
|
|
||||||
| require "rails_helper" | ||||||
|
|
||||||
| RSpec.describe "devise/invitations/edit.html.erb", type: :view do | ||||||
| let(:casa_org) { create(:casa_org) } | ||||||
| let(:volunteer) { create(:volunteer, casa_org: casa_org) } | ||||||
|
|
||||||
| before do | ||||||
| volunteer.invite! | ||||||
| assign(:resource, volunteer) | ||||||
| assign(:resource_name, :user) | ||||||
| assign(:devise_mapping, Devise.mappings[:user]) | ||||||
| assign(:minimum_password_length, 6) | ||||||
|
|
||||||
| # Set the invitation_token on the resource as the controller does | ||||||
| volunteer.invitation_token = volunteer.raw_invitation_token | ||||||
|
|
||||||
| # Allow the class to respond to require_password_on_accepting | ||||||
| allow(volunteer.class).to receive(:require_password_on_accepting).and_return(true) | ||||||
|
|
||||||
| render | ||||||
| end | ||||||
|
|
||||||
| it "uses form_with with local: true" do | ||||||
| # form_with local: true should not have data-remote="true" | ||||||
| expect(rendered).not_to have_selector('form[data-remote="true"]') | ||||||
| end | ||||||
|
|
||||||
| it "includes invitation_token field" do | ||||||
| expect(rendered).to match(/invitation_token/) | ||||||
| end | ||||||
|
|
||||||
| it "does not have readonly attribute on invitation_token field" do | ||||||
| expect(rendered).not_to match(/invitation_token.*readonly/) | ||||||
|
||||||
| expect(rendered).not_to match(/invitation_token.*readonly/) | |
| expect(rendered).to have_field("invitation_token", readonly: false) |
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.
Accessing controller.current_user in a request spec is unreliable and may not work as expected. Request specs are black-box tests that should only verify the HTTP response. Consider checking that the user is signed in by verifying the response redirects to an authenticated page or by checking the session directly. Alternatively, if you need to test controller internals, use a controller spec instead.