Files
syndarix/backend/tests/e2e/test_organization_workflows.py
Felipe Cardoso 400d6f6f75 Enhance OAuth security and state validation
- Implemented stricter OAuth security measures, including CSRF protection via state parameter validation and redirect_uri checks.
- Updated OAuth models to support timezone-aware datetime comparisons, replacing deprecated `utcnow`.
- Enhanced logging for malformed Basic auth headers during token, introspect, and revoke requests.
- Added allowlist validation for OAuth provider domains to prevent open redirect attacks.
- Improved nonce validation for OpenID Connect tokens, ensuring token integrity during Google provider flows.
- Updated E2E and unit tests to cover new security features and expanded OAuth state handling scenarios.
2025-11-25 23:50:43 +01:00

330 lines
12 KiB
Python

"""
Organization E2E workflow tests with real PostgreSQL.
These tests validate complete organization workflows including:
- Creating organizations (via admin)
- Viewing user's organizations
- Organization membership management
- Organization updates
Usage:
make test-e2e # Run all E2E tests
"""
from uuid import uuid4
import pytest
pytestmark = [
pytest.mark.e2e,
pytest.mark.postgres,
pytest.mark.asyncio,
]
async def register_and_login(client, email: str, password: str = "SecurePassword123!"): # noqa: S107
"""Helper to register a user and get tokens."""
# Register
await client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Test",
"last_name": "User",
},
)
# Login
login_resp = await client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
return tokens
async def create_superuser_and_login(client, db_session):
"""Helper to create a superuser directly in DB and login."""
from app.crud.user import user as user_crud
from app.schemas.users import UserCreate
email = f"admin-{uuid4().hex[:8]}@example.com"
password = "AdminPassword123!"
# Create superuser directly
user_in = UserCreate(
email=email,
password=password,
first_name="Admin",
last_name="User",
is_superuser=True,
)
await user_crud.create(db_session, obj_in=user_in)
# Login
login_resp = await client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
return login_resp.json(), email
class TestOrganizationWorkflows:
"""Test organization management workflows."""
async def test_user_has_no_organizations_initially(self, e2e_client):
"""New users should have no organizations."""
email = f"e2e-{uuid4().hex[:8]}@example.com"
tokens = await register_and_login(e2e_client, email)
response = await e2e_client.get(
"/api/v1/organizations/me",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) == 0
async def test_get_organizations_requires_auth(self, e2e_client):
"""Organizations endpoint requires authentication."""
response = await e2e_client.get("/api/v1/organizations/me")
assert response.status_code == 401
async def test_get_nonexistent_organization(self, e2e_client):
"""Getting a non-member organization returns 403."""
email = f"e2e-{uuid4().hex[:8]}@example.com"
tokens = await register_and_login(e2e_client, email)
fake_org_id = str(uuid4())
response = await e2e_client.get(
f"/api/v1/organizations/{fake_org_id}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
# Should be 403 (not a member) or 404 (not found)
assert response.status_code in [403, 404]
class TestOrganizationMembershipWorkflows:
"""Test organization membership workflows."""
async def test_non_member_cannot_view_org_details(self, e2e_client):
"""Users cannot view organizations they're not members of."""
# Create two users
user1_email = f"e2e-user1-{uuid4().hex[:8]}@example.com"
user2_email = f"e2e-user2-{uuid4().hex[:8]}@example.com"
await register_and_login(e2e_client, user1_email)
user2_tokens = await register_and_login(e2e_client, user2_email)
# User2 tries to access a random org ID
fake_org_id = str(uuid4())
response = await e2e_client.get(
f"/api/v1/organizations/{fake_org_id}",
headers={"Authorization": f"Bearer {user2_tokens['access_token']}"},
)
assert response.status_code in [403, 404]
async def test_non_member_cannot_view_org_members(self, e2e_client):
"""Users cannot view members of organizations they don't belong to."""
email = f"e2e-{uuid4().hex[:8]}@example.com"
tokens = await register_and_login(e2e_client, email)
fake_org_id = str(uuid4())
response = await e2e_client.get(
f"/api/v1/organizations/{fake_org_id}/members",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert response.status_code in [403, 404]
async def test_non_admin_cannot_update_organization(self, e2e_client):
"""Regular users cannot update organizations (need admin role)."""
email = f"e2e-{uuid4().hex[:8]}@example.com"
tokens = await register_and_login(e2e_client, email)
fake_org_id = str(uuid4())
response = await e2e_client.put(
f"/api/v1/organizations/{fake_org_id}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Updated Name"},
)
assert response.status_code in [403, 404]
class TestOrganizationWithMembers:
"""Test organization workflows using e2e_org_with_members fixture."""
async def test_owner_can_view_organization(self, e2e_client, e2e_org_with_members):
"""Organization owner can view organization details."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/organizations/{org['org_id']}",
headers={"Authorization": f"Bearer {org['owner']['tokens']['access_token']}"},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == org["org_id"]
assert data["name"] == org["org_name"]
async def test_member_can_view_organization(self, e2e_client, e2e_org_with_members):
"""Organization member can view organization details."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/organizations/{org['org_id']}",
headers={"Authorization": f"Bearer {org['member']['tokens']['access_token']}"},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == org["org_id"]
async def test_owner_can_list_members(self, e2e_client, e2e_org_with_members):
"""Organization owner can list members."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/organizations/{org['org_id']}/members",
headers={"Authorization": f"Bearer {org['owner']['tokens']['access_token']}"},
)
assert response.status_code == 200
data = response.json()
# Should have owner + member = at least 2 members
assert len(data) >= 2
async def test_member_can_list_members(self, e2e_client, e2e_org_with_members):
"""Organization member can list members."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/organizations/{org['org_id']}/members",
headers={"Authorization": f"Bearer {org['member']['tokens']['access_token']}"},
)
assert response.status_code == 200
async def test_owner_appears_in_my_organizations(self, e2e_client, e2e_org_with_members):
"""Owner sees organization in their organizations list."""
org = e2e_org_with_members
response = await e2e_client.get(
"/api/v1/organizations/me",
headers={"Authorization": f"Bearer {org['owner']['tokens']['access_token']}"},
)
assert response.status_code == 200
data = response.json()
org_ids = [o["id"] for o in data]
assert org["org_id"] in org_ids
async def test_member_appears_in_my_organizations(self, e2e_client, e2e_org_with_members):
"""Member sees organization in their organizations list."""
org = e2e_org_with_members
response = await e2e_client.get(
"/api/v1/organizations/me",
headers={"Authorization": f"Bearer {org['member']['tokens']['access_token']}"},
)
assert response.status_code == 200
data = response.json()
org_ids = [o["id"] for o in data]
assert org["org_id"] in org_ids
async def test_owner_can_update_organization(self, e2e_client, e2e_org_with_members):
"""Organization owner can update organization details."""
org = e2e_org_with_members
new_description = f"Updated at {uuid4().hex[:8]}"
response = await e2e_client.put(
f"/api/v1/organizations/{org['org_id']}",
headers={"Authorization": f"Bearer {org['owner']['tokens']['access_token']}"},
json={"description": new_description},
)
assert response.status_code == 200
data = response.json()
assert data["description"] == new_description
async def test_member_cannot_update_organization(self, e2e_client, e2e_org_with_members):
"""Regular member cannot update organization details."""
org = e2e_org_with_members
response = await e2e_client.put(
f"/api/v1/organizations/{org['org_id']}",
headers={"Authorization": f"Bearer {org['member']['tokens']['access_token']}"},
json={"description": "Should fail"},
)
assert response.status_code == 403
async def test_non_member_cannot_view_organization(
self, e2e_client, e2e_org_with_members
):
"""Non-members cannot view organization details."""
org = e2e_org_with_members
# Create a new user who is not a member
non_member_email = f"nonmember-{uuid4().hex[:8]}@example.com"
tokens = await register_and_login(e2e_client, non_member_email)
response = await e2e_client.get(
f"/api/v1/organizations/{org['org_id']}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert response.status_code == 403
async def test_get_organization_by_slug(self, e2e_client, e2e_org_with_members):
"""Organization can be retrieved by slug."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/organizations/slug/{org['org_slug']}",
headers={
"Authorization": f"Bearer {org['owner']['tokens']['access_token']}"
},
)
# May be 200 or 404/403 depending on implementation
assert response.status_code in [200, 403, 404]
class TestOrganizationAdminOperations:
"""Test organization admin operations."""
async def test_admin_list_org_members_with_pagination(
self, e2e_client, e2e_superuser, e2e_org_with_members
):
"""Admin can list org members with pagination."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/admin/organizations/{org['org_id']}/members",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
params={"page": 1, "limit": 10},
)
assert response.status_code == 200
data = response.json()
assert "data" in data
assert "pagination" in data
async def test_admin_list_org_members_filter_active(
self, e2e_client, e2e_superuser, e2e_org_with_members
):
"""Admin can filter org members by active status."""
org = e2e_org_with_members
response = await e2e_client.get(
f"/api/v1/admin/organizations/{org['org_id']}/members",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
params={"is_active": True},
)
assert response.status_code == 200