Files
2026-09-11 06:14:38 +02:00

181 lines
5.2 KiB
Go

package state
import (
"bytes"
"context"
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/example/ollama-fair-gateway/internal/auth"
"github.com/example/ollama-fair-gateway/internal/config"
)
func TestPersistentAPIKeySurvivesAuthenticatorRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "keys.json")
store, err := NewAPIKeyStore(path)
if err != nil {
t.Fatal(err)
}
a, err := auth.NewWithRuntimeStore(context.Background(), config.AuthConfig{}, store)
if err != nil {
t.Fatal(err)
}
info, secret, err := a.CreateAPIKey(auth.APIKeyCreate{Name: "openwebui", Tenant: "interactive", Application: "openwebui"})
if err != nil {
t.Fatal(err)
}
if info.Source != "persistent" {
t.Fatalf("source=%q", info.Source)
}
store2, err := NewAPIKeyStore(path)
if err != nil {
t.Fatal(err)
}
b, err := auth.NewWithRuntimeStore(context.Background(), config.AuthConfig{}, store2)
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest("GET", "http://gateway/api/tags", nil)
r.Header.Set("Authorization", "Bearer "+secret)
id, err := b.Authenticate(r)
if err != nil {
t.Fatal(err)
}
if id.Tenant != "interactive" || id.Application != "openwebui" {
t.Fatalf("identity=%#v", id)
}
if _, ok, err := b.DeleteAPIKey(info.ID); err != nil || !ok {
t.Fatalf("delete ok=%v err=%v", ok, err)
}
store3, err := NewAPIKeyStore(path)
if err != nil {
t.Fatal(err)
}
c, err := auth.NewWithRuntimeStore(context.Background(), config.AuthConfig{}, store3)
if err != nil {
t.Fatal(err)
}
if _, err := c.Authenticate(r); err == nil {
t.Fatal("deleted key authenticated after restart")
}
}
func TestPersistentPolicySurvivesRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "policies.json")
a, err := NewPolicyStore(path)
if err != nil {
t.Fatal(err)
}
want := config.TenantPolicy{TenantWeight: 2, ActorWeight: 3, TenantCreditsPerMinute: 0, ActorCreditsPerMinute: 120}
if err := a.Put(context.Background(), "team", want); err != nil {
t.Fatal(err)
}
b, err := NewPolicyStore(path)
if err != nil {
t.Fatal(err)
}
got, ok, err := b.Get(context.Background(), "team")
if err != nil || !ok {
t.Fatalf("get ok=%v err=%v", ok, err)
}
if got != want {
t.Fatalf("got=%#v want=%#v", got, want)
}
}
func TestConfigStoreKeepsBootstrapSecretsOutOfPersistentFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "gateway-config.json")
base := &config.Config{
Auth: config.AuthConfig{APIKeys: []config.APIKeyConfig{{Name: "admin", Key: "super-secret", Tenant: "ops"}}},
Workers: []config.WorkerConfig{{Name: "w", URL: "http://127.0.0.1:11434", MaxConcurrent: 1}},
UI: config.UIConfig{SessionSecret: "01234567890123456789012345678901", OIDC: config.UIOIDCConfig{ClientSecret: "oidc-secret"}},
}
// Fill defaults/validation through the normal parser so the stored object is realistic.
b, _ := json.Marshal(base)
parsed, err := config.ParseBytes(b)
if err != nil {
t.Fatal(err)
}
store := NewConfigStore(path)
if err := store.Save(parsed); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(raw, []byte("super-secret")) || bytes.Contains(raw, []byte("oidc-secret")) || bytes.Contains(raw, []byte("01234567890123456789012345678901")) {
t.Fatalf("persistent config leaked bootstrap secret: %s", raw)
}
loaded, err := store.LoadWithBootstrap(parsed)
if err != nil {
t.Fatal(err)
}
if loaded.Auth.APIKeys[0].Key != "super-secret" || loaded.UI.SessionSecret != parsed.UI.SessionSecret || loaded.UI.OIDC.ClientSecret != "oidc-secret" {
t.Fatalf("secrets not restored: %#v", loaded.UI)
}
}
func TestModelPlacementStoreSurvivesRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "model-placement.json")
a, err := NewModelPlacementStore(path)
if err != nil {
t.Fatal(err)
}
want := config.ModelPlacementRule{Mode: "whitelist", AllowedModels: []string{"qwen3:8b", "gemma4:*"}, DeniedModels: []string{"gemma4:e4b"}}
if err := a.Put(context.Background(), "rtx-4090", want); err != nil {
t.Fatal(err)
}
b, err := NewModelPlacementStore(path)
if err != nil {
t.Fatal(err)
}
got, ok, err := b.Get(context.Background(), "rtx-4090")
if err != nil || !ok {
t.Fatalf("get ok=%v err=%v", ok, err)
}
if got.Mode != want.Mode || len(got.AllowedModels) != 2 || len(got.DeniedModels) != 1 {
t.Fatalf("got=%#v want=%#v", got, want)
}
if err := b.Delete(context.Background(), "rtx-4090"); err != nil {
t.Fatal(err)
}
c, err := NewModelPlacementStore(path)
if err != nil {
t.Fatal(err)
}
if _, ok, _ := c.Get(context.Background(), "rtx-4090"); ok {
t.Fatal("deleted placement override survived restart")
}
}
func TestWorkerRuntimeStorePersistence(t *testing.T) {
path := filepath.Join(t.TempDir(), "worker-state.json")
s, err := NewWorkerRuntimeStore(path)
if err != nil {
t.Fatal(err)
}
if err := s.Put(context.Background(), "node-2", "draining"); err != nil {
t.Fatal(err)
}
s2, err := NewWorkerRuntimeStore(path)
if err != nil {
t.Fatal(err)
}
m, err := s2.List(context.Background())
if err != nil {
t.Fatal(err)
}
if m["node-2"] != "draining" {
t.Fatalf("modes=%v", m)
}
if err := s2.Delete(context.Background(), "node-2"); err != nil {
t.Fatal(err)
}
}