""" tests/test_admin_phase2.py Phase 2 admin portal tests: system users, plans, tenants, billing, settings override, audit log, analytics. Uses the testing app with SQLite in-memory DB. """ import pytest from app.extensions import db as _db, bcrypt as _bcrypt from app.models.platform import SystemUser, Plan, Tenant, TenantBillingHistory, TenantSettingOverride, AuditLog # ── Helpers ─────────────────────────────────────────────────────────────────── def _login_admin(client, email="admin@test.com", password="AdminPass1A"): return client.post("/login", data={"email": email, "password": password}, follow_redirects=True) def _create_admin(app, email="admin@test.com", password="AdminPass1A"): with app.app_context(): if SystemUser.query.filter_by(email=email).first(): return u = SystemUser( email=email, password_hash=_bcrypt.generate_password_hash(password).decode("utf-8"), name="Test Admin", role="superadmin", is_active=True, ) _db.session.add(u) _db.session.commit() def _create_tenant(app, slug="test-salon"): with app.app_context(): plan = Plan.query.first() if Tenant.query.filter_by(slug=slug).first(): return t = Tenant(slug=slug, name="Test Salon", owner_email="owner@test.com", plan_id=plan.id, status="trial") _db.session.add(t) _db.session.commit() return t.id # ── System Users ────────────────────────────────────────────────────────────── class TestSystemUsers: def test_index_requires_login(self, admin_client): resp = admin_client.get("/system-users/", follow_redirects=False) assert resp.status_code in (302, 401) def test_index_accessible_to_admin(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.get("/system-users/") assert resp.status_code == 200 def test_create_system_user(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.post("/system-users/new", data={ "email": "newadmin@test.com", "name": "New Admin", "role": "superadmin", "password": "NewAdmin1A", "confirm_password": "NewAdmin1A", }, follow_redirects=True) assert resp.status_code == 200 with admin_app.app_context(): assert SystemUser.query.filter_by(email="newadmin@test.com").first() def test_create_user_weak_password_rejected(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.post("/system-users/new", data={ "email": "weak@test.com", "name": "Weak", "role": "superadmin", "password": "weak", "confirm_password": "weak", }, follow_redirects=True) assert b"password" in resp.data.lower() with admin_app.app_context(): assert SystemUser.query.filter_by(email="weak@test.com").first() is None def test_create_user_duplicate_email_rejected(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.post("/system-users/new", data={ "email": "admin@test.com", "name": "Dupe", "role": "superadmin", "password": "AdminPass1A", "confirm_password": "AdminPass1A", }, follow_redirects=True) assert b"already exists" in resp.data # ── Plans ───────────────────────────────────────────────────────────────────── class TestPlans: def test_plans_index(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.get("/plans/") assert resp.status_code == 200 assert b"Starter" in resp.data # seeded in conftest def test_create_plan(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.post("/plans/new", data={ "name": "Enterprise", "price_monthly": "199.00", "max_staff": "", "max_locations": "", "feature_pos": "1", "feature_marketing": "1", "is_active": "1", }, follow_redirects=True) assert resp.status_code == 200 with admin_app.app_context(): assert Plan.query.filter_by(name="Enterprise").first() def test_plan_missing_name_rejected(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.post("/plans/new", data={ "name": "", "price_monthly": "99", }, follow_redirects=True) assert b"required" in resp.data.lower() # ── Tenants ─────────────────────────────────────────────────────────────────── class TestTenants: def test_tenants_index(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.get("/tenants/") assert resp.status_code == 200 def test_create_tenant(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) with admin_app.app_context(): plan = Plan.query.first() resp = admin_client.post("/tenants/new", data={ "slug": "brand-new-salon", "name": "Brand New Salon", "owner_email": "owner@brandnew.com", "plan_id": str(plan.id), "trial_days": "14", "owner_password": "OwnerPass1A", "confirm_password": "OwnerPass1A", }, follow_redirects=True) assert resp.status_code == 200 with admin_app.app_context(): assert Tenant.query.filter_by(slug="brand-new-salon").first() def test_duplicate_slug_rejected(self, admin_app, admin_client): _create_admin(admin_app) _create_tenant(admin_app, slug="dup-slug") _login_admin(admin_client) with admin_app.app_context(): plan = Plan.query.first() resp = admin_client.post("/tenants/new", data={ "slug": "dup-slug", "name": "Dup", "owner_email": "dup@test.com", "plan_id": str(plan.id), "owner_password": "OwnerPass1A", "confirm_password": "OwnerPass1A", }, follow_redirects=True) assert b"already taken" in resp.data def test_invalid_slug_rejected(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) with admin_app.app_context(): plan = Plan.query.first() resp = admin_client.post("/tenants/new", data={ "slug": "INVALID SLUG!", "name": "Bad", "owner_email": "bad@test.com", "plan_id": str(plan.id), "owner_password": "OwnerPass1A", "confirm_password": "OwnerPass1A", }, follow_redirects=True) assert b"slug" in resp.data.lower() def test_tenant_detail_accessible(self, admin_app, admin_client): _create_admin(admin_app) tid = _create_tenant(admin_app, slug="detail-salon") _login_admin(admin_client) resp = admin_client.get(f"/tenants/{tid}") assert resp.status_code == 200 def test_set_status_suspended(self, admin_app, admin_client): _create_admin(admin_app) tid = _create_tenant(admin_app, slug="suspend-salon") _login_admin(admin_client) resp = admin_client.post(f"/tenants/{tid}/set-status", data={"status": "suspended"}, follow_redirects=True) assert resp.status_code == 200 with admin_app.app_context(): t = Tenant.query.get(tid) assert t.status == "suspended" # ── Settings Override ───────────────────────────────────────────────────────── class TestSettingsOverride: def test_set_override(self, admin_app, admin_client): _create_admin(admin_app) tid = _create_tenant(admin_app, slug="override-salon") _login_admin(admin_client) resp = admin_client.post( f"/settings-override/tenant/{tid}/set", data={"setting_key": "feature_marketing", "setting_value": "true", "note": "Test override"}, follow_redirects=True, ) assert resp.status_code == 200 with admin_app.app_context(): ov = TenantSettingOverride.query.filter_by( tenant_id=tid, setting_key="feature_marketing" ).filter(TenantSettingOverride.lifted_at.is_(None)).first() assert ov is not None assert ov.setting_value == "true" def test_lift_override(self, admin_app, admin_client): _create_admin(admin_app) tid = _create_tenant(admin_app, slug="lift-salon") _login_admin(admin_client) # Set first admin_client.post( f"/settings-override/tenant/{tid}/set", data={"setting_key": "feature_inventory", "setting_value": "false"}, follow_redirects=True, ) with admin_app.app_context(): ov = TenantSettingOverride.query.filter_by( tenant_id=tid, setting_key="feature_inventory" ).first() ov_id = ov.id # Lift resp = admin_client.post(f"/settings-override/{ov_id}/lift", follow_redirects=True) assert resp.status_code == 200 with admin_app.app_context(): ov = TenantSettingOverride.query.get(ov_id) assert ov.lifted_at is not None def test_invalid_setting_key_rejected(self, admin_app, admin_client): _create_admin(admin_app) tid = _create_tenant(admin_app, slug="badkey-salon") _login_admin(admin_client) resp = admin_client.post( f"/settings-override/tenant/{tid}/set", data={"setting_key": "bad key!", "setting_value": "true"}, follow_redirects=True, ) assert b"Invalid setting key" in resp.data # ── Audit Log ───────────────────────────────────────────────────────────────── class TestAuditLog: def test_audit_log_accessible(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.get("/audit-log/") assert resp.status_code == 200 def test_actions_are_logged(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) # Create a plan — should produce an audit log entry admin_client.post("/plans/new", data={ "name": "AuditTestPlan", "price_monthly": "49.00", "is_active": "1", }, follow_redirects=True) with admin_app.app_context(): entry = AuditLog.query.filter( AuditLog.action == "plan.create" ).first() assert entry is not None def test_audit_log_csv_export(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.get("/audit-log/export.csv") assert resp.status_code == 200 assert resp.content_type == "text/csv; charset=utf-8" # ── Analytics ───────────────────────────────────────────────────────────────── class TestAnalytics: def test_analytics_dashboard_loads(self, admin_app, admin_client): _create_admin(admin_app) _login_admin(admin_client) resp = admin_client.get("/analytics/") assert resp.status_code == 200 assert b"Platform Analytics" in resp.data def test_analytics_shows_tenant_counts(self, admin_app, admin_client): _create_admin(admin_app) _create_tenant(admin_app, slug="analytics-salon") _login_admin(admin_client) resp = admin_client.get("/analytics/") assert resp.status_code == 200 assert b"Total Tenants" in resp.data