Files
llmgw/internal/store/keys_test.go
T
Ilker ManapandClaude Opus 5 29617f8cb1 Add the store: tenancy, keys and usage on SQLite
Phase 5 begins. This is the persistence layer for the things the cluster
cannot reconstruct — who may call, what they are allowed, what they used.
Everything else llmgw knows is still derived from the fleet, which cannot be
wrong about itself the way a database can (§6.5).

SQLite behind an interface. The plan justified that by write volume, which was
the wrong argument: the write path is batched anyway, and a 500-row
transaction every 200ms is nothing for either engine. The real argument is the
failure mode. Postgres is a second service, and a gateway that stops serving
because its accounting database is unreachable is worse than one that buffers
some accounting. Embedded means the store shares the gateway's fate instead of
adding an independent way to be down. That reasoning stops holding at two
instances, where budgets have to be counted somewhere shared — so the
interface is here from the start, the SQL is portable, and migrations are per
dialect with shared version numbers.

Keys are stored as sha256 and nothing else, so a database leak hands over no
working credential. The fast hash is deliberate: the input is 256 random bits
with no dictionary to attack, so argon2 would buy nothing and charge tens of
milliseconds per request. That assumption is written down next to the code
that depends on it.

Two things the schema now says that it did not: budgets are integer
micro-units, because repeated addition of floats drifts; and an empty model
allowlist forbids everything while an absent one permits everything, which the
resolver has to keep distinct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 08:37:40 +02:00

79 lines
2.1 KiB
Go

package store
import (
"crypto/sha256"
"strings"
"testing"
)
func TestMintedKeysAreUniqueAndWellFormed(t *testing.T) {
seen := map[string]struct{}{}
for range 200 {
secret, k, err := NewKey("id", "name", "proj")
if err != nil {
t.Fatal(err)
}
if _, dup := seen[secret]; dup {
t.Fatal("two keys came out identical")
}
seen[secret] = struct{}{}
if err := ParseKey(secret); err != nil {
t.Fatalf("a minted key does not parse: %v (%q)", err, secret)
}
// Fixed width, so a key's length says nothing about its value and
// listings line up.
if len(secret) != len(KeyPrefix)+43 {
t.Fatalf("length %d: %q", len(secret), secret)
}
sum := sha256.Sum256([]byte(secret))
if string(k.Hash) != string(sum[:]) {
t.Fatal("the stored hash is not the hash of the key")
}
}
}
func TestParseRejectsWhatCannotBeAKey(t *testing.T) {
// Cheap rejection, so a bearer token from another system never reaches the
// database at all.
for _, bad := range []string{
"", "hello", "Bearer abc",
"sk-llmgw-",
"sk-llmgw-short",
"sk-proj-0123456789012345678901234567890123456789012",
"sk-llmgw-0123456789012345678901234567890123456789+/=",
} {
if err := ParseKey(bad); err == nil {
t.Errorf("%q was accepted as a key", bad)
}
}
}
func TestDisplayPrefixRevealsAlmostNothing(t *testing.T) {
secret, k, _ := NewKey("id", "name", "proj")
if !strings.HasPrefix(secret, k.Prefix[:len(KeyPrefix)+4]) {
t.Errorf("prefix %q does not belong to %q", k.Prefix, secret)
}
// Four characters of base62 is enough to tell keys apart in a listing and
// about 15 million times too few to guess the rest.
body := strings.TrimSuffix(strings.TrimPrefix(k.Prefix, KeyPrefix), "…")
if len(body) != 4 {
t.Errorf("the display prefix shows %d characters", len(body))
}
}
func TestEqualHashIsUsable(t *testing.T) {
a := HashKey("sk-llmgw-aaa")
b := HashKey("sk-llmgw-aaa")
c := HashKey("sk-llmgw-bbb")
if !EqualHash(a, b) {
t.Error("equal hashes compared unequal")
}
if EqualHash(a, c) {
t.Error("different hashes compared equal")
}
if EqualHash(a, c[:16]) {
t.Error("a truncated hash compared equal")
}
}