Skip to content

Commit 2b1aac3

Browse files
committed
Add comprehensive tests for invitation acceptance fix
Tests cover: - Invitation edit action with valid/invalid tokens - Invitation update action with various scenarios (valid, invalid token, expired invitation, password validation errors) - Parameter sanitization to ensure invitation_token is permitted - View rendering with local: true form and no readonly on hidden field All 23 examples passing.
1 parent 573fe0b commit 2b1aac3

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe "Users::InvitationsController", type: :request do
6+
let(:casa_org) { create(:casa_org) }
7+
let(:volunteer) { create(:volunteer, casa_org: casa_org) }
8+
9+
describe "GET /users/invitation/accept" do
10+
context "with valid invitation_token" do
11+
before do
12+
volunteer.invite!
13+
end
14+
15+
it "sets invitation_token on the resource" do
16+
get accept_user_invitation_path(invitation_token: volunteer.raw_invitation_token)
17+
18+
expect(response).to have_http_status(:success)
19+
expect(response.body).to include("Set my password")
20+
expect(response.body).to include(volunteer.raw_invitation_token)
21+
end
22+
23+
it "renders the edit template" do
24+
get accept_user_invitation_path(invitation_token: volunteer.raw_invitation_token)
25+
26+
expect(response).to render_template(:edit)
27+
end
28+
end
29+
30+
context "without invitation_token" do
31+
it "redirects away" do
32+
get accept_user_invitation_path
33+
34+
# Devise may redirect to root or sign_in depending on configuration
35+
expect(response).to have_http_status(:redirect)
36+
end
37+
end
38+
end
39+
40+
describe "PUT /users/invitation" do
41+
let(:valid_password) { "Password123!" }
42+
43+
context "with valid invitation_token and password" do
44+
before do
45+
volunteer.invite!
46+
end
47+
48+
let(:params) do
49+
{
50+
user: {
51+
invitation_token: volunteer.raw_invitation_token,
52+
password: valid_password,
53+
password_confirmation: valid_password
54+
}
55+
}
56+
end
57+
58+
it "accepts the invitation" do
59+
expect {
60+
put user_invitation_path, params: params
61+
}.to change { volunteer.reload.invitation_accepted_at }.from(nil)
62+
end
63+
64+
it "sets the password" do
65+
put user_invitation_path, params: params
66+
67+
volunteer.reload
68+
expect(volunteer.valid_password?(valid_password)).to be true
69+
end
70+
71+
it "signs in the user" do
72+
put user_invitation_path, params: params
73+
74+
expect(controller.current_user).to eq(volunteer)
75+
end
76+
77+
it "redirects after acceptance" do
78+
put user_invitation_path, params: params
79+
80+
expect(response).to redirect_to(authenticated_user_root_path)
81+
end
82+
end
83+
84+
context "with invalid invitation_token" do
85+
let(:params) do
86+
{
87+
user: {
88+
invitation_token: "invalid_token",
89+
password: valid_password,
90+
password_confirmation: valid_password
91+
}
92+
}
93+
end
94+
95+
it "does not accept the invitation" do
96+
put user_invitation_path, params: params
97+
98+
expect(response).to have_http_status(:success)
99+
expect(response.body).to include("Invitation token is invalid")
100+
end
101+
end
102+
103+
context "with blank invitation_token" do
104+
let(:params) do
105+
{
106+
user: {
107+
invitation_token: "",
108+
password: valid_password,
109+
password_confirmation: valid_password
110+
}
111+
}
112+
end
113+
114+
it "shows validation error" do
115+
put user_invitation_path, params: params
116+
117+
expect(response).to have_http_status(:success)
118+
expect(response.body).to include("Invitation token")
119+
end
120+
end
121+
122+
context "with mismatched passwords" do
123+
before do
124+
volunteer.invite!
125+
end
126+
127+
let(:params) do
128+
{
129+
user: {
130+
invitation_token: volunteer.raw_invitation_token,
131+
password: valid_password,
132+
password_confirmation: "DifferentPassword123!"
133+
}
134+
}
135+
end
136+
137+
it "does not accept the invitation" do
138+
expect {
139+
put user_invitation_path, params: params
140+
}.not_to change { volunteer.reload.invitation_accepted_at }
141+
end
142+
143+
it "shows validation error" do
144+
put user_invitation_path, params: params
145+
146+
expect(response).to have_http_status(:success)
147+
expect(response.body).to include("Password confirmation")
148+
end
149+
end
150+
151+
context "with password too short" do
152+
before do
153+
volunteer.invite!
154+
end
155+
156+
let(:params) do
157+
{
158+
user: {
159+
invitation_token: volunteer.raw_invitation_token,
160+
password: "short",
161+
password_confirmation: "short"
162+
}
163+
}
164+
end
165+
166+
it "does not accept the invitation" do
167+
expect {
168+
put user_invitation_path, params: params
169+
}.not_to change { volunteer.reload.invitation_accepted_at }
170+
end
171+
172+
it "shows validation error" do
173+
put user_invitation_path, params: params
174+
175+
expect(response).to have_http_status(:success)
176+
expect(response.body).to include("Password is too short")
177+
end
178+
end
179+
180+
context "with expired invitation" do
181+
before do
182+
volunteer.invite!
183+
travel_to 2.years.from_now
184+
end
185+
186+
after do
187+
travel_back
188+
end
189+
190+
let(:params) do
191+
{
192+
user: {
193+
invitation_token: volunteer.raw_invitation_token,
194+
password: valid_password,
195+
password_confirmation: valid_password
196+
}
197+
}
198+
end
199+
200+
it "does not accept the invitation" do
201+
expect {
202+
put user_invitation_path, params: params
203+
}.not_to change { volunteer.reload.invitation_accepted_at }
204+
end
205+
206+
it "shows validation error" do
207+
put user_invitation_path, params: params
208+
209+
expect(response).to have_http_status(:success)
210+
expect(response.body).to include("Invitation token is invalid")
211+
end
212+
end
213+
end
214+
215+
describe "parameter sanitization" do
216+
before do
217+
volunteer.invite!
218+
end
219+
220+
it "permits invitation_token in update" do
221+
params = {
222+
user: {
223+
invitation_token: volunteer.raw_invitation_token,
224+
password: "Password123!",
225+
password_confirmation: "Password123!",
226+
extra_param: "should_not_be_permitted"
227+
}
228+
}
229+
230+
put user_invitation_path, params: params
231+
232+
# If the invitation_token was properly permitted, the invitation should be accepted
233+
expect(volunteer.reload.invitation_accepted_at).to be_present
234+
end
235+
end
236+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe "devise/invitations/edit.html.erb", type: :view do
6+
let(:casa_org) { create(:casa_org) }
7+
let(:volunteer) { create(:volunteer, casa_org: casa_org) }
8+
9+
before do
10+
volunteer.invite!
11+
assign(:resource, volunteer)
12+
assign(:resource_name, :user)
13+
assign(:devise_mapping, Devise.mappings[:user])
14+
assign(:minimum_password_length, 6)
15+
16+
# Set the invitation_token on the resource as the controller does
17+
volunteer.invitation_token = volunteer.raw_invitation_token
18+
19+
# Allow the class to respond to require_password_on_accepting
20+
allow(volunteer.class).to receive(:require_password_on_accepting).and_return(true)
21+
22+
render
23+
end
24+
25+
it "uses form_with with local: true" do
26+
# form_with local: true should not have data-remote="true"
27+
expect(rendered).not_to have_selector('form[data-remote="true"]')
28+
end
29+
30+
it "includes invitation_token field" do
31+
expect(rendered).to match(/invitation_token/)
32+
end
33+
34+
it "does not have readonly attribute on invitation_token field" do
35+
expect(rendered).not_to match(/invitation_token.*readonly/)
36+
end
37+
38+
it "includes password fields" do
39+
expect(rendered).to match(/password/)
40+
expect(rendered).to match(/password_confirmation/)
41+
end
42+
43+
it "includes submit button" do
44+
expect(rendered).to have_button("Set my password")
45+
end
46+
47+
it "uses PUT method" do
48+
expect(rendered).to have_selector('form[method="post"]') # Rails uses POST with _method=put
49+
expect(rendered).to have_field("_method", type: :hidden, with: "put")
50+
end
51+
52+
it "posts to invitation_path" do
53+
expect(rendered).to have_selector("form[action='#{user_invitation_path}']")
54+
end
55+
end

0 commit comments

Comments
 (0)