Skip to content

Commit cd0ab54

Browse files
Set follow_redirects=False session-wide on TestClient and add redirect location assertions
Closes #101 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 236618b commit cd0ab54

8 files changed

Lines changed: 75 additions & 153 deletions

File tree

tests/conftest.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def env_vars(monkeypatch):
2929
m.setenv("DB_PORT", os.getenv("DB_PORT", "5432"))
3030
m.setenv("DB_USER", os.getenv("DB_USER", "postgres"))
3131
m.setenv("DB_PASSWORD", os.getenv("DB_PASSWORD", "postgres"))
32-
m.setenv("SECRET_KEY", "testsecretkey")
32+
m.setenv("SECRET_KEY", "testsecretkey-that-is-at-least-32-bytes-long")
3333
m.setenv("HOST_NAME", "Test Organization")
3434
m.setenv("DB_NAME", "webapp-test-db")
3535
m.setenv("RESEND_API_KEY", "test")
@@ -102,7 +102,7 @@ def unauth_client(session: Session) -> Generator[TestClient, None, None]:
102102
"""
103103
Provides a TestClient instance without authentication.
104104
"""
105-
client = TestClient(app)
105+
client = TestClient(app, follow_redirects=False)
106106
yield client
107107

108108

@@ -111,7 +111,7 @@ def auth_client(session: Session, test_account: Account, test_user: User) -> Gen
111111
"""
112112
Provides a TestClient instance with valid authentication tokens.
113113
"""
114-
client = TestClient(app)
114+
client = TestClient(app, follow_redirects=False)
115115

116116
# Create and set valid tokens
117117
access_token = create_access_token({"sub": test_account.email})
@@ -275,8 +275,8 @@ def non_member_user(session: Session) -> User:
275275
@pytest.fixture
276276
def auth_client_owner(session: Session, org_owner: User) -> Generator[TestClient, None, None]:
277277
"""Provides a TestClient authenticated as the organization owner"""
278-
client = TestClient(app)
279-
278+
client = TestClient(app, follow_redirects=False)
279+
280280
# Initialize tokens
281281
access_token = ""
282282
refresh_token = ""
@@ -295,8 +295,8 @@ def auth_client_owner(session: Session, org_owner: User) -> Generator[TestClient
295295
@pytest.fixture
296296
def auth_client_admin(session: Session, org_admin_user: User) -> Generator[TestClient, None, None]:
297297
"""Provides a TestClient authenticated as an organization administrator"""
298-
client = TestClient(app)
299-
298+
client = TestClient(app, follow_redirects=False)
299+
300300
# Initialize tokens
301301
access_token = ""
302302
refresh_token = ""
@@ -315,8 +315,8 @@ def auth_client_admin(session: Session, org_admin_user: User) -> Generator[TestC
315315
@pytest.fixture
316316
def auth_client_member(session: Session, org_member_user: User) -> Generator[TestClient, None, None]:
317317
"""Provides a TestClient authenticated as the organization member"""
318-
client = TestClient(app)
319-
318+
client = TestClient(app, follow_redirects=False)
319+
320320
# Initialize tokens
321321
access_token = ""
322322
refresh_token = ""
@@ -335,8 +335,8 @@ def auth_client_member(session: Session, org_member_user: User) -> Generator[Tes
335335
@pytest.fixture
336336
def auth_client_non_member(session: Session, non_member_user: User) -> Generator[TestClient, None, None]:
337337
"""Provides a TestClient authenticated as a non-member"""
338-
client = TestClient(app)
339-
338+
client = TestClient(app, follow_redirects=False)
339+
340340
# Initialize tokens
341341
access_token = ""
342342
refresh_token = ""
@@ -469,8 +469,8 @@ def existing_invitee_user(session: Session, existing_invitee_account: Account) -
469469
@pytest.fixture
470470
def auth_client_invitee(session: Session, existing_invitee_user: User) -> Generator[TestClient, None, None]:
471471
"""Provides a TestClient authenticated as the existing_invitee_user."""
472-
client = TestClient(app)
473-
472+
client = TestClient(app, follow_redirects=False)
473+
474474
# Initialize tokens
475475
access_token = ""
476476
refresh_token = ""

tests/routers/core/test_account.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ def test_register_endpoint(unauth_client: TestClient, session: Session):
5555
"password": "NewPass123!@#",
5656
"confirm_password": "NewPass123!@#"
5757
},
58-
follow_redirects=False
5958
)
60-
59+
6160
# Just check the response status code
6261
assert response.status_code == 303
62+
assert response.headers["location"] == str(app.url_path_for("read_dashboard"))
6363

6464
# Verify the account was created
6565
account = session.exec(select(Account).where(Account.email == "new@example.com")).first()
@@ -79,9 +79,9 @@ def test_login_endpoint(unauth_client: TestClient, test_account: Account):
7979
"email": test_account.email,
8080
"password": "Test123!@#"
8181
},
82-
follow_redirects=False
8382
)
8483
assert response.status_code == 303
84+
assert response.headers["location"] == str(app.url_path_for("read_dashboard"))
8585

8686
# Check if cookies are set
8787
cookies = response.cookies
@@ -99,9 +99,9 @@ def test_refresh_token_endpoint(auth_client: TestClient, test_account: Account):
9999

100100
response = auth_client.post(
101101
app.url_path_for("refresh_token"),
102-
follow_redirects=False
103102
)
104103
assert response.status_code == 303
104+
assert response.headers["location"] == str(app.url_path_for("read_dashboard"))
105105

106106
# Check for new tokens in headers
107107
cookie_headers = response.headers.get_list("set-cookie")
@@ -125,9 +125,9 @@ def test_password_reset_flow(unauth_client: TestClient, session: Session, test_a
125125
response = unauth_client.post(
126126
app.url_path_for("forgot_password"),
127127
data={"email": test_account.email},
128-
follow_redirects=False
129128
)
130129
assert response.status_code == 303
130+
assert response.headers["location"] == "/forgot_password?show_form=false"
131131

132132
# Verify the email was "sent" with correct parameters
133133
mock_resend_send.assert_called_once()
@@ -167,9 +167,9 @@ def test_password_reset_flow(unauth_client: TestClient, session: Session, test_a
167167
def test_logout_endpoint(auth_client: TestClient):
168168
response = auth_client.get(
169169
app.url_path_for("logout"),
170-
follow_redirects=False
171170
)
172171
assert response.status_code == 303
172+
assert response.headers["location"] == "/"
173173

174174
# Check for cookie deletion in headers
175175
cookie_headers = response.headers.get_list("set-cookie")
@@ -227,9 +227,9 @@ def test_password_reset_email_url(unauth_client: TestClient, session: Session, t
227227
response = unauth_client.post(
228228
app.url_path_for("forgot_password"),
229229
data={"email": test_account.email},
230-
follow_redirects=False
231230
)
232231
assert response.status_code == 303
232+
assert response.headers["location"] == "/forgot_password?show_form=false"
233233

234234
# Get the reset token from the database
235235
reset_token = session.exec(select(PasswordResetToken)
@@ -269,16 +269,16 @@ def test_forgot_password_does_not_send_second_email_while_token_is_active(
269269
first_response = unauth_client.post(
270270
app.url_path_for("forgot_password"),
271271
data={"email": test_account.email},
272-
follow_redirects=False,
273272
)
274273
assert first_response.status_code == 303
274+
assert first_response.headers["location"] == "/forgot_password?show_form=false"
275275

276276
second_response = unauth_client.post(
277277
app.url_path_for("forgot_password"),
278278
data={"email": test_account.email},
279-
follow_redirects=False,
280279
)
281280
assert second_response.status_code == 303
281+
assert second_response.headers["location"] == "/forgot_password?show_form=false"
282282

283283
tokens = session.exec(
284284
select(PasswordResetToken).where(PasswordResetToken.account_id == test_account.id)
@@ -294,9 +294,8 @@ def test_request_email_update_success(auth_client: TestClient, test_account: Acc
294294
response = auth_client.post(
295295
app.url_path_for("request_email_update"),
296296
data={"email": test_account.email, "new_email": new_email},
297-
follow_redirects=False
298297
)
299-
298+
300299
assert response.status_code == 303
301300
assert f"{app.url_path_for('read_profile')}?email_update_requested=true" in response.headers["location"]
302301

@@ -316,7 +315,6 @@ def test_request_email_update_same_email_returns_error_page(auth_client: TestCli
316315
response = auth_client.post(
317316
app.url_path_for("request_email_update"),
318317
data={"email": test_account.email, "new_email": test_account.email},
319-
follow_redirects=False,
320318
)
321319

322320
assert response.status_code == 401
@@ -349,10 +347,10 @@ def test_request_email_update_unauthenticated(unauth_client: TestClient):
349347
response = unauth_client.post(
350348
app.url_path_for("request_email_update"),
351349
data={"email": "test@example.com", "new_email": "new@example.com"},
352-
follow_redirects=False
353350
)
354-
351+
355352
assert response.status_code == 303 # Redirect to login
353+
assert response.headers["location"] == str(app.url_path_for("read_login"))
356354

357355

358356
def test_confirm_email_update_success(unauth_client: TestClient, session: Session, test_account: Account):
@@ -371,7 +369,6 @@ def test_confirm_email_update_success(unauth_client: TestClient, session: Sessio
371369
"token": update_token.token,
372370
"new_email": new_email
373371
},
374-
follow_redirects=False
375372
)
376373

377374
assert response.status_code == 303
@@ -486,9 +483,9 @@ def test_login_success_resets_email_limiter(unauth_client: TestClient, test_acco
486483
response = unauth_client.post(
487484
app.url_path_for("login"),
488485
data={"email": test_account.email, "password": "Test123!@#"},
489-
follow_redirects=False,
490486
)
491487
assert response.status_code == 303
488+
assert response.headers["location"] == str(app.url_path_for("read_dashboard"))
492489

493490
# Verify the limiter was reset — full allowance available
494491
assert login_email_limiter.remaining(f"email:{test_account.email.lower().strip()}") == login_email_limiter.max_attempts
@@ -505,7 +502,6 @@ def test_register_ip_rate_limit(unauth_client: TestClient, session: Session):
505502
"password": "Test123!@#",
506503
"confirm_password": "Test123!@#",
507504
},
508-
follow_redirects=False,
509505
)
510506

511507
response = unauth_client.post(
@@ -526,13 +522,11 @@ def test_forgot_password_ip_rate_limit(unauth_client: TestClient):
526522
unauth_client.post(
527523
app.url_path_for("forgot_password"),
528524
data={"email": f"user{i}@example.com"},
529-
follow_redirects=False,
530525
)
531526

532527
response = unauth_client.post(
533528
app.url_path_for("forgot_password"),
534529
data={"email": "extra@example.com"},
535-
follow_redirects=False,
536530
)
537531
assert response.status_code == 429
538532

@@ -543,7 +537,6 @@ def test_forgot_password_email_rate_limit(unauth_client: TestClient, test_accoun
543537
unauth_client.post(
544538
app.url_path_for("forgot_password"),
545539
data={"email": test_account.email},
546-
follow_redirects=False,
547540
)
548541

549542
response = unauth_client.post(

tests/routers/core/test_invitation.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,6 @@ def test_create_invitation_success(auth_client, inviter_user: User, test_organiz
277277
"role_id": str(member_role_id), # Form data is usually string
278278
"organization_id": str(test_organization.id) # Form data is usually string
279279
},
280-
follow_redirects=False # Important for checking redirect
281280
)
282281

283282
assert response.status_code == 303, f"Expected 303 redirect, got {response.status_code}. Response: {response.text}" # See Other redirect
@@ -327,7 +326,6 @@ def test_create_invitation_unauthorized(auth_client_member, test_user: User, tes
327326
"role_id": str(member_role.id),
328327
"organization_id": str(test_organization.id)
329328
},
330-
follow_redirects=False
331329
)
332330

333331
assert response.status_code == 403, f"Expected 403 Forbidden, got {response.status_code}. Response: {response.text}" # Forbidden
@@ -388,7 +386,6 @@ def test_create_invitation_for_existing_member(auth_client, inviter_user: User,
388386
"role_id": str(member_role.id),
389387
"organization_id": str(test_organization.id)
390388
},
391-
follow_redirects=False
392389
)
393390

394391
# Expecting a 409 Conflict based on the plan
@@ -405,7 +402,6 @@ def test_create_invitation_duplicate_active(auth_client, inviter_user: User, exi
405402
"role_id": str(existing_invitation.role_id),
406403
"organization_id": str(existing_invitation.organization_id)
407404
},
408-
follow_redirects=False
409405
)
410406

411407
assert response.status_code == 409, f"Expected 409 Conflict, got {response.status_code}. Response: {response.text}" # Conflict - ActiveInvitationExistsError
@@ -421,7 +417,6 @@ def test_create_invitation_role_not_found(auth_client, inviter_user: User, test_
421417
"role_id": str(non_existent_role_id),
422418
"organization_id": str(test_organization.id)
423419
},
424-
follow_redirects=False
425420
)
426421

427422
# Depending on implementation, this might be 404 (Role Not Found) or 400 (Invalid Role for Org)
@@ -448,7 +443,6 @@ def test_create_invitation_role_wrong_organization(auth_client, inviter_user: Us
448443
"role_id": str(other_role.id), # Role from the wrong org
449444
"organization_id": str(test_organization.id) # Target the main test org
450445
},
451-
follow_redirects=False
452446
)
453447

454448
# Plan suggests 400 Bad Request
@@ -473,12 +467,10 @@ def test_create_invitation_unauthenticated(unauth_client, test_organization: Org
473467
"role_id": str(member_role.id),
474468
"organization_id": str(test_organization.id)
475469
},
476-
follow_redirects=False # Check for redirect explicitly
477470
)
478471

479472
assert response.status_code == 303, f"Expected 303 redirect to login, got {response.status_code}" # Redirect to login
480-
# Optionally check that the redirect location is the login page
481-
# assert "/login" in response.headers.get("location", "")
473+
assert response.headers["location"] == app.url_path_for("read_login")
482474

483475
def test_create_invitation_email_send_failure(auth_client, inviter_user: User, test_organization: Organization, session: Session, mock_resend_send: MagicMock):
484476
"""Test that invitation creation fails and rolls back if email sending fails."""
@@ -501,7 +493,6 @@ def test_create_invitation_email_send_failure(auth_client, inviter_user: User, t
501493
"role_id": str(member_role_id),
502494
"organization_id": str(test_organization.id)
503495
},
504-
follow_redirects=False
505496
)
506497

507498
assert response.status_code == 500, f"Expected 500 Internal Server Error, got {response.status_code}. Response: {response.text}"
@@ -523,7 +514,6 @@ def test_organization_page_shows_active_invitations(auth_client_owner, test_orga
523514
assert test_organization.id is not None
524515
response = auth_client_owner.get(
525516
app.url_path_for("read_organization", org_id=test_organization.id),
526-
follow_redirects=False
527517
)
528518

529519
assert response.status_code == 200
@@ -542,7 +532,6 @@ def test_organization_page_invite_form_visibility(auth_client_owner, auth_client
542532
# Owner should see invitation form (has INVITE_USER permission via Owner role -> permission fixture)
543533
owner_response = auth_client_owner.get(
544534
app.url_path_for("read_organization", org_id=test_organization.id),
545-
follow_redirects=False
546535
)
547536
assert owner_response.status_code == 200
548537
assert '<form' in owner_response.text
@@ -553,7 +542,6 @@ def test_organization_page_invite_form_visibility(auth_client_owner, auth_client
553542
# Admin should also see invitation form (has INVITE_USER permission via Admin role -> permission fixture)
554543
admin_response = auth_client_admin.get(
555544
app.url_path_for("read_organization", org_id=test_organization.id),
556-
follow_redirects=False
557545
)
558546
assert admin_response.status_code == 200
559547
assert '<form' in admin_response.text
@@ -562,7 +550,6 @@ def test_organization_page_invite_form_visibility(auth_client_owner, auth_client
562550
# Regular member should not see invitation form (lacks INVITE_USER permission)
563551
member_response = auth_client_member.get(
564552
app.url_path_for("read_organization", org_id=test_organization.id),
565-
follow_redirects=False
566553
)
567554
assert member_response.status_code == 200
568555

0 commit comments

Comments
 (0)