Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/auth/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@ async def require_member(
) -> Session:
"""Require session with active membership.

IRE staff (ire.org email addresses) are granted access regardless of
membership status.
Comment thread
palewire marked this conversation as resolved.
Outdated

Use for member-only endpoints.
"""
if not session.is_active_member:
is_ire_staff = session.email.lower().endswith("@ire.org")
if not is_ire_staff and not session.is_active_member:
from app.auth.exceptions import MembershipRequiredError

raise MembershipRequiredError()
Comment thread
palewire marked this conversation as resolved.
Expand Down
75 changes: 75 additions & 0 deletions tests/test_auth/test_require_member.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests for the require_member dependency."""

import time

import pytest

from app.auth.dependencies import require_member
from app.auth.exceptions import MembershipRequiredError
from app.auth.session import Session


def _make_session(email: str, is_active_member: bool) -> Session:
return Session(
session_id="test-session-id",
user_id="test-user-123",
email=email,
first_name="Test",
last_name="User",
full_name="Test User",
is_active_member=is_active_member,
membership_id=None,
created_at=time.time(),
expires_at=9999999999,
)


class TestRequireMember:
@pytest.mark.asyncio
async def test_active_member_allowed(self):
"""Regular active members are allowed access."""
session = _make_session("journalist@example.com", is_active_member=True)
result = await require_member(session)
assert result is session

@pytest.mark.asyncio
async def test_inactive_member_denied(self):
"""Non-members without an ire.org email are denied."""
session = _make_session("journalist@example.com", is_active_member=False)
with pytest.raises(MembershipRequiredError):
await require_member(session)

@pytest.mark.asyncio
async def test_ire_staff_allowed_without_membership(self):
"""IRE staff (ire.org email) bypass the membership check."""
session = _make_session("staff@ire.org", is_active_member=False)
result = await require_member(session)
assert result is session

@pytest.mark.asyncio
async def test_ire_staff_allowed_with_membership(self):
"""IRE staff with active membership are also allowed."""
session = _make_session("staff@ire.org", is_active_member=True)
result = await require_member(session)
assert result is session

@pytest.mark.asyncio
async def test_ire_staff_email_case_insensitive(self):
"""Email domain check is case-insensitive."""
session = _make_session("STAFF@IRE.ORG", is_active_member=False)
result = await require_member(session)
assert result is session

@pytest.mark.asyncio
async def test_non_ire_domain_denied(self):
"""Emails that merely contain ire.org but are not @ire.org are denied."""
session = _make_session("attacker@notire.org", is_active_member=False)
with pytest.raises(MembershipRequiredError):
await require_member(session)

@pytest.mark.asyncio
async def test_ire_org_subdomain_denied(self):
"""Subdomains of ire.org (e.g. staff.ire.org) are denied — only @ire.org is allowed."""
session = _make_session("user@staff.ire.org", is_active_member=False)
with pytest.raises(MembershipRequiredError):
await require_member(session)