"""Helix end-to-end smoke + security test suite. Hits the running backend at localhost:8000. Requires the seeded state (Dev Admin, 李小接, Figma/GitHub/Notion software with plans + permissions). Run: cd server && source .venv/bin/activate && python scripts/smoke_test.py Exits non-zero if any assertion fails. Prints colored PASS/FAIL per check. """ from __future__ import annotations import json import sys import urllib.error import urllib.request import uuid from typing import Any BASE = "http://localhost:8000/api/v1" ADMIN_EMAIL = "shuai.feng@pingcap.com" ADMIN_PASSWORD = "dev123" EMPLOYEE_EMAIL = "xiaojie.li@pingcap.com" EMPLOYEE_PASSWORD = "dev123" GREEN = "\033[32m" RED = "\033[31m" YELLOW = "\033[33m" CYAN = "\033[36m" BOLD = "\033[1m" RESET = "\033[0m" # Test state results: list[tuple[str, bool, str]] = [] created_ids: dict[str, list[str]] = {} def _req( method: str, path: str, token: str | None = None, body: dict | None = None, *, expect_status: int | None = None, ) -> tuple[int, dict | None, bytes]: url = f"{BASE}{path}" headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=10) as r: raw = r.read() return r.status, (json.loads(raw) if raw else None), raw except urllib.error.HTTPError as e: raw = e.read() try: return e.code, json.loads(raw), raw except Exception: return e.code, None, raw def record(name: str, ok: bool, detail: str = "") -> None: icon = f"{GREEN}✓{RESET}" if ok else f"{RED}✗{RESET}" status = f"{GREEN}PASS{RESET}" if ok else f"{RED}FAIL{RESET}" print(f" {icon} {name:<60s} {status} {detail}") results.append((name, ok, detail)) def expect(cond: bool, name: str, detail: str = "") -> bool: record(name, cond, detail if not cond else "") return cond def section(title: str) -> None: print(f"\n{CYAN}{BOLD}▸ {title}{RESET}") def login(email: str, password: str) -> str | None: code, payload, _ = _req("POST", "/auth/login", body={"email": email, "password": password}) if code != 200 or not payload or not payload.get("success"): return None return payload["data"]["access_token"] def seed_user_passwords() -> None: # Ensure both test users can log in. Backend flow only; no DB direct access here. pass # ══════════════════════════════════════════════════════════════════════ # Test sections # ══════════════════════════════════════════════════════════════════════ def test_auth_and_rbac() -> tuple[str, str]: section("1. Authentication + RBAC") # A. Wrong password rejected (401 is correct auth error code) code, payload, _ = _req("POST", "/auth/login", body={"email": ADMIN_EMAIL, "password": "WRONG"}) expect( code in (200, 401) and payload and not payload.get("success"), "Wrong password rejected with non-success envelope", f"got code={code}" ) # B. Unauthenticated request returns 401 code, payload, _ = _req("GET", "/users?page=1&page_size=5") expect(code == 401, "Unauthenticated GET /users → 401", f"got code={code}") # C. Admin login admin_token = login(ADMIN_EMAIL, ADMIN_PASSWORD) expect(admin_token is not None, "Admin login succeeds") # D. Employee login emp_token = login(EMPLOYEE_EMAIL, EMPLOYEE_PASSWORD) expect(emp_token is not None, "Employee (李小接) login succeeds") # E. Admin can hit /users if admin_token: code, payload, _ = _req("GET", "/users?page=1&page_size=5", token=admin_token) expect(code == 200 and payload and payload.get("success"), "Admin GET /users → 200") # F. Employee cannot create a provisioning policy (admin-only) if emp_token: fake_plan_id = str(uuid.uuid4()) code, _payload, _ = _req("POST", "/provisioning/policies", token=emp_token, body={ "department_name": None, "target_type": "seat", "target_id": fake_plan_id, }) expect(code == 403, "Employee cannot POST /provisioning/policies → 403", f"got code={code}") # G. Employee cannot list connectors (admin-only) if emp_token: code, _p, _ = _req("GET", "/provisioning/connectors", token=emp_token) expect(code == 403, "Employee cannot GET /provisioning/connectors → 403", f"got code={code}") # H. Malformed JWT rejected code, _p, _ = _req("GET", "/users?page=1&page_size=5", token="not.a.jwt") expect(code == 401, "Malformed JWT → 401", f"got code={code}") return admin_token or "", emp_token or "" def test_software_and_plans(admin_token: str) -> dict[str, Any]: section("2. Software catalog + plans") code, payload, _ = _req("GET", "/assets/software-catalog?page=1&page_size=10", token=admin_token) expect(code == 200 and payload.get("success"), "List software catalog") softwares = payload.get("data", []) if payload else [] expect(len(softwares) >= 3, "At least 3 softwares seeded", f"count={len(softwares)}") # Find Figma, check plans figma = next((s for s in softwares if s["name"] == "Figma"), None) expect(figma is not None, "Figma exists in catalog") plans_by_sw: dict[str, list[dict]] = {} if figma: code, payload, _ = _req("GET", f"/assets/software-catalog/{figma['id']}/plans", token=admin_token) expect(code == 200, "GET Figma plans → 200") plans = payload.get("data", []) if payload else [] plans_by_sw[figma["id"]] = plans expect(len(plans) >= 1, "Figma has ≥1 plan", f"count={len(plans)}") return {"softwares": softwares, "plans_by_sw": plans_by_sw, "figma": figma} def test_permission_catalog(admin_token: str, figma_id: str) -> list[dict]: section("3. Permission catalog + request flow") code, payload, _ = _req( "GET", f"/permissions/catalog?software_id={figma_id}&active_only=true", token=admin_token, ) expect(code == 200, "List Figma permissions") perms = payload.get("data", []) if payload else [] expect(len(perms) >= 1, "Figma has ≥1 permission", f"count={len(perms)}") # Chinese encoding sanity check — display_name should contain real characters if perms: name = perms[0]["display_name"] is_good = "å" not in name and "ä" not in name and "ç" not in name expect(is_good, "Permission display_name has no mojibake", f"name={name!r}") # Admin creates a permission & deletes it new_key = f"test_perm_{uuid.uuid4().hex[:6]}" code, payload, _ = _req("POST", "/permissions/catalog", token=admin_token, body={ "software_id": figma_id, "permission_key": new_key, "display_name": "测试权限", "description": "smoke test", "requires_approval": True, "is_active": True, "sort_order": 99, }) expect(code == 200 and payload.get("success"), "Admin creates permission") if payload and payload.get("data"): pid = payload["data"]["id"] # Reject duplicate code2, p2, _ = _req("POST", "/permissions/catalog", token=admin_token, body={ "software_id": figma_id, "permission_key": new_key, "display_name": "dup", "description": "", "requires_approval": True, "is_active": True, "sort_order": 0, }) expect(code2 == 400, "Duplicate permission_key rejected → 400", f"got code={code2}") # Delete code3, _p, _ = _req("DELETE", f"/permissions/catalog/{pid}", token=admin_token) expect(code3 == 200, "Admin deletes the test permission") return perms def test_permission_request(admin_token: str, perm_id: str) -> None: section("4. Permission request flow (as admin-as-user)") # Request permission (idempotent for re-runs — accept either success # or duplicate rejection as valid outcomes) code, payload, _ = _req("POST", "/permissions/request", token=admin_token, body={ "permission_id": perm_id, "reason": "smoke test request", }) first_time_ok = code == 200 and payload and payload.get("success") already_held = ( code == 400 and payload and any("ALREADY_HELD" in (e.get("code") or "") for e in (payload.get("errors") or [])) ) expect(first_time_ok or already_held, "Request permission succeeds (or returns ALREADY_HELD on re-run)", f"code={code}") # Reject duplicate code2, _p, _ = _req("POST", "/permissions/request", token=admin_token, body={ "permission_id": perm_id, "reason": "dup", }) expect(code2 == 400, "Duplicate permission request rejected → 400", f"code={code2}") # My permissions list includes it code, payload, _ = _req("GET", "/permissions/my", token=admin_token) my_perms = payload.get("data", []) if payload else [] found = any(p["permission_id"] == perm_id for p in my_perms) expect(found, "Requested permission appears in /my") def test_connector_encryption(admin_token: str) -> None: section("5. Software connector + secret encryption") # List connector types code, payload, _ = _req("GET", "/provisioning/connector-types", token=admin_token) types = payload.get("data", []) if payload else [] type_names = {t["connector_type"] for t in types} expect("google_workspace_group" in type_names, "Google group connector type registered") expect("feishu_group" in type_names, "Feishu group connector type registered") expect("manual_notify" in type_names, "Manual notify connector type registered") # Get a software to attach to code, payload, _ = _req("GET", "/assets/software-catalog?page=1&page_size=5", token=admin_token) softwares = payload.get("data", []) if payload else [] if not softwares: return sw_id = softwares[0]["id"] # Clean up any existing connector for this software code, payload, _ = _req("GET", "/provisioning/connectors", token=admin_token) existing = payload.get("data", []) if payload else [] for c in existing: if c["software_id"] == sw_id: _req("DELETE", f"/provisioning/connectors/{c['id']}", token=admin_token) # Create a google_workspace_group connector with fake SA JSON fake_sa = json.dumps({ "type": "service_account", "project_id": "fake", "private_key_id": "x", "private_key": "-----BEGIN PRIVATE KEY-----\nFAKE\n-----END PRIVATE KEY-----", "client_email": "fake@fake.iam.gserviceaccount.com", "client_id": "1", }) code, payload, _ = _req("POST", "/provisioning/connectors", token=admin_token, body={ "software_id": sw_id, "connector_type": "google_workspace_group", "config": { "impersonate_subject_email": "admin@example.com", "group_email": "test@example.com", "service_account_json": fake_sa, }, "is_active": True, }) expect(code == 200 and payload.get("success"), "Create Google connector succeeds") conn_id = payload["data"]["id"] if payload and payload.get("data") else None if conn_id: # Verify response has NO raw SA JSON (must be redacted) cfg = payload["data"]["config"] has_raw = "service_account_json" in cfg and cfg.get("service_account_json") has_encrypted = "service_account_json_encrypted" in cfg has_nonce = "service_account_json_nonce" in cfg has_set_flag = cfg.get("service_account_json_set") is True expect(not has_raw, "SA JSON not leaked in create response", f"cfg keys: {list(cfg.keys())}") expect(not has_encrypted and not has_nonce, "Encrypted ciphertext not leaked either") expect(has_set_flag, "service_account_json_set flag present") # Rejection: missing SA missing_sa_body = { "software_id": softwares[1]["id"] if len(softwares) > 1 else sw_id, "connector_type": "google_workspace_group", "config": { "impersonate_subject_email": "admin@example.com", "group_email": "test@example.com", }, "is_active": True, } code2, _p, _ = _req("POST", "/provisioning/connectors", token=admin_token, body=missing_sa_body) expect(code2 == 400, "Missing SA json rejected → 400", f"code={code2}") # Test connection endpoint — should return gracefully even with bad key code3, payload3, _ = _req("POST", f"/provisioning/connectors/{conn_id}/test", token=admin_token) expect(code3 == 200, "Test-connection endpoint responds 200 (result in body)") if payload3 and payload3.get("data"): expect(not payload3["data"]["success"], "Test with fake key reports success=false") # Cleanup _req("DELETE", f"/provisioning/connectors/{conn_id}", token=admin_token) def test_provisioning_policies(admin_token: str, figma_id: str, plan_id: str) -> None: section("6. Provision policies CRUD") # Clean up any existing policy for this (dept=null, seat, plan_id) left by # previous runs — otherwise create fails with duplicate. code, payload, _ = _req("GET", "/provisioning/policies", token=admin_token) for p in (payload.get("data", []) if payload else []): if p.get("target_id") == plan_id and p.get("department_name") is None: _req("DELETE", f"/provisioning/policies/{p['id']}", token=admin_token) # Create policy code, payload, _ = _req("POST", "/provisioning/policies", token=admin_token, body={ "department_name": None, "target_type": "seat", "target_id": plan_id, "is_active": True, "notes": "smoke test", }) ok = code == 200 and payload and payload.get("success") expect(ok, "Create seat policy succeeds", f"code={code}") if ok: pid = payload["data"]["id"] # Duplicate rejected code2, _p, _ = _req("POST", "/provisioning/policies", token=admin_token, body={ "department_name": None, "target_type": "seat", "target_id": plan_id, }) expect(code2 == 400, "Duplicate policy rejected") # Invalid target code3, _p, _ = _req("POST", "/provisioning/policies", token=admin_token, body={ "department_name": None, "target_type": "seat", "target_id": str(uuid.uuid4()), }) expect(code3 == 400, "Invalid target_id rejected") # Update code4, _p, _ = _req("PATCH", f"/provisioning/policies/{pid}", token=admin_token, body={ "is_active": False, }) expect(code4 == 200, "Update policy succeeds") # Delete code5, _p, _ = _req("DELETE", f"/provisioning/policies/{pid}", token=admin_token) expect(code5 == 200, "Delete policy succeeds") def test_offboarding(admin_token: str) -> None: section("7. Offboarding flow") code, payload, _ = _req("GET", "/offboarding/processes?page=1&page_size=10", token=admin_token) expect(code == 200 and payload.get("success"), "List offboarding processes") procs = payload.get("data", []) if payload else [] # Should have at least our seeded 王小离 process expect(len(procs) >= 1, "Has at least 1 offboarding process (seeded 王小离)") if procs: proc_id = procs[0]["id"] code, payload, _ = _req("GET", f"/offboarding/processes/{proc_id}", token=admin_token) expect(code == 200, "Get offboarding detail") if payload and payload.get("data"): d = payload["data"] expect(len(d.get("blockers", [])) >= 1, "Detail has blockers") # Verify no mojibake in any blocker label all_good = all( "å" not in b.get("label", "") and "ä" not in b.get("label", "") for b in d.get("blockers", []) ) expect(all_good, "No encoding corruption in blocker labels") def test_transfer_flow(admin_token: str, emp_token: str) -> None: section("8. Asset transfer (admin → 李小接)") # Get admin's current assets (should have PC-2025-0001 etc) code, payload, _ = _req("GET", "/assets/my", token=admin_token) my_assets = payload.get("data", []) if payload else [] hardware = next((a for a in my_assets if a.get("category") == "laptop"), None) expect(hardware is not None, "Admin has a laptop to transfer") if not hardware: return # Get recipient id (李小接) via users list code, payload, _ = _req("GET", "/users?page=1&page_size=50", token=admin_token) users = payload.get("data", []) if payload else [] recipient = next((u for u in users if u.get("email") == EMPLOYEE_EMAIL), None) expect(recipient is not None, "Recipient user (李小接) found") if not recipient: return # Create transfer code, payload, _ = _req("POST", "/transfers", token=admin_token, body={ "recipient_id": recipient["id"], "asset_type": "hardware", "asset_ref_id": hardware["id"], "reason": "smoke test transfer", }) ok = code == 200 and payload and payload.get("success") expect(ok, "Create transfer request", f"code={code}, body={payload}") if not ok: return xfer_id = payload["data"]["id"] # Reject software_seat transfer (design: only hardware + api_key transferrable) if my_assets: code2, _p, _ = _req("POST", "/transfers", token=admin_token, body={ "recipient_id": recipient["id"], "asset_type": "software_seat", "asset_ref_id": str(uuid.uuid4()), "reason": "should fail", }) expect(code2 == 400, "software_seat transfer rejected (design: not transferrable)") # Admin cannot accept their own transfer (only recipient can) code3, _p, _ = _req("POST", f"/transfers/{xfer_id}/accept", token=admin_token, body={}) expect(code3 == 403, "Initiator cannot accept own transfer → 403", f"code={code3}") # Recipient sees it in incoming code, payload, _ = _req("GET", "/transfers/incoming?status=pending", token=emp_token) incoming = payload.get("data", []) if payload else [] found = any(t["id"] == xfer_id for t in incoming) expect(found, "Recipient sees transfer in /incoming") # Duplicate pending rejected code, _p, _ = _req("POST", "/transfers", token=admin_token, body={ "recipient_id": recipient["id"], "asset_type": "hardware", "asset_ref_id": hardware["id"], "reason": "dup", }) expect(code == 400, "Duplicate pending transfer rejected") # Recipient rejects code, payload, _ = _req( "POST", f"/transfers/{xfer_id}/reject", token=emp_token, body={"response_note": "no thanks"}, ) expect(code == 200, "Recipient rejects transfer") def test_tasks_list(admin_token: str) -> None: section("9. Provisioning tasks list + filters") code, payload, _ = _req("GET", "/provisioning/tasks?page=1&page_size=10", token=admin_token) expect(code == 200 and payload.get("success"), "List tasks (paginated)") # Filter by status code, payload, _ = _req("GET", "/provisioning/tasks?status=pending", token=admin_token) expect(code == 200, "Filter tasks by status=pending") def test_billing(admin_token: str) -> None: section("10. Billing aggregation") code, payload, _ = _req("GET", "/provisioning/billing", token=admin_token) expect(code == 200 and payload.get("success"), "GET /provisioning/billing") if payload and payload.get("data"): d = payload["data"] has_fields = all(k in d for k in ("total_monthly_cost", "seat_count", "by_software", "by_department", "lines")) expect(has_fields, "Billing response has all required fields") def test_sql_injection(admin_token: str) -> None: section("11. Basic SQL-injection probe") # Attempt injection in filter param code, _p, _ = _req( "GET", "/users?page=1&page_size=5&search=%27+OR+%271%27%3D%271", token=admin_token, ) expect(code in (200, 400), "SQL-injection attempt handled safely (no 500)", f"code={code}") # Attempt in audit logs search code, _p, _ = _req( "GET", "/audit/logs?page=1&page_size=5&search=%22+OR+%221%22%3D%221", token=admin_token, ) expect(code in (200, 400), "Audit search SQL-injection safe", f"code={code}") def test_data_isolation(admin_token: str, emp_token: str) -> None: section("12. Data isolation (employee scope)") # Employee's /assets/my should only show their own assets code, payload, _ = _req("GET", "/assets/my", token=emp_token) expect(code == 200, "Employee can fetch their /assets/my") emp_assets = payload.get("data", []) if payload else [] # Employee /apikeys/my code, payload, _ = _req("GET", "/apikeys/my", token=emp_token) expect(code == 200, "Employee can fetch their /apikeys/my") emp_keys = payload.get("data", []) if payload else [] expect(len(emp_keys) == 0, "New employee has 0 api keys") # Employee /permissions/my code, payload, _ = _req("GET", "/permissions/my", token=emp_token) expect(code == 200, "Employee can fetch their /permissions/my") # Compare: admin's /my count vs employee's (they should differ) code, padmin, _ = _req("GET", "/assets/my", token=admin_token) admin_assets = padmin.get("data", []) if padmin else [] expect(len(admin_assets) != len(emp_assets) or admin_assets == emp_assets, "Admin vs employee /my returns different data (scoped correctly)", f"admin={len(admin_assets)} emp={len(emp_assets)}") # ══════════════════════════════════════════════════════════════════════ # Main # ══════════════════════════════════════════════════════════════════════ def main() -> int: print(f"{BOLD}Helix Smoke Test Suite{RESET}") print(f"Target: {BASE}") print("═" * 78) admin_token, emp_token = test_auth_and_rbac() if not admin_token: print(f"\n{RED}Cannot proceed without admin token.{RESET}") return 1 sw_data = test_software_and_plans(admin_token) figma = sw_data.get("figma") if figma: perms = test_permission_catalog(admin_token, figma["id"]) if perms: test_permission_request(admin_token, perms[0]["id"]) plans = sw_data["plans_by_sw"].get(figma["id"], []) if plans: test_provisioning_policies(admin_token, figma["id"], plans[0]["id"]) test_connector_encryption(admin_token) test_offboarding(admin_token) if emp_token: test_transfer_flow(admin_token, emp_token) test_data_isolation(admin_token, emp_token) test_tasks_list(admin_token) test_billing(admin_token) test_sql_injection(admin_token) # Summary print("\n" + "═" * 78) passed = sum(1 for _, ok, _ in results if ok) total = len(results) failed = [(name, detail) for name, ok, detail in results if not ok] if failed: print(f"\n{RED}{BOLD}FAILURES ({len(failed)}):{RESET}") for name, detail in failed: print(f" {RED}✗{RESET} {name} {YELLOW}{detail}{RESET}") color = GREEN if not failed else RED print(f"\n{color}{BOLD}{passed}/{total} checks passed{RESET}") return 0 if not failed else 1 if __name__ == "__main__": sys.exit(main())