Admin user editing, knight-rider demos, self-contained user seeds

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 02:23:53 +12:00
parent b02a182bf1
commit f7892dd31b
16 changed files with 864 additions and 75 deletions

View File

@@ -92,15 +92,27 @@ def test_new_user_workspace_has_default_main_py(tmp_path, monkeypatch):
assert reg.status_code == 200
assert reg.json()["username"] == "alice"
uid = reg.json()["id"]
on_disk = tmp_path / "users" / f"alice-{uid}" / "code" / "main.py"
code_root = tmp_path / "users" / f"alice-{uid}" / "code"
on_disk = code_root / "main.py"
assert on_disk.is_file()
assert on_disk.read_text(encoding="utf-8") == 'print("Hello, World!")\n'
canonical = ("pattern_rainbow_demo.py", "pattern_twinkle_demo.py", "pattern_chase_demo.py")
for fname in canonical:
cp = code_root / fname
assert cp.is_file(), f"missing bundled copy {fname} (workspace/code must ship with app)"
text = cp.read_text(encoding="utf-8")
assert len(text.strip()) > 20
assert "from led_patterns" not in text
assert "import led_patterns" not in text
assert client.post("/api/auth/login", json={"username": "alice", "password": "password99"}).status_code == 200
fetched = client.get("/api/file/code/main.py")
assert fetched.status_code == 200
assert fetched.json()["filename"] == "main.py"
assert 'Hello, World!' in fetched.json()["content"]
chase = client.get("/api/file/code/pattern_chase_demo.py")
assert chase.status_code == 200
assert "knight_rider_scanner_frame" in chase.json()["content"]
def test_second_user_not_superuser(tmp_path, monkeypatch):
@@ -179,7 +191,54 @@ def test_invite_required_blocks_public_register(tmp_path, monkeypatch):
assert blocked.status_code == 403
def test_superuser_lists_and_creates_users(tmp_path, monkeypatch):
def test_superuser_can_patch_user_account(tmp_path, monkeypatch):
with TestClient(
_reload_app(tmp_path, monkeypatch, AUTH_ENABLED="true", AUTH_REGISTER_OPEN="true")
) as client:
client.post("/api/auth/register", json={"username": "admin", "password": "password99"})
client.post("/api/auth/login", json={"username": "admin", "password": "password99"})
invite = client.post("/api/users/invites", json={"email": None, "expires_days": 7})
token = invite.json()["invite_url"].split("invite=", 1)[1]
client.post("/api/auth/logout")
sub = client.post(
"/api/auth/register",
json={"username": "subacc", "password": "original99", "invite_token": token},
).json()
uid = sub["id"]
client.post("/api/auth/login", json={"username": "admin", "password": "password99"})
pat = client.patch(
f"/api/users/{uid}",
json={"username": "renamed", "is_superuser": True, "password": "renewedpw8"},
)
assert pat.status_code == 200
assert pat.json()["username"] == "renamed"
assert pat.json()["is_superuser"] is True
client.post("/api/auth/logout")
assert client.post(
"/api/auth/login",
json={"username": "renamed", "password": "renewedpw8"},
).status_code == 200
def test_superuser_cannot_demote_last_admin(tmp_path, monkeypatch):
with TestClient(
_reload_app(tmp_path, monkeypatch, AUTH_ENABLED="true", AUTH_REGISTER_OPEN="true")
) as client:
client.post("/api/auth/register", json={"username": "solo_admin", "password": "password99"})
client.post("/api/auth/login", json={"username": "solo_admin", "password": "password99"})
uid = client.get("/api/auth/me").json()["user"]["id"]
demote = client.patch(
f"/api/users/{uid}",
json={"username": "solo_admin", "is_superuser": False},
)
assert demote.status_code == 400
detail = demote.json().get("detail") or ""
assert "last" in detail.lower() or "administrator" in detail.lower()
def test_superuser_lists_users_after_invite_signup(tmp_path, monkeypatch):
with TestClient(
_reload_app(tmp_path, monkeypatch, AUTH_ENABLED="true", AUTH_REGISTER_OPEN="true")
) as client:
@@ -190,14 +249,20 @@ def test_superuser_lists_and_creates_users(tmp_path, monkeypatch):
assert listed.status_code == 200
assert len(listed.json()) == 1
created = client.post(
"/api/users",
json={"username": "sub", "password": "password99", "is_superuser": False},
)
assert created.status_code == 200
assert created.json()["username"] == "sub"
assert created.json()["is_superuser"] is False
invite = client.post("/api/users/invites", json={"email": None, "expires_days": 7})
assert invite.status_code == 200
token = invite.json()["invite_url"].split("invite=", 1)[1]
client.post("/api/auth/logout")
reg = client.post(
"/api/auth/register",
json={"username": "sub", "password": "password99", "invite_token": token},
)
assert reg.status_code == 200
assert reg.json()["username"] == "sub"
assert reg.json()["is_superuser"] is False
client.post("/api/auth/login", json={"username": "admin", "password": "password99"})
names = {u["username"] for u in client.get("/api/users").json()}
assert names == {"admin", "sub"}

View File

@@ -30,6 +30,33 @@ def test_chase_frame_has_head_and_tail():
assert sum(1 for c in frame if c != (0, 0, 0)) == 2
def test_bounce_head_pingpongs_off_ends():
patterns = _load_patterns_module()
bounce = patterns._bounce_head_index
assert bounce(5, 0) == 0 and bounce(5, 4) == 4
assert bounce(5, 5) == 3 and bounce(5, 8) == 1 and bounce(5, 16) == bounce(5, 0)
def test_scanner_bounce_has_head_and_fading_tail():
patterns = _load_patterns_module()
frame = patterns.scanner_bounce_frame(
8, 20, head_color=(80, 10, 10), tail_color=(20, 50, 90), tail_len=4
)
assert len(frame) == 8
bright = max(range(8), key=lambda i: sum(frame[i]))
assert sum(frame[bright]) >= 80
def test_knight_rider_tail_falls_off():
patterns = _load_patterns_module()
fr = patterns.knight_rider_scanner_frame(16, 55, tail_len=8, falloff_gamma=3.0)
assert len(fr) == 16
reds_sorted = sorted(fr[i][0] for i in range(16) if sum(fr[i]) > 0)
assert len(reds_sorted) >= 2
assert reds_sorted[-1] >= 200
assert reds_sorted[0] < reds_sorted[-1] * 0.5
def test_twinkle_frame_is_deterministic_for_same_inputs():
patterns = _load_patterns_module()
a = patterns.twinkle_frame(20, frame=9, seed=777, sparkles=4)