Files
platform/auth/reload_test.go
T
mats 2f3ea99745 feat(auth): minted keys + device login for the admin API and MCP endpoints
platform/auth gains the LocalJWT (HS256 mint/verify, AUTH_TOKEN_SECRET,
disabled when unset), the RFC 8628 DeviceFlow (start/approve/poll with
requested-scope gating) and KeyStore.ReloadIfChanged (mtime-based, so a
freshly minted key works across processes without a restart). One shared
store at /commerce/apikeys.json (AUTH_APIKEYS_FILE) now backs the
backoffice admin gate, the CMS API and every MCP endpoint; AUTH_TOKEN_SECRET
lets the companion app's device flow mint bearer tokens the same surfaces
accept. Submodule bumps: backoffice (AuthGate + device endpoints + mcp
scope), go-cart-actor (mcpauth sources), go-graph-cms-v2 (shared store +
token chain).

Also lands the still-pending receipt-ocr sidecar (previous pass): the
PP-OCR ONNX inference service + download-ocr-models.sh + k8s
deployment/initContainer, wired into compose and the ledger RECEIPT_OCR_URL,
with the extractor pairing fix riding the go-cart-actor bump.

Deploy: AUTH_APIKEYS_FILE/AUTH_TOKEN_SECRET/AUTH_DEVICE_BASE_URL on
backoffice + cart/profile/order/ledger in compose and k8s, /device ingress
route, stack-secrets AUTH_TOKEN_SECRET optional ref, docs updated.

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-08-16 12:48:03 +02:00

91 lines
2.4 KiB
Go

package auth
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
)
func TestKeyStoreReloadIfChanged(t *testing.T) {
path := filepath.Join(t.TempDir(), "apikeys.json")
// Writer process mints into the store.
writer, err := OpenKeyStore(path)
if err != nil {
t.Fatal(err)
}
key, rec, err := writer.Mint("integration", []string{"mcp"})
if err != nil {
t.Fatal(err)
}
// Reader process opens the same file fresh.
reader, err := OpenKeyStore(path)
if err != nil {
t.Fatal(err)
}
req := apiKeyReq(key)
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(req); err != nil {
t.Fatalf("fresh key should validate: %v", err)
}
// Writer mints a second key AFTER the reader loaded → reader must reload.
time.Sleep(10 * time.Millisecond) // ensure mtime advances
key2, _, err := writer.Mint("companion", []string{"write"})
if err != nil {
t.Fatal(err)
}
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key2)); err == nil {
t.Fatal("key minted after load should NOT validate before reload")
}
if err := reader.ReloadIfChanged(); err != nil {
t.Fatal(err)
}
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key2)); err != nil {
t.Fatalf("key should validate after ReloadIfChanged: %v", err)
}
// Unchanged file → no-op (and keys survive).
if err := reader.ReloadIfChanged(); err != nil {
t.Fatal(err)
}
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key)); err != nil {
t.Fatalf("key should still validate: %v", err)
}
// Revoke on the writer side → reader sees it after reload.
if err := writer.Revoke(rec.ID); err != nil {
t.Fatal(err)
}
if err := reader.ReloadIfChanged(); err != nil {
t.Fatal(err)
}
if _, err := (APIKeyAuth{Keys: reader}).Authenticate(apiKeyReq(key)); err == nil {
t.Fatal("revoked key should not validate")
}
}
func apiKeyReq(key string) *http.Request {
r := httptest.NewRequest(http.MethodGet, "/api/nodes", nil)
r.Header.Set("X-API-Key", key)
return r
}
func TestKeyStoreReloadCorruptFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "apikeys.json")
ks, err := OpenKeyStore(path)
if err != nil {
t.Fatal(err)
}
raw := []byte(`{not json`)
if err := os.WriteFile(path, raw, 0o600); err != nil {
t.Fatal(err)
}
time.Sleep(10 * time.Millisecond)
if err := ks.ReloadIfChanged(); err == nil {
t.Fatal("corrupt file should error")
}
}