"""Multi-workspace login, selection, switching, and tenant-scoped API keys.""" import pytest from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password from django.test import RequestFactory from rest_framework_simplejwt.tokens import AccessToken from infrasynth.shared.crypto import decrypt from infrasynth.tenancy.context import get_current_tenant from infrasynth.tenancy.models import Tenant, TenantMembership pytestmark = pytest.mark.django_db UserModel = get_user_model() LOGIN = "/api/v1/auth/login/" SELECT = "/api/v1/auth/select-workspace/" SWITCH = "/api/v1/auth/switch-workspace/" CREDS = {"username": "testuser", "password": "testpass123"} class TestLoginWorkspaceFlow: def test_single_membership_issues_tenant_token(self, api_client, user, tenant): resp = api_client.post(LOGIN, CREDS, format="json") assert resp.status_code == 200 assert resp.json()["tenant"] == str(tenant.pk) token = AccessToken(decrypt(api_client.cookies["access_token"].value)) assert token["tenant"] == str(tenant.pk) def test_multiple_memberships_returns_picker_without_access_token(self, api_client, user, tenant): other = Tenant.objects.create(slug="second", name="Second") TenantMembership.objects.create(tenant=other, user=user, is_active=True) resp = api_client.post(LOGIN, CREDS, format="json") assert resp.status_code == 200 assert len(resp.json()["workspaces"]) == 2 assert "access_token" not in resp.cookies def test_select_workspace_issues_bound_tokens(self, api_client, user, tenant): other = Tenant.objects.create(slug="second", name="Second") TenantMembership.objects.create(tenant=other, user=user, is_active=True) assert api_client.post(LOGIN, CREDS, format="json").status_code == 200 resp = api_client.post(SELECT, {"tenantId": str(other.pk)}, format="json") assert resp.status_code == 200 assert resp.json()["tenant"] == str(other.pk) token = AccessToken(decrypt(api_client.cookies["access_token"].value)) assert token["tenant"] == str(other.pk) def test_select_workspace_without_pending_session(self, api_client): resp = api_client.post(SELECT, {"tenantId": "whatever"}, format="json") assert resp.status_code == 401 def test_select_workspace_rejects_foreign_tenant(self, api_client, user, tenant): other = Tenant.objects.create(slug="second", name="Second") TenantMembership.objects.create(tenant=other, user=user, is_active=True) api_client.post(LOGIN, CREDS, format="json") stranger = Tenant.objects.create(slug="stranger", name="Stranger") resp = api_client.post(SELECT, {"tenantId": str(stranger.pk)}, format="json") assert resp.status_code == 404 def test_switch_workspace_requires_auth(self, api_client, tenant): resp = api_client.post(SWITCH, {"tenantId": str(tenant.pk)}, format="json") assert resp.status_code == 401 class TestAPIKeyTenantScope: def test_api_key_authentication_binds_tenant(self, tenant): from infrasynth.security.auth.api_keys import APIKeyAuthentication from infrasynth.security.models import APIKey APIKey.all_objects.create( tenant=tenant, name="svc", prefix="abcd1234", key_hash=make_password("secret"), scopes=["read:users"], ) request = RequestFactory().get("/", HTTP_X_API_KEY="abcd1234.secret") user, auth = APIKeyAuthentication().authenticate(request) assert user.tenant_id == tenant.id assert user.scopes == ["read:users"] assert get_current_tenant() == tenant