185 lines
4.9 KiB
Go
185 lines
4.9 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
key32a = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // 35 chars
|
|
key32b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
)
|
|
|
|
// clearKeyEnv removes all key-related env vars so each case starts clean.
|
|
func clearKeyEnv(t *testing.T) {
|
|
t.Helper()
|
|
for _, k := range []string{"API_KEY", "API_KEYS", "API_KEYS_FILE"} {
|
|
t.Setenv(k, "")
|
|
}
|
|
}
|
|
|
|
func TestLoadAPIKeys(t *testing.T) {
|
|
t.Run("legacy single key is full access", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
t.Setenv("API_KEY", key32a)
|
|
|
|
keys, err := loadAPIKeys()
|
|
if err != nil {
|
|
t.Fatalf("loadAPIKeys: %v", err)
|
|
}
|
|
if len(keys) != 1 || keys[0].Key != key32a || !keys[0].Scope.IsEmpty() {
|
|
t.Fatalf("unexpected keys: %+v", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("inline API_KEYS with scope", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
t.Setenv("API_KEYS", `[{"name":"dev","key":"`+key32a+`","collections":["Secrets - DEV"]}]`)
|
|
|
|
keys, err := loadAPIKeys()
|
|
if err != nil {
|
|
t.Fatalf("loadAPIKeys: %v", err)
|
|
}
|
|
if len(keys) != 1 {
|
|
t.Fatalf("want 1 key, got %d", len(keys))
|
|
}
|
|
if keys[0].Name != "dev" || len(keys[0].Scope.Collections) != 1 {
|
|
t.Errorf("unexpected key: %+v", keys[0])
|
|
}
|
|
})
|
|
|
|
t.Run("API_KEYS_FILE preferred over inline and merged with legacy", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "keys.json")
|
|
if err := os.WriteFile(path, []byte(`[{"name":"file","key":"`+key32b+`"}]`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("API_KEYS_FILE", path)
|
|
t.Setenv("API_KEYS", `[{"name":"ignored","key":"`+key32a+`"}]`)
|
|
t.Setenv("API_KEY", key32a)
|
|
|
|
keys, err := loadAPIKeys()
|
|
if err != nil {
|
|
t.Fatalf("loadAPIKeys: %v", err)
|
|
}
|
|
// File entry + legacy entry; inline API_KEYS is ignored when file is set.
|
|
if len(keys) != 2 {
|
|
t.Fatalf("want 2 keys (file + legacy), got %d: %+v", len(keys), keys)
|
|
}
|
|
if keys[0].Name != "file" || keys[1].Name != "legacy" {
|
|
t.Errorf("unexpected key order/names: %+v", keys)
|
|
}
|
|
})
|
|
|
|
t.Run("no keys configured", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
if _, err := loadAPIKeys(); err == nil {
|
|
t.Error("expected error when no keys configured")
|
|
}
|
|
})
|
|
|
|
t.Run("short key rejected", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
t.Setenv("API_KEY", "too-short")
|
|
if _, err := loadAPIKeys(); err == nil {
|
|
t.Error("expected error for short key")
|
|
}
|
|
})
|
|
|
|
t.Run("malformed JSON rejected", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
t.Setenv("API_KEYS", `not json`)
|
|
if _, err := loadAPIKeys(); err == nil {
|
|
t.Error("expected error for malformed JSON")
|
|
}
|
|
})
|
|
|
|
t.Run("unknown field rejected", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
// "collection" (singular) is a typo for "collections"; must fail loudly
|
|
// rather than silently leaving the key unscoped (full access).
|
|
t.Setenv("API_KEYS", `[{"name":"dev","key":"`+key32a+`","collection":["DEV"]}]`)
|
|
if _, err := loadAPIKeys(); err == nil {
|
|
t.Error("expected error for unknown JSON field")
|
|
}
|
|
})
|
|
|
|
t.Run("entry missing key rejected", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
t.Setenv("API_KEYS", `[{"name":"x"}]`)
|
|
if _, err := loadAPIKeys(); err == nil {
|
|
t.Error("expected error for entry without key")
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate key material rejected", func(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
// Same key string used twice would let one entry silently override the
|
|
// other's scope in the store.
|
|
t.Setenv("API_KEYS", `[{"name":"a","key":"`+key32a+`"},{"name":"b","key":"`+key32a+`"}]`)
|
|
if _, err := loadAPIKeys(); err == nil {
|
|
t.Error("expected error for duplicate key material")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseInt(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
in string
|
|
fallback int
|
|
want int
|
|
}{
|
|
{"50", 30, 50},
|
|
{"", 30, 30},
|
|
{"abc", 30, 30},
|
|
{"0", 30, 30},
|
|
{"-5", 30, 30},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := parseInt(tt.in, tt.fallback); got != tt.want {
|
|
t.Errorf("parseInt(%q, %d) = %d, want %d", tt.in, tt.fallback, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadRateLimitDefaultsAndOverrides(t *testing.T) {
|
|
clearKeyEnv(t)
|
|
t.Setenv("API_KEY", key32a)
|
|
t.Setenv("VAULTWARDEN_URL", "https://vault.example.com")
|
|
|
|
t.Run("defaults", func(t *testing.T) {
|
|
// Clear any inherited overrides so defaults are evaluated deterministically.
|
|
t.Setenv("RATE_LIMIT_MAX", "")
|
|
t.Setenv("RATE_LIMIT_WINDOW", "")
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.RateLimitMax != 30 {
|
|
t.Errorf("RateLimitMax = %d, want 30", cfg.RateLimitMax)
|
|
}
|
|
if cfg.RateLimitWindow != time.Minute {
|
|
t.Errorf("RateLimitWindow = %v, want 1m", cfg.RateLimitWindow)
|
|
}
|
|
})
|
|
|
|
t.Run("overrides", func(t *testing.T) {
|
|
t.Setenv("RATE_LIMIT_MAX", "100")
|
|
t.Setenv("RATE_LIMIT_WINDOW", "30s")
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.RateLimitMax != 100 {
|
|
t.Errorf("RateLimitMax = %d, want 100", cfg.RateLimitMax)
|
|
}
|
|
if cfg.RateLimitWindow != 30*time.Second {
|
|
t.Errorf("RateLimitWindow = %v, want 30s", cfg.RateLimitWindow)
|
|
}
|
|
})
|
|
}
|