181 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|