Files
hamlab/internal/api/guests_test.go
Mitchell-JohnsonandClaude Opus 5 179f342a2c A guest login is a full account with a clock on it
Mitchell, on the read-only version built earlier today: scratch it. He hands
these to people he trusts and revokes them otherwise, so a console with half
its tabs missing solved a problem he doesn't have.

So the allow-list, the middleware check and every "not for a guest" branch in
the UI are gone. What is left is the part that was the point: a throwaway
account that expires by itself, is checked on every request, swept once a
minute, and can be ended in one tap. A guest now gets the whole console on
every machine, terminal included.

Said once, since it is his call and he has made it: that means a guest has a
root-capable shell on every host, and can make an account or a token of its own
that outlives the guest login.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 17:53:55 -07:00

71 lines
2.1 KiB
Go

package api
import (
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"hamlab/internal/auth"
"hamlab/internal/config"
"hamlab/internal/store"
)
// A guest login is an ordinary account with a clock on it, so the clock is the thing worth
// testing: one whose time is up is stopped at the door, whether or not the minute-by-minute
// sweep has reached their account yet, and their session is dropped on the way — so the
// console they left open goes back to the login form rather than carrying on.
func TestExpiredGuestIsRefusedAndSignedOut(t *testing.T) {
for _, tc := range []struct {
name string
expires time.Time
wantOK bool
}{
{"still has time", time.Now().Add(10 * time.Minute), true},
{"time is up", time.Now().Add(-time.Second), false},
{"no expiry at all", time.Time{}, false},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
cfg := config.Default(filepath.Join(dir, "config.json"))
if err := cfg.Update(func(c *config.Config) {
c.Users = []config.User{{Name: "guest-abcd", Hash: "x", Guest: true, Expires: tc.expires}}
}); err != nil {
t.Fatal(err)
}
st, err := store.Open(filepath.Join(dir, "state.json"))
if err != nil {
t.Fatal(err)
}
s := &Server{cfg: cfg, store: st, sessions: auth.NewSessions(st)}
tok, err := s.sessions.Create("guest-abcd", "10.0.0.5", "curl")
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("GET", "/api/me", nil)
r.AddCookie(&http.Cookie{Name: cookieName, Value: tok})
w := httptest.NewRecorder()
a, ok, answered := s.cookieActor(w, r)
if ok != tc.wantOK {
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
}
if tc.wantOK {
if !a.Guest || a.User != "guest-abcd" {
t.Errorf("actor = %+v, want the guest account marked as one", a)
}
if answered {
t.Error("answered the caller for a guest that still has time")
}
return
}
if !answered || w.Code != 401 {
t.Errorf("answered = %v, code = %d, want true and 401", answered, w.Code)
}
if _, live := s.sessions.Lookup(tok, "10.0.0.5"); live {
t.Error("the session of an expired guest is still live")
}
})
}
}