015c622016
Add 162 integration tests covering authentication flows, TOTP MFA, SSH key/certificate management, organization workflows, multi-org access, self-service features, admin operations, authorization, security edge cases, department/principal management, CA management, policy compliance, WebAuthn passkeys, and ZeroTier network access. Includes: - Reusable API client library with session management - Test fixtures for users, organizations, memberships, and CAs - Helper functions for SSH key generation and verification - Documentation for running and writing tests Also update test configuration to disable conflicting maas plugins and configure WebAuthn/session settings for localhost testing.
214 lines
9.8 KiB
Python
214 lines
9.8 KiB
Python
"""Admin operations integration tests.
|
|
|
|
Covers user suspension, MFA removal, password reset, and hard deletion.
|
|
All endpoints require admin/superadmin privileges.
|
|
"""
|
|
import pytest
|
|
|
|
from tests.integration.client.base import ApiError
|
|
from gatehouse_app.utils.constants import OrganizationRole
|
|
|
|
|
|
def assert_success(response: dict, message_contains: str = "") -> dict:
|
|
data = response.get("data", {})
|
|
assert response.get("success") is not False, (
|
|
f"Expected success but got error: {response.get('message')}"
|
|
)
|
|
if message_contains:
|
|
assert message_contains.lower() in response.get("message", "").lower()
|
|
return data
|
|
|
|
|
|
def assert_error(exc: ApiError, expected_status: int, expected_error_type: str | None = None):
|
|
assert exc.status_code == expected_status, (
|
|
f"Expected status {expected_status} but got {exc.status_code}"
|
|
)
|
|
if expected_error_type:
|
|
assert exc.error_type == expected_error_type
|
|
|
|
|
|
class TestAdminUserManagement:
|
|
"""Test admin-only user management endpoints."""
|
|
|
|
def test_list_users_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-01 — List all users as admin.
|
|
|
|
WHAT: Create an admin user, login, then GET /admin/users.
|
|
WHY: The user management page needs a paginated user list.
|
|
EXPECTED: 200 OK with users array.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.admin.list_users()
|
|
data = assert_success(result)
|
|
assert "users" in data or "count" in data
|
|
|
|
def test_list_users_non_admin_negative(self, integration_client, create_test_user):
|
|
"""TEST: ADMIN-02 — Reject listing users as non-admin.
|
|
|
|
WHAT: Regular user attempts GET /admin/users.
|
|
WHY: User lists contain sensitive data; must be admin-only.
|
|
EXPECTED: 403 Forbidden.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.admin.list_users()
|
|
|
|
assert exc_info.value.status_code == 403
|
|
|
|
def test_suspend_user_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-03 — Suspend user account.
|
|
|
|
WHAT: Admin suspends a user, then verify the user cannot login.
|
|
WHY: Suspension is a critical security tool for compromised
|
|
accounts.
|
|
EXPECTED: 200 OK on suspend. Login returns 403.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
victim = create_test_user(password="VictimPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
create_test_membership(victim["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.admin.suspend_user(victim["id"])
|
|
assert_success(result)
|
|
|
|
# Verify victim cannot login
|
|
integration_client.auth.logout()
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.login(email=victim["email"], password="VictimPass123!")
|
|
assert exc_info.value.status_code == 403
|
|
|
|
def test_unsuspend_user_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-05 — Unsuspend user account.
|
|
|
|
WHAT: Admin suspends then unsuspends a user, verify they can
|
|
login again.
|
|
WHY: False positives happen; admins must be able to restore
|
|
access.
|
|
EXPECTED: 200 OK on unsuspend. Login succeeds afterwards.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
victim = create_test_user(password="VictimPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
create_test_membership(victim["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
integration_client.admin.suspend_user(victim["id"])
|
|
result = integration_client.admin.unsuspend_user(victim["id"])
|
|
assert_success(result)
|
|
|
|
integration_client.auth.logout()
|
|
login_result = integration_client.auth.login(email=victim["email"], password="VictimPass123!")
|
|
assert_success(login_result, "login successful")
|
|
|
|
def test_admin_verify_email_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-07 — Admin verifies user email.
|
|
|
|
WHAT: Create an unverified user, admin calls verify endpoint.
|
|
WHY: Admins may need to bypass verification for support
|
|
reasons.
|
|
EXPECTED: 200 OK, user.email_verified becomes True.
|
|
"""
|
|
from gatehouse_app.models.user.user import User
|
|
from gatehouse_app.extensions import db
|
|
|
|
admin = create_test_user(password="AdminPass123!")
|
|
victim = create_test_user(password="VictimPass123!", email_verified=False)
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
create_test_membership(victim["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.admin.verify_user_email(victim["id"])
|
|
assert_success(result)
|
|
|
|
with integration_app.app_context():
|
|
user = User.query.get(victim["id"])
|
|
assert user.email_verified is True
|
|
|
|
def test_admin_set_password_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-08 — Admin sets user password.
|
|
|
|
WHAT: Admin overrides a user's password, then verify the user
|
|
can login with the new password.
|
|
WHY: Account recovery when user has lost access to email/MFA.
|
|
EXPECTED: 200 OK. Login with new password succeeds.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
victim = create_test_user(password="VictimPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
create_test_membership(victim["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.admin.set_user_password(victim["id"], "NewAdminSet456!")
|
|
assert_success(result)
|
|
|
|
integration_client.auth.logout()
|
|
login_result = integration_client.auth.login(email=victim["email"], password="NewAdminSet456!")
|
|
assert_success(login_result, "login successful")
|
|
|
|
def test_admin_remove_totp_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-10 — Admin removes user TOTP.
|
|
|
|
WHAT: User enrolls TOTP, admin removes it.
|
|
WHY: Account recovery when user lost their authenticator.
|
|
EXPECTED: 200 OK. TOTP status returns disabled.
|
|
"""
|
|
import pyotp
|
|
|
|
admin = create_test_user(password="AdminPass123!")
|
|
victim = create_test_user(password="VictimPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
create_test_membership(victim["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
# Victim enrolls TOTP
|
|
integration_client.auth.login(email=victim["email"], password="VictimPass123!")
|
|
enroll = integration_client.mfa.enroll_totp()
|
|
secret = enroll["data"]["secret"]
|
|
integration_client.mfa.verify_enrollment(pyotp.TOTP(secret).now())
|
|
integration_client.auth.logout()
|
|
|
|
# Admin removes TOTP
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.admin.remove_user_mfa(victim["id"], "totp")
|
|
assert_success(result)
|
|
|
|
# Verify victim's TOTP is disabled
|
|
integration_client.auth.logout()
|
|
integration_client.auth.login(email=victim["email"], password="VictimPass123!")
|
|
status = integration_client.mfa.get_totp_status()
|
|
assert status["data"].get("totp_enabled") is False
|
|
|
|
def test_admin_hard_delete_user_positive(self, integration_app, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ADMIN-11 — Admin hard-deletes user.
|
|
|
|
WHAT: Admin hard-deletes a user, verify they cannot login.
|
|
WHY: GDPR compliance and removing malicious actors.
|
|
EXPECTED: 200 OK. Login fails (user no longer exists).
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
victim = create_test_user(password="VictimPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.OWNER)
|
|
create_test_membership(victim["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.admin.hard_delete_user(victim["id"], confirm=True)
|
|
assert_success(result)
|
|
|
|
# Verify victim cannot login
|
|
integration_client.auth.logout()
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.auth.login(email=victim["email"], password="VictimPass123!")
|
|
assert exc_info.value.status_code in (400, 401)
|