// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package adminui import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "echo-lot.app/server/internal/adminauth" ) // tokenFixture wires just enough of the Server for the mint endpoint: a break-glass admin and // a stand-in EnrollLink (the real one belongs to the control server, injected the same way). func tokenFixture(t *testing.T) *Server { t.Helper() s, _, _, _ := fixture(t) secret, err := s.Store.SessionSecret() if err != nil { t.Fatal(err) } s.Sessions = adminauth.NewSessions(secret, time.Hour) s.Throttle = adminauth.NewThrottle() cred, err := adminauth.NewCredential("admin", "a-long-test-password") if err != nil { t.Fatal(err) } if err := s.Store.SetLocalAdmin(cred); err != nil { t.Fatal(err) } s.EnrollLink = func(tok string) string { return "echolot://enroll?v=1&t=" + tok } return s } func TestEnrollTokensAPISpecShape(t *testing.T) { h := tokenFixture(t).Handler() // No credentials → 401 with a challenge, never a token. req := httptest.NewRequest("POST", "/admin/enroll-tokens?note=phone", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized || rec.Header().Get("WWW-Authenticate") == "" { t.Fatalf("unauthenticated: code=%d", rec.Code) } // Wrong password → still 401. req = httptest.NewRequest("POST", "/admin/enroll-tokens", nil) req.SetBasicAuth("admin", "wrong") rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("bad password: code=%d, want 401", rec.Code) } // Basic + Accept: application/json → the §2.1 shape. req = httptest.NewRequest("POST", "/admin/enroll-tokens?note=phone", nil) req.SetBasicAuth("admin", "a-long-test-password") req.Header.Set("Accept", "application/json") rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("mint: code=%d body=%s", rec.Code, rec.Body.String()) } var body struct { Token string `json:"token"` ExpiresS int `json:"expires_in_s"` EnrollURI string `json:"enroll_uri"` } if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Token == "" || body.ExpiresS != 86400 || !strings.HasPrefix(body.EnrollURI, "echolot://enroll?") { t.Fatalf("spec shape violated: %+v", body) } // Without Accept: the browser flow — redirect to the QR page, link in the query. req = httptest.NewRequest("POST", "/admin/enroll-tokens", nil) req.SetBasicAuth("admin", "a-long-test-password") rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther || !strings.HasPrefix(rec.Header().Get("Location"), "/devices?link=") { t.Fatalf("html flow: code=%d location=%q", rec.Code, rec.Header().Get("Location")) } }