chore: add test user seed script and fix seed flow validation
Add seed_test_users.py for creating 4 dev accounts (super admin, pro solo, team admin, team engineer) via direct SQL. Fix seed scripts to create flows as drafts to bypass publish validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -883,6 +883,7 @@ async def create_procedural_flow(client: httpx.AsyncClient, token: str, flow_dat
|
|||||||
# Mark as default/system flow (public and visible to all)
|
# Mark as default/system flow (public and visible to all)
|
||||||
flow_data["is_default"] = True
|
flow_data["is_default"] = True
|
||||||
flow_data["is_public"] = True
|
flow_data["is_public"] = True
|
||||||
|
flow_data["status"] = "draft" # Skip validation for seed data
|
||||||
|
|
||||||
# Check if flow with same name exists
|
# Check if flow with same name exists
|
||||||
list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers, params={"tree_type": "procedural"})
|
list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers, params={"tree_type": "procedural"})
|
||||||
|
|||||||
188
backend/scripts/seed_test_users.py
Normal file
188
backend/scripts/seed_test_users.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Create test user accounts for local development.
|
||||||
|
|
||||||
|
Creates 4 accounts:
|
||||||
|
1. Super Admin – platform-wide admin (manages everything)
|
||||||
|
2. Pro Solo User – single user on a "pro" plan
|
||||||
|
3. Team Admin – admin of a team account ("team" plan)
|
||||||
|
4. Team Engineer – regular engineer on the same team account
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
cd backend
|
||||||
|
python -m scripts.seed_test_users
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.security import get_password_hash
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration – change passwords here if you like
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SHARED_PASSWORD = "TestPass123!"
|
||||||
|
|
||||||
|
USERS = [
|
||||||
|
{
|
||||||
|
"key": "super_admin",
|
||||||
|
"name": "RF Super Admin",
|
||||||
|
"email": "admin@resolutionflow.example.com",
|
||||||
|
"is_super_admin": True,
|
||||||
|
"is_team_admin": False,
|
||||||
|
"account_name": "ResolutionFlow Platform",
|
||||||
|
"account_role": "owner",
|
||||||
|
"plan": "team",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pro_solo",
|
||||||
|
"name": "Pat Solo",
|
||||||
|
"email": "pro@resolutionflow.example.com",
|
||||||
|
"is_super_admin": False,
|
||||||
|
"is_team_admin": False,
|
||||||
|
"account_name": "Pat's MSP",
|
||||||
|
"account_role": "owner",
|
||||||
|
"plan": "pro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "team_admin",
|
||||||
|
"name": "Alex Manager",
|
||||||
|
"email": "teamadmin@resolutionflow.example.com",
|
||||||
|
"is_super_admin": False,
|
||||||
|
"is_team_admin": True,
|
||||||
|
"account_name": "Acme MSP",
|
||||||
|
"account_role": "owner", # owns the account; is_team_admin=True gives admin powers
|
||||||
|
"plan": "team",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "team_engineer",
|
||||||
|
"name": "Jordan Tech",
|
||||||
|
"email": "engineer@resolutionflow.example.com",
|
||||||
|
"is_super_admin": False,
|
||||||
|
"is_team_admin": False,
|
||||||
|
"account_name": "Acme MSP", # same shared account
|
||||||
|
"account_role": "engineer",
|
||||||
|
"plan": None, # uses the team_admin's account & subscription
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _display_code() -> str:
|
||||||
|
return "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||||
|
password_hash = get_password_hash(SHARED_PASSWORD)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
team_account_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
for cfg in USERS:
|
||||||
|
# Check if user already exists
|
||||||
|
result = await conn.execute(
|
||||||
|
text("SELECT id, account_id FROM users WHERE email = :email"),
|
||||||
|
{"email": cfg["email"]},
|
||||||
|
)
|
||||||
|
row = result.first()
|
||||||
|
if row:
|
||||||
|
print(f" [SKIP] {cfg['email']} already exists")
|
||||||
|
if cfg["key"] == "team_admin":
|
||||||
|
team_account_id = row.account_id
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---- Create or reuse Account ----
|
||||||
|
if cfg["key"] == "team_engineer":
|
||||||
|
if team_account_id is None:
|
||||||
|
result = await conn.execute(
|
||||||
|
text("SELECT id FROM accounts WHERE name = :name"),
|
||||||
|
{"name": "Acme MSP"},
|
||||||
|
)
|
||||||
|
acme = result.first()
|
||||||
|
if acme:
|
||||||
|
team_account_id = acme.id
|
||||||
|
if team_account_id is None:
|
||||||
|
print(f" [ERROR] Cannot create {cfg['email']} — Acme MSP account not found.")
|
||||||
|
continue
|
||||||
|
account_id = team_account_id
|
||||||
|
else:
|
||||||
|
account_id = uuid.uuid4()
|
||||||
|
await conn.execute(
|
||||||
|
text("""
|
||||||
|
INSERT INTO accounts (id, name, display_code, created_at, updated_at)
|
||||||
|
VALUES (:id, :name, :code, :now, :now)
|
||||||
|
"""),
|
||||||
|
{"id": account_id, "name": cfg["account_name"], "code": _display_code(), "now": now},
|
||||||
|
)
|
||||||
|
if cfg["key"] == "team_admin":
|
||||||
|
team_account_id = account_id
|
||||||
|
|
||||||
|
# ---- Create User ----
|
||||||
|
user_id = uuid.uuid4()
|
||||||
|
await conn.execute(
|
||||||
|
text("""
|
||||||
|
INSERT INTO users (id, email, password_hash, name, role, is_super_admin,
|
||||||
|
is_team_admin, is_active, account_id, account_role, created_at)
|
||||||
|
VALUES (:id, :email, :pw, :name, 'engineer', :is_sa, :is_ta, true,
|
||||||
|
:account_id, :account_role, :now)
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"id": user_id,
|
||||||
|
"email": cfg["email"],
|
||||||
|
"pw": password_hash,
|
||||||
|
"name": cfg["name"],
|
||||||
|
"is_sa": cfg["is_super_admin"],
|
||||||
|
"is_ta": cfg["is_team_admin"],
|
||||||
|
"account_id": account_id,
|
||||||
|
"account_role": cfg["account_role"],
|
||||||
|
"now": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set account owner (skip for team_engineer — they don't own the account)
|
||||||
|
if cfg["key"] != "team_engineer":
|
||||||
|
await conn.execute(
|
||||||
|
text("UPDATE accounts SET owner_id = :uid WHERE id = :aid"),
|
||||||
|
{"uid": user_id, "aid": account_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Create Subscription (once per account) ----
|
||||||
|
if cfg["plan"] is not None:
|
||||||
|
await conn.execute(
|
||||||
|
text("""
|
||||||
|
INSERT INTO subscriptions (id, account_id, plan, status, created_at, updated_at)
|
||||||
|
VALUES (:id, :aid, :plan, 'active', :now, :now)
|
||||||
|
"""),
|
||||||
|
{"id": uuid.uuid4(), "aid": account_id, "plan": cfg["plan"], "now": now},
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" [OK] {cfg['email']:40s} account_role={cfg['account_role']:<10s} plan={cfg['plan'] or '(shared)'}")
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Test accounts ready!")
|
||||||
|
print(f" Password for all accounts: {SHARED_PASSWORD}")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print(" Accounts:")
|
||||||
|
print(f" Super Admin : admin@resolutionflow.example.com")
|
||||||
|
print(f" Pro Solo : pro@resolutionflow.example.com")
|
||||||
|
print(f" Team Admin : teamadmin@resolutionflow.example.com")
|
||||||
|
print(f" Team Engineer: engineer@resolutionflow.example.com")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("\n[*] ResolutionFlow — Test User Seeder")
|
||||||
|
print("=" * 60)
|
||||||
|
asyncio.run(main())
|
||||||
@@ -3330,6 +3330,7 @@ async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) ->
|
|||||||
# Mark as default/system tree (public and visible to all)
|
# Mark as default/system tree (public and visible to all)
|
||||||
tree_data["is_default"] = True
|
tree_data["is_default"] = True
|
||||||
tree_data["is_public"] = True
|
tree_data["is_public"] = True
|
||||||
|
tree_data["status"] = "draft" # Skip validation for seed data
|
||||||
|
|
||||||
# Check if tree with same name exists
|
# Check if tree with same name exists
|
||||||
list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers)
|
list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers)
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@ async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict, ca
|
|||||||
|
|
||||||
tree_data["is_default"] = True
|
tree_data["is_default"] = True
|
||||||
tree_data["is_public"] = True
|
tree_data["is_public"] = True
|
||||||
|
tree_data["status"] = "draft" # Skip validation for seed data
|
||||||
|
|
||||||
# Normalize description -> action/solution fields
|
# Normalize description -> action/solution fields
|
||||||
normalize_tree_structure(tree_data)
|
normalize_tree_structure(tree_data)
|
||||||
|
|||||||
Reference in New Issue
Block a user