05/06 Phase 2: updated and added new files
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user