Admin user editing, knight-rider demos, self-contained user seeds
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user