Files
magpie/internal/access/access_test.go
T
yetone 46f03154ff gateway: a gateway key can be held to some models (#882, LCorleone) — a key could reach every model magpie serves. A key now keeps an optional list of models (caller-keys.json "models", none = every model, so older files read as before): <provider>/<model> or <provider>/*, matched against the provider that serves the call, so a bare model name is resolved first and OpenRouter's "anthropic/x" is not Anthropic's. Such a key sees only its models in /v1/models, /v1/models/{id} and the Anthropic (Claude Desktop) and Gemini lists, a routing group only when every member is its; asking for another, by id, bare name or through a group with a member it may not use, is refused before any provider is asked with a 403 in the API's own error shape (permission_error) that names the key and its models, logged as a rejected call like an over-limit key's 429; a fallback it may not use is skipped; embeddings, images and videos it names are held too. magpie's own calls for it (a web search, a picture described, Codex titles with the model Settings name) are marked in their context, not by a User-Agent a client could send, and go on. Set it with the new badge on the key's row in the Gateway page (the app's menu: All models, every model of a provider, each model; sent when the menu closes) or magpie gateway-key models <id> openai/gpt-5 anthropic/* (all takes it off); gateway-key list has a MODELS column. The app's menu, fitting neither below nor above its button, now goes on the roomier side and scrolls instead of running off the window. en/zh/ja/de. TestGatewayKeyModelsHoldAKey (lists, 403s by id, bare name and group, Anthropic shape, fallback skipped, magpie's own call, a free key) fails without the change; TestModelPatterns, TestKeyModelsKept, TestGatewayKeyModelsCLI, TestCallerKeyModelsRoutes pass; gateway-key-models.test.cjs (Chromium and WebKit, en and zh: no native select, menu inside a 420px window, picks sent on close, All models, no scroll, no left border, 560px fits) passes with gateway-connect-keys, gateway-key-limit, gateway-caller-keys and click-scroll. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 16:52:23 +08:00

305 lines
8.4 KiB
Go

package access
import (
"encoding/json"
"os"
"reflect"
"runtime"
"strings"
"sync"
"testing"
"github.com/yetone/magpie/internal/settings"
)
func TestNamedKeys(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
if _, err := Update("add-key", Change{Name: " "}); err == nil {
t.Fatal("accepted empty name")
}
keys, err := List()
if err != nil || len(keys) != 0 {
t.Fatal(keys, err)
}
secret, err := Update("add-key", Change{Name: "Laptop"})
if err != nil {
t.Fatal(err)
}
keys, err = List()
if err != nil || len(keys) != 1 || keys[0].Name != "Laptop" || keys[0].Secret != "" || !strings.HasPrefix(keys[0].Masked, Prefix+"…") {
t.Fatal(keys, err)
}
id := keys[0].ID
if who, ok := Authenticate(secret); !ok || who.KeyID != id || who.KeyName != "Laptop" {
t.Fatal(who, ok)
}
if _, ok := Authenticate(secret + "x"); ok {
t.Fatal("accepted invalid key")
}
for _, step := range []struct {
action, name string
valid bool
}{
{"rename-key", "Travel", true}, {"off-key", "", false}, {"on-key", "", true},
} {
if _, err := Update(step.action, Change{Key: id, Name: step.name}); err != nil {
t.Fatal(err)
}
if who, ok := Authenticate(secret); ok != step.valid || (ok && who.KeyName != "Travel") {
t.Fatal(step.action, who, ok)
}
}
if got, err := Update("copy-key", Change{Key: id}); err != nil || got != secret {
t.Fatal("copy", err)
}
st, err := os.Stat(Path())
if err != nil || runtime.GOOS != "windows" && st.Mode().Perm() != 0o600 {
t.Fatal("permissions", err)
}
var persisted []Key
b, _ := os.ReadFile(Path())
if err := json.Unmarshal(b, &persisted); err != nil || len(persisted) != 1 || persisted[0].ID != id || persisted[0].Secret != secret {
t.Fatal("key was not persisted", err)
}
if _, err := Update("remove-key", Change{Key: id}); err != nil {
t.Fatal(err)
}
if _, ok := Authenticate(secret); ok {
t.Fatal("removed key works")
}
}
func TestConcurrentKeyCreationAndCorruptStore(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
var wg sync.WaitGroup
for i := 0; i < 12; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := Update("add-key", Change{Name: "Key"}); err != nil {
t.Error(err)
}
}()
}
wg.Wait()
keys, _ := List()
if len(keys) != 12 {
t.Fatal("lost concurrent changes")
}
secret, _ := Update("copy-key", Change{Key: keys[0].ID})
os.WriteFile(Path(), []byte("broken"), 0o600)
if _, ok := Authenticate(secret); ok {
t.Fatal("corrupt store accepted a key")
}
if _, err := Update("add-key", Change{Name: "New"}); err == nil {
t.Fatal("overwrote corrupt store")
}
}
func TestLegacyLANKeyMigrationIsIdempotentAndRevocable(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
s := settings.Load()
s.LAN, s.LANKey = true, "sk-magpie-legacy-lan-secret"
if err := settings.Save(s); err != nil {
t.Fatal(err)
}
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
keys, err := List()
if err != nil || len(keys) != 1 || keys[0].Name != "Magpie" || !strings.HasPrefix(keys[0].Masked, "sk-magpie-…") {
t.Fatal(keys, err)
}
if settings.Load().LANKey != s.LANKey || !settings.Load().LAN {
t.Fatal("migration broke older Magpie's sharing settings")
}
if who, ok := Authenticate(s.LANKey); !ok || who.KeyID != keys[0].ID {
t.Fatal(who, ok)
}
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
keys, _ = List()
if len(keys) != 1 {
t.Fatal("duplicate migration", keys)
}
if _, err := Update("off-key", Change{Key: keys[0].ID}); err != nil {
t.Fatal(err)
}
if _, ok := Authenticate(s.LANKey); ok {
t.Fatal("disabled LAN key still works")
}
if _, err := Update("remove-key", Change{Key: keys[0].ID}); err != nil {
t.Fatal(err)
}
if _, ok := Authenticate(s.LANKey); ok {
t.Fatal("removed LAN key still works")
}
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
if keys, _ := List(); len(keys) != 0 {
t.Fatal("retained legacy credential resurrected a removed key", keys)
}
}
func TestLegacyLANKeyMigrationPreservesNamedKeys(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
secret, err := Update("add-key", Change{Name: "Client"})
if err != nil {
t.Fatal(err)
}
keys, _ := List()
id := keys[0].ID
s := settings.Load()
s.LANKey = "sk-magpie-legacy"
if err := settings.Save(s); err != nil {
t.Fatal(err)
}
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
keys, _ = List()
if len(keys) != 2 || keys[0].ID != id || keys[1].Name != "Magpie" {
t.Fatal(keys)
}
if _, ok := Authenticate(secret); !ok {
t.Fatal("lost named key")
}
if _, ok := Authenticate(s.LANKey); !ok {
t.Fatal("lost LAN key")
}
}
func TestLegacyLANKeyMigrationRetryAndFailure(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
s := settings.Load()
s.LANKey = "sk-magpie-test-legacy-lan"
if err := settings.Save(s); err != nil {
t.Fatal(err)
}
// The key write succeeded but saving the migration marker was interrupted.
mu.Lock()
err := save([]Key{{ID: "migrated", Name: "Remote", Off: true, Secret: s.LANKey}})
mu.Unlock()
if err != nil {
t.Fatal(err)
}
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
keys, err := List()
if err != nil || len(keys) != 1 || keys[0].ID != "migrated" || !keys[0].Off || settings.Load().LANKeyID != "migrated" {
t.Fatal("retry duplicated or re-enabled a key", keys, err)
}
if err := settings.Save(s); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(Path(), []byte("broken"), 0o600); err != nil {
t.Fatal(err)
}
if err := MigrateLegacyLANKey(); err == nil {
t.Fatal("migration accepted a corrupt store")
}
if settings.Load().LANKey != s.LANKey {
t.Fatal("failed migration cleared the old credential")
}
}
func TestNamedLANKeyKeepsItsName(t *testing.T) {
for _, name := range []string{"Local network", "Magpie", "Desk"} {
t.Run(name, func(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
s := settings.Load()
s.LANKeyID = "lan"
if err := settings.Save(s); err != nil {
t.Fatal(err)
}
original := []Key{{ID: "lan", Name: name, Off: true, Secret: Prefix + "original"},
{ID: "other", Name: "Other", Secret: Prefix + "other"},
{ID: "named", Name: "Local network", Secret: Prefix + "named"}}
if err := save(original); err != nil {
t.Fatal(err)
}
for range 2 {
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
}
keys, err := load()
if err != nil || !reflect.DeepEqual(keys, original) {
t.Fatal("migration changed an existing named key", keys, err)
}
})
}
}
func TestCallerKeyRotationPreservesIdentityAndState(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
old, err := Update("add-key", Change{Name: "Desk"})
if err != nil {
t.Fatal(err)
}
other, err := Update("add-key", Change{Name: "Other"})
if err != nil {
t.Fatal(err)
}
keys, _ := List()
id := keys[0].ID
for _, off := range []bool{false, true} {
if off {
if _, err := Update("off-key", Change{Key: id}); err != nil {
t.Fatal(err)
}
}
secret, err := Update("rotate-key", Change{Key: id})
if err != nil || secret == old || !strings.HasPrefix(secret, Prefix) {
t.Fatal("key was not rotated", err)
}
if _, ok := Authenticate(old); ok {
t.Fatal("old key still authenticates")
}
if who, ok := Authenticate(secret); ok == off || (!off && (who.KeyID != id || who.KeyName != "Desk")) {
t.Fatal("rotation changed identity or enabled state", who, ok)
}
keys, _ = List()
if keys[0].ID != id || keys[0].Name != "Desk" || keys[0].Off != off || keys[0].Secret != "" {
t.Fatal(keys)
}
if _, ok := Authenticate(other); !ok {
t.Fatal("rotation revoked another key")
}
old = secret
}
if _, err := Update("rotate-key", Change{Key: "missing"}); err == nil {
t.Fatal("rotated a missing key")
}
}
func TestLANRotationKeepsOlderMagpieWorking(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
if err := ConfigureLAN(true, false); err != nil {
t.Fatal(err)
}
before := settings.Load()
if before.LANKey == "" {
t.Fatal("older Magpie has no sharing key")
}
next, err := Update("rotate-key", Change{Key: before.LANKeyID})
if err != nil {
t.Fatal(err)
}
if settings.Load().LANKey != next {
t.Fatal("rotation left the old version with a stale key")
}
if err := MigrateLegacyLANKey(); err != nil {
t.Fatal(err)
}
if keys, _ := List(); len(keys) != 1 || keys[0].ID != before.LANKeyID {
t.Fatal("rotation duplicated the default key", keys)
}
if _, ok := Authenticate(before.LANKey); ok {
t.Fatal("old key resurrected")
}
}