2820823b36
Add an optional API-key layer to the public read-only API. Keys grant higher per-minute rate limits and attribute usage; anonymous callers are still allowed at a lower IP-based budget. - migration 0008_api_key: api_key table (sha256 hash only, plaintext shown once) - apikey pkg: key generation + hashing - ratelimit pkg: Redis fixed-window limiter + per-key usage counters; fails open - public API middleware: X-API-Key / Bearer auth, X-RateLimit-* headers, 429+Retry-After - admin: issue/list/revoke keys + usage view (API + UI tab) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
61 lines
1.4 KiB
Go
61 lines
1.4 KiB
Go
package apikey
|
|
|
|
import "testing"
|
|
|
|
func TestGenerate(t *testing.T) {
|
|
key, hash, prefix, err := Generate()
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if !IsWellFormed(key) {
|
|
t.Fatalf("generated key not well-formed: %q", key)
|
|
}
|
|
if Hash(key) != hash {
|
|
t.Fatalf("Hash(key) != returned hash")
|
|
}
|
|
if len(prefix) != prefixLen || key[:prefixLen] != prefix {
|
|
t.Fatalf("prefix %q not a %d-char prefix of key %q", prefix, prefixLen, key)
|
|
}
|
|
if len(hash) != 64 {
|
|
t.Fatalf("hash not hex sha-256: %q", hash)
|
|
}
|
|
}
|
|
|
|
func TestGenerateUnique(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 100; i++ {
|
|
k, _, _, err := Generate()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seen[k] {
|
|
t.Fatalf("duplicate key generated: %q", k)
|
|
}
|
|
seen[k] = true
|
|
}
|
|
}
|
|
|
|
func TestHashStableAndTrimmed(t *testing.T) {
|
|
if Hash("og_live_abc") != Hash(" og_live_abc ") {
|
|
t.Fatal("Hash should ignore surrounding whitespace")
|
|
}
|
|
if Hash("a") == Hash("b") {
|
|
t.Fatal("distinct inputs must hash differently")
|
|
}
|
|
}
|
|
|
|
func TestIsWellFormed(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"og_live_abcdefghijkl": true, // body longer than 8 chars
|
|
"og_live_": false, // empty body
|
|
"og_live_abc": false, // body too short
|
|
"nope_abcdefghijkl": false, // wrong prefix
|
|
"": false,
|
|
}
|
|
for in, want := range cases {
|
|
if got := IsWellFormed(in); got != want {
|
|
t.Errorf("IsWellFormed(%q) = %v, want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|