// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package adminui import ( "testing" "time" "echo-lot.app/server/internal/adminauth" "echo-lot.app/server/internal/runs" "echo-lot.app/server/internal/store" ) // Two accounts, one device each, plus an unlinked device nobody owns. func fixture(t *testing.T) (*Server, string, string, string) { t.Helper() dir := t.TempDir() st, err := store.Open(dir) if err != nil { t.Fatal(err) } rs, err := runs.Open(dir, runs.DefaultPolicy()) if err != nil { t.Fatal(err) } enroll := func(name string) string { tok, err := st.NewEnrollToken(time.Hour, "test") if err != nil { t.Fatal(err) } d, err := st.Redeem(tok, name) if err != nil { t.Fatal(err) } return d.ID } mine, theirs, orphan := enroll("mine"), enroll("theirs"), enroll("orphan") if err := st.LinkAccount(mine, "oidc#me", "Me"); err != nil { t.Fatal(err) } if err := st.LinkAccount(theirs, "oidc#you", "You"); err != nil { t.Fatal(err) } for _, d := range []string{mine, theirs, orphan} { if _, err := rs.Put(d, []byte(`{"run":{"id":"r"}}`), true); err != nil { t.Fatal(err) } } return &Server{Store: st, Runs: rs}, mine, theirs, orphan } func user() *adminauth.Session { return &adminauth.Session{Subject: "oidc#me", Display: "Me"} } func admin() *adminauth.Session { return &adminauth.Session{Subject: "local:a", Admin: true} } func TestVisibleDevicesScopesToAccount(t *testing.T) { s, mine, theirs, orphan := fixture(t) got := s.visibleDevices(user()) if len(got) != 1 || got[0].ID != mine { t.Fatalf("a user should see only their own device, got %+v", got) } all := s.visibleDevices(admin()) if len(all) != 3 { t.Fatalf("an admin should see every device, got %d", len(all)) } _ = theirs _ = orphan } func TestUnlinkedDevicesBelongToNobody(t *testing.T) { // An enrolled but never-signed-in device is not "everyone's" — a user must not inherit it // just because no account claimed it. s, _, _, orphan := fixture(t) if s.mayTouchRun(user(), orphan) { t.Fatal("an unlinked device was treated as the user's own") } } func TestRunAccessFollowsDeviceOwnership(t *testing.T) { s, mine, theirs, _ := fixture(t) if !s.mayTouchRun(user(), mine) { t.Fatal("a user cannot reach their own run") } if s.mayTouchRun(user(), theirs) { t.Fatal("a user reached someone else's run") } if !s.mayTouchRun(admin(), theirs) { t.Fatal("an admin should reach any run") } } func TestAccessEndsWhenTheLinkDoes(t *testing.T) { // Ownership is read from the device list on every request rather than captured at sign-in, // so unlinking takes effect immediately — a session issued while linked must not keep working. s, mine, _, _ := fixture(t) sess := user() if !s.mayTouchRun(sess, mine) { t.Fatal("precondition: the device should start out owned") } if err := s.Store.LinkAccount(mine, "", ""); err != nil { t.Fatal(err) } if s.mayTouchRun(sess, mine) { t.Fatal("access survived the account link being removed") } } func TestEmptySubjectMatchesNothing(t *testing.T) { // The dangerous degenerate case: a session with no subject must own nothing, not everything // that happens to have an empty account id. s, _, _, _ := fixture(t) anon := &adminauth.Session{Subject: "", Display: ""} if got := s.visibleDevices(anon); len(got) != 0 { t.Fatalf("an empty subject matched %d devices", len(got)) } }