remove junk

This commit is contained in:
2026-02-23 13:25:05 +10:30
parent 7637d7df45
commit cbdf6185b6
24 changed files with 0 additions and 6789 deletions
-1
View File
@@ -1 +0,0 @@
"""Integration tests package."""
-107
View File
@@ -1,107 +0,0 @@
"""Integration tests for authentication flow."""
import pytest
import json
@pytest.mark.integration
class TestAuthFlow:
"""Integration tests for authentication endpoints."""
def test_register_login_logout_flow(self, client, db):
"""Test complete registration, login, and logout flow."""
# Register
register_data = {
"email": "integration@example.com",
"password": "TestPassword123!",
"password_confirm": "TestPassword123!",
"full_name": "Integration Test",
}
response = client.post(
"/api/v1/auth/register",
data=json.dumps(register_data),
content_type="application/json",
)
assert response.status_code == 201
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
assert data["data"]["user"]["email"] == "integration@example.com"
# Logout
response = client.post("/api/v1/auth/logout")
assert response.status_code == 200
# Login
login_data = {
"email": "integration@example.com",
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
# Logout again
response = client.post("/api/v1/auth/logout")
assert response.status_code == 200
def test_get_current_user_authenticated(self, authenticated_client):
"""Test getting current user when authenticated."""
response = authenticated_client.get("/api/v1/auth/me")
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
def test_get_current_user_unauthenticated(self, client):
"""Test getting current user when not authenticated."""
response = client.get("/api/v1/auth/me")
assert response.status_code == 401
data = response.get_json()
assert data["success"] is False
def test_invalid_credentials(self, client, test_user):
"""Test login with invalid credentials."""
login_data = {
"email": test_user.email,
"password": "WrongPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 401
data = response.get_json()
assert data["success"] is False
def test_duplicate_registration(self, client, test_user):
"""Test registering with existing email."""
register_data = {
"email": test_user.email,
"password": "TestPassword123!",
"password_confirm": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/register",
data=json.dumps(register_data),
content_type="application/json",
)
assert response.status_code == 409
data = response.get_json()
assert data["success"] is False
@@ -1,696 +0,0 @@
"""Integration tests for external authentication API flows."""
import pytest
import json
from unittest.mock import patch, Mock
from gatehouse_app.services.external_auth_service import (
ExternalAuthService,
ExternalProviderConfig,
OAuthState,
)
from gatehouse_app.services.audit_service import AuditService
from gatehouse_app.utils.constants import AuthMethodType, OrganizationRole
from gatehouse_app.models import User, AuthenticationMethod, OrganizationMember
@pytest.mark.integration
class TestExternalAuthApiFlows:
"""Integration tests for external auth API flows."""
def test_complete_account_linking_flow(
self, app, db, client, test_user, test_organization
):
"""Test complete account linking flow: initiate → callback → complete."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
client_secret_encrypted="encrypted-secret",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
assert login_response.status_code == 200
token = login_response.get_json()["data"]["token"]
with patch.object(
ExternalAuthService, '_exchange_code'
) as mock_exchange, patch.object(
ExternalAuthService, '_get_user_info'
) as mock_get_user_info:
# Mock external provider responses
mock_exchange.return_value = {
"access_token": "mock-access-token",
"refresh_token": "mock-refresh-token",
"id_token": "mock-id-token",
"expires_in": 3600,
}
mock_get_user_info.return_value = {
"provider_user_id": "google-123",
"email": "user@gmail.com",
"email_verified": True,
"name": "Test User",
"picture": "https://example.com/avatar.jpg",
"raw_data": {},
}
# Step 1: Initiate link flow
initiate_response = client.post(
"/api/v1/auth/external/google/link",
json={},
headers={"Authorization": f"Bearer {token}"},
)
assert initiate_response.status_code == 200
initiate_data = initiate_response.get_json()
assert "authorization_url" in initiate_data["data"]
assert "state" in initiate_data["data"]
state = initiate_data["data"]["state"]
# Step 2: Simulate callback (complete link flow)
with patch.object(AuditService, 'log_external_auth_link_completed'):
complete_response = client.get(
f"/api/v1/auth/external/google/callback",
query_string={
"code": "mock-auth-code",
"state": state,
},
)
# The callback returns 200 on success
assert complete_response.status_code == 200
# Verify account is linked
auth_method = AuthenticationMethod.query.filter_by(
user_id=test_user.id,
method_type=AuthMethodType.GOOGLE,
provider_user_id="google-123",
).first()
assert auth_method is not None
def test_complete_login_flow(
self, app, db, client, test_user, test_organization
):
"""Test complete login flow: initiate → callback → authenticate."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
client_secret_encrypted="encrypted-secret",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Create authentication method for user
auth_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.GOOGLE,
provider_user_id="google-123",
provider_data={"email": test_user.email},
verified=True,
)
auth_method.save()
with patch.object(
ExternalAuthService, '_exchange_code'
) as mock_exchange, patch.object(
ExternalAuthService, '_get_user_info'
) as mock_get_user_info:
# Mock external provider responses
mock_exchange.return_value = {
"access_token": "mock-access-token",
"refresh_token": "mock-refresh-token",
"id_token": "mock-id-token",
"expires_in": 3600,
}
mock_get_user_info.return_value = {
"provider_user_id": "google-123",
"email": test_user.email,
"email_verified": True,
"name": "Test User",
"picture": "https://example.com/avatar.jpg",
"raw_data": {},
}
# Initiate login flow
login_init_response = client.get(
"/api/v1/auth/external/google/authorize",
query_string={"flow": "login"},
)
assert login_init_response.status_code == 200
login_init_data = login_init_response.get_json()
assert "authorization_url" in login_init_data["data"]
state = login_init_data["data"]["state"]
# Simulate callback
callback_response = client.get(
f"/api/v1/auth/external/google/callback",
query_string={
"code": "mock-auth-code",
"state": state,
},
)
assert callback_response.status_code == 200
callback_data = callback_response.get_json()
assert callback_data["success"] is True
assert callback_data["flow_type"] == "login"
assert "token" in callback_data["data"]
assert callback_data["data"]["user"]["id"] == test_user.id
def test_account_unlinking_flow(
self, app, db, client, test_user, test_organization
):
"""Test account unlinking flow."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Create password auth method
password_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.PASSWORD,
provider_user_id=test_user.id,
)
password_method.save()
# Create Google auth method
google_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.GOOGLE,
provider_user_id="google-123",
provider_data={"email": test_user.email},
verified=True,
)
google_method.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Unlink Google account
with patch.object(AuditService, 'log_external_auth_unlink'):
unlink_response = client.delete(
"/api/v1/auth/external/google/unlink",
headers={"Authorization": f"Bearer {token}"},
)
assert unlink_response.status_code == 200
unlink_data = unlink_response.get_json()
assert "success" in unlink_data or "message" in unlink_data
# Verify account is unlinked
auth_method = AuthenticationMethod.query.filter_by(
user_id=test_user.id,
method_type=AuthMethodType.GOOGLE,
).first()
assert auth_method is None
def test_provider_configuration_crud(
self, app, db, client, test_user, test_organization
):
"""Test provider configuration CRUD operations."""
with app.app_context():
# Create organization membership as admin
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.ADMIN,
)
member.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Step 1: Create provider config
with patch.object(AuditService, 'log_external_auth_config_create'):
create_response = client.post(
"/api/v1/auth/external/google/config",
json={
"client_id": "new-client-id",
"client_secret": "new-client-secret",
"scopes": ["openid", "profile", "email"],
"redirect_uris": ["http://localhost:3000/callback"],
},
headers={"Authorization": f"Bearer {token}"},
)
assert create_response.status_code == 201
create_data = create_response.get_json()
assert create_data["data"]["provider_type"] == "google"
assert create_data["data"]["client_id"] == "new-client-id"
config_id = create_data["data"]["id"]
# Step 2: List providers
list_response = client.get(
"/api/v1/auth/external/providers",
headers={"Authorization": f"Bearer {token}"},
)
assert list_response.status_code == 200
list_data = list_response.get_json()
google_provider = next(
p for p in list_data["data"]["providers"] if p["id"] == "google"
)
assert google_provider["is_configured"] is True
# Step 3: Get provider config
get_response = client.get(
"/api/v1/auth/external/google/config",
headers={"Authorization": f"Bearer {token}"},
)
assert get_response.status_code == 200
get_data = get_response.get_json()
assert get_data["data"]["client_id"] == "new-client-id"
# Step 4: Update provider config
with patch.object(AuditService, 'log_external_auth_config_update'):
update_response = client.post(
"/api/v1/auth/external/google/config",
json={
"client_id": "updated-client-id",
"client_secret": "updated-client-secret",
},
headers={"Authorization": f"Bearer {token}"},
)
assert update_response.status_code == 200
update_data = update_response.get_json()
assert update_data["data"]["client_id"] == "updated-client-id"
# Step 5: Delete provider config
with patch.object(AuditService, 'log_external_auth_config_delete'):
delete_response = client.delete(
"/api/v1/auth/external/google/config",
headers={"Authorization": f"Bearer {token}"},
)
assert delete_response.status_code == 200
# Verify deletion
get_deleted_response = client.get(
"/api/v1/auth/external/google/config",
headers={"Authorization": f"Bearer {token}"},
)
assert get_deleted_response.status_code == 404
def test_invalid_state_error(self, app, db, client, test_user, test_organization):
"""Test error handling for invalid OAuth state."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Try callback with invalid state
callback_response = client.get(
"/api/v1/auth/external/google/callback",
query_string={
"code": "mock-auth-code",
"state": "invalid-state",
},
)
assert callback_response.status_code == 400
callback_data = callback_response.get_json()
assert callback_data["error_type"] == "INVALID_STATE"
def test_expired_state_error(self, app, db, client, test_user, test_organization):
"""Test error handling for expired OAuth state."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Create expired state
state = OAuthState.create_state(
flow_type="login",
provider_type=AuthMethodType.GOOGLE,
organization_id=test_organization.id,
lifetime_seconds=-1, # Already expired
)
# Try callback with expired state
callback_response = client.get(
"/api/v1/auth/external/google/callback",
query_string={
"code": "mock-auth-code",
"state": state.state,
},
)
assert callback_response.status_code == 400
callback_data = callback_response.get_json()
assert callback_data["error_type"] == "INVALID_STATE"
def test_provider_not_configured_error(
self, app, db, client, test_user, test_organization
):
"""Test error handling when provider is not configured."""
with app.app_context():
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Try to link with unconfigured provider
link_response = client.post(
"/api/v1/auth/external/google/link",
json={},
headers={"Authorization": f"Bearer {token}"},
)
assert link_response.status_code == 400
link_data = link_response.get_json()
assert link_data["error_type"] == "PROVIDER_NOT_CONFIGURED"
def test_linked_accounts_list(self, app, db, client, test_user, test_organization):
"""Test listing linked accounts."""
with app.app_context():
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Create authentication methods
google_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.GOOGLE,
provider_user_id="google-123",
provider_data={
"email": test_user.email,
"name": "Test User",
"picture": "https://example.com/avatar.jpg",
},
verified=True,
)
google_method.save()
github_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.GITHUB,
provider_user_id="github-456",
provider_data={
"email": "user@github.com",
"name": "Test User",
},
verified=True,
)
github_method.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# List linked accounts
list_response = client.get(
"/api/v1/auth/external/linked-accounts",
headers={"Authorization": f"Bearer {token}"},
)
assert list_response.status_code == 200
list_data = list_response.get_json()
assert len(list_data["data"]["linked_accounts"]) == 2
assert list_data["data"]["unlink_available"] is True
def test_non_admin_cannot_manage_providers(
self, app, db, client, test_user, test_organization
):
"""Test that non-admin users cannot manage provider configurations."""
with app.app_context():
# Create organization membership as regular member
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Try to create provider config (should fail)
create_response = client.post(
"/api/v1/auth/external/google/config",
json={
"client_id": "client-id",
"client_secret": "client-secret",
},
headers={"Authorization": f"Bearer {token}"},
)
assert create_response.status_code == 403
assert create_response.get_json()["error_type"] == "FORBIDDEN"
def test_unsupported_provider_error(
self, app, db, client, test_user, test_organization
):
"""Test error handling for unsupported provider."""
with app.app_context():
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Try to link with unsupported provider
link_response = client.post(
"/api/v1/auth/external/unsupported/link",
json={},
headers={"Authorization": f"Bearer {token}"},
)
assert link_response.status_code == 400
link_data = link_response.get_json()
assert link_data["error_type"] == "UNSUPPORTED_PROVIDER"
@pytest.mark.integration
class TestExternalAuthAuditLogging:
"""Integration tests for audit logging in external auth flows."""
@patch('gatehouse_app.services.audit_service.AuditService')
def test_audit_log_on_link_initiated(
self, mock_audit, app, db, client, test_user, test_organization
):
"""Test audit log is created when link flow is initiated."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Initiate link flow
link_response = client.post(
"/api/v1/auth/external/google/link",
json={},
headers={"Authorization": f"Bearer {token}"},
)
# Verify audit log was called
mock_audit.log_external_auth_link_initiated.assert_called_once()
@patch('gatehouse_app.services.audit_service.AuditService')
def test_audit_log_on_unlink(
self, mock_audit, app, db, client, test_user, test_organization
):
"""Test audit log is created when account is unlinked."""
with app.app_context():
# Create provider config
config = ExternalProviderConfig(
organization_id=test_organization.id,
provider_type=AuthMethodType.GOOGLE.value,
client_id="test-client-id",
auth_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
userinfo_url="https://www.googleapis.com/oauth2/v3/userinfo",
scopes=["openid", "profile", "email"],
redirect_uris=["http://localhost:3000/callback"],
is_active=True,
)
config.save()
# Create organization membership
member = OrganizationMember(
user_id=test_user.id,
organization_id=test_organization.id,
role=OrganizationRole.MEMBER,
)
member.save()
# Create password auth method
password_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.PASSWORD,
provider_user_id=test_user.id,
)
password_method.save()
# Create Google auth method
google_method = AuthenticationMethod(
user_id=test_user.id,
method_type=AuthMethodType.GOOGLE,
provider_user_id="google-123",
provider_data={"email": test_user.email},
verified=True,
)
google_method.save()
# Login to get token
login_response = client.post(
"/api/v1/auth/login",
json={
"email": test_user.email,
"password": test_user._test_password,
},
)
token = login_response.get_json()["data"]["token"]
# Unlink Google account
unlink_response = client.delete(
"/api/v1/auth/external/google/unlink",
headers={"Authorization": f"Bearer {token}"},
)
# Verify audit log was called
mock_audit.log_external_auth_unlink.assert_called_once()
-933
View File
@@ -1,933 +0,0 @@
"""Integration tests for MFA compliance enforcement."""
import pytest
import json
from datetime import datetime, timezone, timedelta
from gatehouse_app.models.user import User
from gatehouse_app.models.organization import Organization
from gatehouse_app.models.organization_member import OrganizationMember
from gatehouse_app.models.organization_security_policy import OrganizationSecurityPolicy
from gatehouse_app.models.mfa_policy_compliance import MfaPolicyCompliance
from gatehouse_app.models.user_security_policy import UserSecurityPolicy
from gatehouse_app.models.session import Session
from gatehouse_app.utils.constants import MfaPolicyMode, MfaComplianceStatus, UserStatus, MfaRequirementOverride
from gatehouse_app.services.mfa_policy_service import MfaPolicyService
@pytest.mark.integration
class TestMfaComplianceLogin:
"""Integration tests for MFA compliance during login."""
def test_login_with_no_policy(self, client, db, test_user):
"""Test login with no MFA policy (should work normally)."""
login_data = {
"email": test_user.email,
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
assert "token" in data["data"]
# No MFA compliance info should be present when no policy exists
assert "mfa_compliance" not in data["data"]
assert "requires_mfa_enrollment" not in data["data"]
def test_login_with_optional_policy(self, client, db, test_user, test_organization):
"""Test login with optional MFA policy (should work normally)."""
# Create an optional MFA policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.OPTIONAL,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
login_data = {
"email": test_user.email,
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
assert "token" in data["data"]
# MFA compliance should be present but status should be not_applicable
assert "mfa_compliance" in data["data"]
assert data["data"]["mfa_compliance"]["overall_status"] == "not_applicable"
assert "requires_mfa_enrollment" not in data["data"]
def test_login_with_required_policy_in_grace_period(self, client, db, test_user, test_organization):
"""Test login with required policy within grace period (should work with warning)."""
# Create a required MFA policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
login_data = {
"email": test_user.email,
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
assert "token" in data["data"]
# MFA compliance should be present with in_grace status
assert "mfa_compliance" in data["data"]
assert data["data"]["mfa_compliance"]["overall_status"] == "in_grace"
assert "requires_mfa_enrollment" not in data["data"]
assert "totp" in data["data"]["mfa_compliance"]["missing_methods"]
def test_login_with_required_policy_after_deadline(self, client, db, test_user, test_organization):
"""Test login with required policy after deadline (should get compliance-only session)."""
# Create a required MFA policy with past deadline
past_deadline = datetime.now(timezone.utc) - timedelta(days=1)
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
# Create compliance record past due
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.PAST_DUE,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=15),
deadline_at=past_deadline,
)
db.session.add(compliance)
db.session.commit()
login_data = {
"email": test_user.email,
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
assert "token" in data["data"]
# Should have compliance-only session
assert data["data"]["requires_mfa_enrollment"] is True
assert "mfa_compliance" in data["data"]
assert data["data"]["mfa_compliance"]["overall_status"] in ["past_due", "suspended"]
def test_login_with_suspended_user(self, client, db, test_user, test_organization):
"""Test login with compliance suspended user (should get compliance-only session)."""
# Set user status to compliance suspended
test_user.status = UserStatus.COMPLIANCE_SUSPENDED
db.session.commit()
login_data = {
"email": test_user.email,
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "user" in data["data"]
assert "token" in data["data"]
# Should have compliance-only session
assert data["data"]["requires_mfa_enrollment"] is True
@pytest.mark.integration
class TestMfaComplianceAccess:
"""Integration tests for MFA compliance access control."""
def test_compliance_only_session_denied_full_access(self, client, db, test_user, test_organization):
"""Test that compliance-only session cannot access full access endpoints."""
# Create a required MFA policy with past deadline
past_deadline = datetime.now(timezone.utc) - timedelta(days=1)
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
# Create compliance record past due
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.PAST_DUE,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=15),
deadline_at=past_deadline,
)
db.session.add(compliance)
# Create a compliance-only session
session = Session(
user_id=test_user.id,
token="compliance_only_token",
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
is_compliance_only=True,
)
db.session.add(session)
db.session.commit()
# Try to access a full-access endpoint (get_my_organizations)
response = client.get(
"/api/v1/users/me/organizations",
headers={"Authorization": "Bearer compliance_only_token"},
)
assert response.status_code == 403
data = response.get_json()
assert data["success"] is False
assert data["error_type"] == "MFA_COMPLIANCE_REQUIRED"
def test_compliance_only_session_can_access_mfa_enrollment(self, client, db, test_user, test_organization):
"""Test that compliance-only session can access MFA enrollment endpoints."""
# Create a required MFA policy with past deadline
past_deadline = datetime.now(timezone.utc) - timedelta(days=1)
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
# Create compliance record past due
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.PAST_DUE,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=15),
deadline_at=past_deadline,
)
db.session.add(compliance)
# Create a compliance-only session
session = Session(
user_id=test_user.id,
token="compliance_only_token",
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
is_compliance_only=True,
)
db.session.add(session)
db.session.commit()
# Try to access MFA enrollment endpoint (should work)
response = client.get(
"/api/v1/auth/totp/status",
headers={"Authorization": "Bearer compliance_only_token"},
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
def test_compliance_only_session_can_access_logout(self, client, db, test_user, test_organization):
"""Test that compliance-only session can access logout endpoint."""
# Create a required MFA policy with past deadline
past_deadline = datetime.now(timezone.utc) - timedelta(days=1)
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
# Create compliance record past due
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.PAST_DUE,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=15),
deadline_at=past_deadline,
)
db.session.add(compliance)
# Create a compliance-only session
session = Session(
user_id=test_user.id,
token="compliance_only_token",
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
is_compliance_only=True,
)
db.session.add(session)
db.session.commit()
# Try to access logout endpoint (should work)
response = client.post(
"/api/v1/auth/logout",
headers={"Authorization": "Bearer compliance_only_token"},
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
@pytest.mark.integration
class TestMfaComplianceWebAuthn:
"""Integration tests for MFA compliance with WebAuthn login."""
def test_webauthn_login_with_required_policy_in_grace_period(self, client, db, test_user, test_organization):
"""Test WebAuthn login with required policy within grace period."""
# Create a required MFA policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Note: Full WebAuthn login test would require WebAuthn setup
# This test verifies the compliance response structure
login_data = {
"email": test_user.email,
"password": "TestPassword123!",
}
response = client.post(
"/api/v1/auth/login",
data=json.dumps(login_data),
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert data["success"] is True
assert "mfa_compliance" in data["data"]
assert data["data"]["mfa_compliance"]["overall_status"] == "in_grace"
@pytest.mark.integration
class TestMfaComplianceOIDC:
"""Integration tests for MFA compliance with OIDC authorization."""
def test_oidc_authorize_with_compliance_required(self, client, db, test_user, test_organization, app):
"""Test OIDC authorize with compliance required (should show error)."""
# Create a required MFA policy with past deadline
past_deadline = datetime.now(timezone.utc) - timedelta(days=1)
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
# Create compliance record past due
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.PAST_DUE,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=15),
deadline_at=past_deadline,
)
db.session.add(compliance)
db.session.commit()
# Try OIDC authorize with credentials
response = client.post(
"/oidc/authorize",
data={
"client_id": "test_client",
"redirect_uri": "http://localhost:8080/callback",
"response_type": "code",
"scope": "openid profile email",
"state": "test_state",
"email": test_user.email,
"password": "TestPassword123!",
},
)
# Should return login page with error
assert response.status_code == 200
assert b"Your account requires multi factor enrollment before using single sign on" in response.data
# =============================================================================
# Phase 4: Edge Case Tests
# =============================================================================
@pytest.mark.integration
class TestMfaComplianceMultiOrg:
"""Integration tests for multi-organization MFA compliance edge cases."""
def test_user_with_multiple_orgs_different_policies(self, client, db, test_user):
"""Test user belonging to multiple orgs with different MFA policies."""
# Create two organizations
org1 = Organization(
name="Org1",
slug="org1-test-multi",
)
org2 = Organization(
name="Org2",
slug="org2-test-multi",
)
db.session.add_all([org1, org2])
db.session.commit()
# Add user to both orgs
membership1 = OrganizationMember(
user_id=test_user.id,
organization_id=org1.id,
role="member",
)
membership2 = OrganizationMember(
user_id=test_user.id,
organization_id=org2.id,
role="member",
)
db.session.add_all([membership1, membership2])
db.session.commit()
# Create different policies for each org
# Org1: OPTIONAL (no requirement)
policy1 = OrganizationSecurityPolicy(
organization_id=org1.id,
mfa_policy_mode=MfaPolicyMode.OPTIONAL,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
# Org2: REQUIRE_TOTP (strictest)
policy2 = OrganizationSecurityPolicy(
organization_id=org2.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add_all([policy1, policy2])
db.session.commit()
# Evaluate user MFA state
compliance_summary = MfaPolicyService.evaluate_user_mfa_state(test_user)
# Overall status should reflect the strictest policy (REQUIRE_TOTP from org2)
assert compliance_summary.overall_status == MfaComplianceStatus.IN_GRACE.value
assert "totp" in compliance_summary.missing_methods
# Verify per-org breakdown
assert len(compliance_summary.orgs) == 2
org1_status = next((o for o in compliance_summary.orgs if o.organization_id == org1.id), None)
org2_status = next((o for o in compliance_summary.orgs if o.organization_id == org2.id), None)
assert org1_status is not None
assert org2_status is not None
assert org1_status.status == MfaComplianceStatus.NOT_APPLICABLE.value
assert org2_status.status == MfaComplianceStatus.IN_GRACE.value
def test_user_with_multiple_orgs_all_suspended(self, client, db, test_user):
"""Test user with multiple orgs where all require MFA and are past due."""
# Create two organizations
org1 = Organization(
name="Org1",
slug="org1-test-suspended",
)
org2 = Organization(
name="Org2",
slug="org2-test-suspended",
)
db.session.add_all([org1, org2])
db.session.commit()
# Add user to both orgs
membership1 = OrganizationMember(
user_id=test_user.id,
organization_id=org1.id,
role="member",
)
membership2 = OrganizationMember(
user_id=test_user.id,
organization_id=org2.id,
role="member",
)
db.session.add_all([membership1, membership2])
db.session.commit()
# Create required policies
policy1 = OrganizationSecurityPolicy(
organization_id=org1.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
policy2 = OrganizationSecurityPolicy(
organization_id=org2.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add_all([policy1, policy2])
db.session.commit()
# Create past-due compliance records for both
past_deadline = datetime.now(timezone.utc) - timedelta(days=1)
compliance1 = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=org1.id,
status=MfaComplianceStatus.SUSPENDED,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=30),
deadline_at=past_deadline,
suspended_at=past_deadline,
)
compliance2 = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=org2.id,
status=MfaComplianceStatus.SUSPENDED,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=30),
deadline_at=past_deadline,
suspended_at=past_deadline,
)
db.session.add_all([compliance1, compliance2])
db.session.commit()
# Evaluate user MFA state
compliance_summary = MfaPolicyService.evaluate_user_mfa_state(test_user)
# Overall status should be SUSPENDED
assert compliance_summary.overall_status == MfaComplianceStatus.SUSPENDED.value
def test_strictest_mode_selection(self):
"""Test that get_strictest_mode returns the most restrictive policy."""
modes = [
MfaPolicyMode.DISABLED.value,
MfaPolicyMode.OPTIONAL.value,
MfaPolicyMode.REQUIRE_TOTP.value,
]
result = MfaPolicyService.get_strictest_mode(modes)
assert result == MfaPolicyMode.REQUIRE_TOTP.value
# Test with REQUIRE_TOTP_OR_WEBAUTHN (strictest)
modes_strictest = [
MfaPolicyMode.REQUIRE_TOTP.value,
MfaPolicyMode.REQUIRE_WEBAUTHN.value,
MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN.value,
]
result = MfaPolicyService.get_strictest_mode(modes_strictest)
assert result == MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN.value
@pytest.mark.integration
class TestMfaComplianceUserOverrides:
"""Integration tests for user override edge cases."""
def test_user_override_inherit_mode(self, client, db, test_user, test_organization):
"""Test INHERIT mode - org policy applies as is."""
# Create a required policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Create INHERIT override (default behavior)
override = UserSecurityPolicy(
user_id=test_user.id,
organization_id=test_organization.id,
mfa_override_mode=MfaRequirementOverride.INHERIT,
)
db.session.add(override)
db.session.commit()
# Get effective policy
effective = MfaPolicyService.get_effective_user_policy(test_user.id, test_organization.id)
# Should inherit org policy
assert effective.effective_mode == MfaPolicyMode.REQUIRE_TOTP.value
assert effective.requires_totp is True
assert effective.is_exempt is False
def test_user_override_required_mode(self, client, db, test_user, test_organization):
"""Test REQUIRED mode - user always required to have MFA."""
# Create an optional policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.OPTIONAL,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Create REQUIRED override
override = UserSecurityPolicy(
user_id=test_user.id,
organization_id=test_organization.id,
mfa_override_mode=MfaRequirementOverride.REQUIRED,
)
db.session.add(override)
db.session.commit()
# Get effective policy
effective = MfaPolicyService.get_effective_user_policy(test_user.id, test_organization.id)
# Should be upgraded to REQUIRE_TOTP_OR_WEBAUTHN
assert effective.effective_mode == MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN.value
assert effective.requires_totp is True
assert effective.requires_webauthn is True
assert effective.is_exempt is False
def test_user_override_exempt_mode(self, client, db, test_user, test_organization):
"""Test EXEMPT mode - org policy does not apply."""
# Create a required policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Create EXEMPT override
override = UserSecurityPolicy(
user_id=test_user.id,
organization_id=test_organization.id,
mfa_override_mode=MfaRequirementOverride.EXEMPT,
)
db.session.add(override)
db.session.commit()
# Get effective policy
effective = MfaPolicyService.get_effective_user_policy(test_user.id, test_organization.id)
# Should be exempt from policy
assert effective.is_exempt is True
assert effective.effective_mode == MfaPolicyMode.DISABLED.value
assert effective.requires_totp is False
assert effective.requires_webauthn is False
def test_get_override_summary(self, client, db, test_user, test_organization):
"""Test getting override summary for a user."""
# No override exists
summary = MfaPolicyService.get_override_summary(test_user.id, test_organization.id)
assert summary["has_override"] is False
assert summary["mode"] == "inherit"
# Create an override
override = UserSecurityPolicy(
user_id=test_user.id,
organization_id=test_organization.id,
mfa_override_mode=MfaRequirementOverride.EXEMPT,
)
db.session.add(override)
db.session.commit()
# Get summary again
summary = MfaPolicyService.get_override_summary(test_user.id, test_organization.id)
assert summary["has_override"] is True
assert summary["mode"] == "exempt"
assert summary["is_exempt"] is True
@pytest.mark.integration
class TestMfaCompliancePolicyChanges:
"""Integration tests for policy changes affecting existing users."""
def test_policy_change_triggers_compliance_reevaluation(self, client, db, test_user, test_organization):
"""Test that policy change triggers compliance reevaluation."""
# Create initial optional policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.OPTIONAL,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Create compliance record (should be NOT_APPLICABLE)
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.NOT_APPLICABLE,
policy_version=1,
)
db.session.add(compliance)
db.session.commit()
# Update policy to REQUIRE_TOTP
MfaPolicyService.create_org_policy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP,
mfa_grace_period_days=14,
notify_days_before=7,
updated_by_user_id=test_user.id,
)
# Reevaluate all compliance
updated_count = MfaPolicyService.reevaluate_all_org_compliance(test_organization.id)
# Should have updated at least one record
assert updated_count >= 1
# Check compliance status was updated
updated_compliance = MfaPolicyService.get_user_compliance(test_user.id, test_organization.id)
assert updated_compliance.status == MfaComplianceStatus.IN_GRACE.value
assert updated_compliance.deadline_at is not None
def test_policy_relaxation_clears_requirements(self, client, db, test_user, test_organization):
"""Test that relaxing policy clears compliance requirements."""
# Create required policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Create IN_GRACE compliance record
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.IN_GRACE,
policy_version=1,
applied_at=datetime.now(timezone.utc),
deadline_at=datetime.now(timezone.utc) + timedelta(days=14),
)
db.session.add(compliance)
db.session.commit()
# Update policy to OPTIONAL
MfaPolicyService.create_org_policy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.OPTIONAL,
mfa_grace_period_days=14,
notify_days_before=7,
updated_by_user_id=test_user.id,
)
# Reevaluate compliance
MfaPolicyService.reevaluate_all_org_compliance(test_organization.id)
# Check compliance status was updated to NOT_APPLICABLE
updated_compliance = MfaPolicyService.get_user_compliance(test_user.id, test_organization.id)
assert updated_compliance.status == MfaComplianceStatus.NOT_APPLICABLE.value
@pytest.mark.integration
class TestMfaComplianceScheduledJob:
"""Integration tests for the MFA compliance scheduled job."""
def test_transition_to_suspended(self, client, db, test_user, test_organization):
"""Test that past-due users are transitioned to suspended."""
# Create required policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# Create past-due compliance record
past_deadline = datetime.now(timezone.utc) - timedelta(hours=1)
compliance = MfaPolicyCompliance(
user_id=test_user.id,
organization_id=test_organization.id,
status=MfaComplianceStatus.PAST_DUE,
policy_version=1,
applied_at=datetime.now(timezone.utc) - timedelta(days=15),
deadline_at=past_deadline,
)
db.session.add(compliance)
db.session.commit()
# Run the job
now = datetime.now(timezone.utc)
suspended_count = MfaPolicyService.transition_to_suspended_if_past_due(now)
# Should have suspended the user
assert suspended_count >= 1
# Check compliance status
updated_compliance = MfaPolicyService.get_user_compliance(test_user.id, test_organization.id)
assert updated_compliance.status == MfaComplianceStatus.SUSPENDED.value
assert updated_compliance.suspended_at is not None
# Check user status
db.refresh(test_user)
assert test_user.status == UserStatus.COMPLIANCE_SUSPENDED
def test_check_and_restore_user_status(self, client, db, test_user, test_organization):
"""Test that suspended users are restored when they become compliant."""
# Create required policy
policy = OrganizationSecurityPolicy(
organization_id=test_organization.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add(policy)
db.session.commit()
# User is suspended
test_user.status = UserStatus.COMPLIANCE_SUSPENDED
db.session.commit()
# Create EXEMPT override to clear requirement
override = UserSecurityPolicy(
user_id=test_user.id,
organization_id=test_organization.id,
mfa_override_mode=MfaRequirementOverride.EXEMPT,
)
db.session.add(override)
db.session.commit()
# Check and restore status
restored = MfaPolicyService.check_and_restore_user_status(test_user.id)
# Should have restored user
assert restored is True
db.refresh(test_user)
assert test_user.status == UserStatus.ACTIVE
@pytest.mark.integration
class TestMfaComplianceMultiOrgAggregate:
"""Integration tests for multi-org aggregate state calculation."""
def test_get_multi_org_aggregate_state(self, client, db, test_user):
"""Test aggregate state calculation for multi-org user."""
# Create two organizations
org1 = Organization(
name="AggOrg1",
slug="agg-org1-test",
)
org2 = Organization(
name="AggOrg2",
slug="agg-org2-test",
)
db.session.add_all([org1, org2])
db.session.commit()
# Add user to both
membership1 = OrganizationMember(
user_id=test_user.id,
organization_id=org1.id,
role="member",
)
membership2 = OrganizationMember(
user_id=test_user.id,
organization_id=org2.id,
role="member",
)
db.session.add_all([membership1, membership2])
db.session.commit()
# Create policies
policy1 = OrganizationSecurityPolicy(
organization_id=org1.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_TOTP,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
policy2 = OrganizationSecurityPolicy(
organization_id=org2.id,
mfa_policy_mode=MfaPolicyMode.REQUIRE_WEBAUTHN,
mfa_grace_period_days=14,
notify_days_before=7,
policy_version=1,
)
db.session.add_all([policy1, policy2])
db.session.commit()
# Get aggregate state
aggregate = MfaPolicyService.get_multi_org_aggregate_state(test_user)
# Verify structure
assert "overall_status" in aggregate
assert "strictest_mode" in aggregate
assert "missing_methods" in aggregate
assert "requiring_org_count" in aggregate
assert "requiring_orgs" in aggregate
assert "per_org_details" in aggregate
# Strictest mode should be REQUIRE_TOTP_OR_WEBAUTHN
assert aggregate["strictest_mode"] == MfaPolicyMode.REQUIRE_TOTP_OR_WEBAUTHN.value
# Both orgs should require MFA
assert aggregate["requiring_org_count"] == 2
assert len(aggregate["requiring_orgs"]) == 2
assert len(aggregate["per_org_details"]) == 2
File diff suppressed because it is too large Load Diff