Pulse/internal/config/api_tokens_test.go
rcourtman ed75f2f096 test: Add comprehensive tests for API token management
- Clone: deep copy verification for pointers and slices
- NewAPITokenRecord/NewHashedAPITokenRecord: creation and validation
- Config methods: HasAPITokens, APITokenCount, ActiveAPITokenHashes
- Config methods: HasAPITokenHash, PrimaryAPITokenHash, PrimaryAPITokenHint
- Config methods: ValidateAPIToken, UpsertAPIToken, RemoveAPIToken, SortAPITokens

config package coverage: 43.5% → 46.3%
2025-12-01 17:37:27 +00:00

1029 lines
28 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
)
func TestAPITokenRecordHasScope(t *testing.T) {
record := APITokenRecord{Scopes: []string{ScopeMonitoringRead}}
if !record.HasScope(ScopeMonitoringRead) {
t.Fatalf("expected scope %q to be granted", ScopeMonitoringRead)
}
if record.HasScope(ScopeSettingsWrite) {
t.Fatalf("did not expect scope %q to be granted", ScopeSettingsWrite)
}
record.Scopes = nil // legacy tokens with no scopes should default to wildcard
if !record.HasScope(ScopeSettingsWrite) {
t.Fatalf("expected wildcard to grant %q", ScopeSettingsWrite)
}
// Empty scope always returns true
record.Scopes = []string{ScopeMonitoringRead}
if !record.HasScope("") {
t.Fatalf("expected empty scope to always be granted")
}
// Explicit wildcard scope in list grants any scope
record.Scopes = []string{ScopeWildcard}
if !record.HasScope(ScopeSettingsWrite) {
t.Fatalf("expected wildcard scope in list to grant %q", ScopeSettingsWrite)
}
if !record.HasScope(ScopeMonitoringRead) {
t.Fatalf("expected wildcard scope in list to grant %q", ScopeMonitoringRead)
}
if !IsKnownScope(ScopeMonitoringRead) {
t.Fatalf("expected %q to be known scope", ScopeMonitoringRead)
}
if IsKnownScope("unknown:scope") {
t.Fatalf("unexpected scope recognised")
}
}
func TestTokenPrefix(t *testing.T) {
tests := []struct {
name string
value string
expected string
}{
{name: "longer than 6 chars", value: "abcdefghij", expected: "abcdef"},
{name: "exactly 6 chars", value: "abcdef", expected: "abcdef"},
{name: "shorter than 6 chars", value: "abc", expected: "abc"},
{name: "empty string", value: "", expected: ""},
{name: "single char", value: "x", expected: "x"},
{name: "5 chars", value: "12345", expected: "12345"},
{name: "7 chars", value: "1234567", expected: "123456"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tokenPrefix(tt.value)
if result != tt.expected {
t.Errorf("tokenPrefix(%q) = %q, want %q", tt.value, result, tt.expected)
}
})
}
}
func TestTokenSuffix(t *testing.T) {
tests := []struct {
name string
value string
expected string
}{
{name: "longer than 4 chars", value: "abcdefghij", expected: "ghij"},
{name: "exactly 4 chars", value: "abcd", expected: "abcd"},
{name: "shorter than 4 chars", value: "abc", expected: "abc"},
{name: "empty string", value: "", expected: ""},
{name: "single char", value: "x", expected: "x"},
{name: "3 chars", value: "123", expected: "123"},
{name: "5 chars", value: "12345", expected: "2345"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tokenSuffix(tt.value)
if result != tt.expected {
t.Errorf("tokenSuffix(%q) = %q, want %q", tt.value, result, tt.expected)
}
})
}
}
func TestNormalizeScopes(t *testing.T) {
tests := []struct {
name string
scopes []string
expected []string
}{
{
name: "nil returns wildcard",
scopes: nil,
expected: []string{ScopeWildcard},
},
{
name: "empty returns wildcard",
scopes: []string{},
expected: []string{ScopeWildcard},
},
{
name: "single scope preserved",
scopes: []string{ScopeMonitoringRead},
expected: []string{ScopeMonitoringRead},
},
{
name: "multiple scopes preserved",
scopes: []string{ScopeMonitoringRead, ScopeSettingsWrite},
expected: []string{ScopeMonitoringRead, ScopeSettingsWrite},
},
{
name: "wildcard alone preserved",
scopes: []string{ScopeWildcard},
expected: []string{ScopeWildcard},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeScopes(tt.scopes)
if len(result) != len(tt.expected) {
t.Fatalf("normalizeScopes(%v) length = %d, want %d", tt.scopes, len(result), len(tt.expected))
}
for i, v := range result {
if v != tt.expected[i] {
t.Errorf("normalizeScopes(%v)[%d] = %q, want %q", tt.scopes, i, v, tt.expected[i])
}
}
})
}
}
func TestNormalizeScopes_ReturnsCopy(t *testing.T) {
original := []string{ScopeMonitoringRead, ScopeSettingsWrite}
result := normalizeScopes(original)
// Modify result and verify original is unchanged
result[0] = "modified"
if original[0] != ScopeMonitoringRead {
t.Errorf("normalizeScopes did not return a copy; original was modified")
}
}
func TestIsKnownScope(t *testing.T) {
tests := []struct {
name string
scope string
expected bool
}{
{name: "wildcard", scope: ScopeWildcard, expected: true},
{name: "monitoring:read", scope: ScopeMonitoringRead, expected: true},
{name: "monitoring:write", scope: ScopeMonitoringWrite, expected: true},
{name: "docker:report", scope: ScopeDockerReport, expected: true},
{name: "docker:manage", scope: ScopeDockerManage, expected: true},
{name: "host-agent:report", scope: ScopeHostReport, expected: true},
{name: "host-agent:manage", scope: ScopeHostManage, expected: true},
{name: "settings:read", scope: ScopeSettingsRead, expected: true},
{name: "settings:write", scope: ScopeSettingsWrite, expected: true},
{name: "unknown scope", scope: "unknown:scope", expected: false},
{name: "empty string", scope: "", expected: false},
{name: "partial match", scope: "monitoring", expected: false},
{name: "case sensitive", scope: "MONITORING:READ", expected: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsKnownScope(tt.scope)
if result != tt.expected {
t.Errorf("IsKnownScope(%q) = %v, want %v", tt.scope, result, tt.expected)
}
})
}
}
func TestLoadAPITokensAppliesLegacyScopes(t *testing.T) {
if len(AllKnownScopes) == 0 {
t.Fatal("expected known scopes to be defined")
}
dir := t.TempDir()
persistence := NewConfigPersistence(dir)
if err := persistence.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
payload := `[{"id":"legacy","name":"legacy","hash":"abc","createdAt":"2024-01-01T00:00:00Z"}]`
if err := os.WriteFile(filepath.Join(dir, "api_tokens.json"), []byte(payload), 0600); err != nil {
t.Fatalf("write api_tokens.json: %v", err)
}
tokens, err := persistence.LoadAPITokens()
if err != nil {
t.Fatalf("LoadAPITokens: %v", err)
}
if len(tokens) != 1 {
t.Fatalf("expected 1 token, got %d", len(tokens))
}
if len(tokens[0].Scopes) != 1 || tokens[0].Scopes[0] != ScopeWildcard {
t.Fatalf("expected legacy token to default to wildcard scope, got %#v", tokens[0].Scopes)
}
}
func TestLoadAPITokens_ErrorPaths(t *testing.T) {
t.Run("nonexistent file returns empty slice", func(t *testing.T) {
dir := t.TempDir()
persistence := NewConfigPersistence(dir)
tokens, err := persistence.LoadAPITokens()
if err != nil {
t.Fatalf("expected no error for missing file, got: %v", err)
}
if len(tokens) != 0 {
t.Fatalf("expected empty slice, got %d tokens", len(tokens))
}
})
t.Run("empty file returns empty slice", func(t *testing.T) {
dir := t.TempDir()
persistence := NewConfigPersistence(dir)
if err := persistence.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Write empty file
if err := os.WriteFile(filepath.Join(dir, "api_tokens.json"), []byte{}, 0600); err != nil {
t.Fatalf("write file: %v", err)
}
tokens, err := persistence.LoadAPITokens()
if err != nil {
t.Fatalf("expected no error for empty file, got: %v", err)
}
if len(tokens) != 0 {
t.Fatalf("expected empty slice, got %d tokens", len(tokens))
}
})
t.Run("invalid JSON returns error", func(t *testing.T) {
dir := t.TempDir()
persistence := NewConfigPersistence(dir)
if err := persistence.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Write invalid JSON
if err := os.WriteFile(filepath.Join(dir, "api_tokens.json"), []byte("not valid json"), 0600); err != nil {
t.Fatalf("write file: %v", err)
}
_, err := persistence.LoadAPITokens()
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
})
}
func TestClone(t *testing.T) {
t.Run("clones all fields", func(t *testing.T) {
now := time.Now().UTC()
original := APITokenRecord{
ID: "test-id",
Name: "test-name",
Hash: "test-hash",
Prefix: "prefix",
Suffix: "suffix",
CreatedAt: now,
LastUsedAt: &now,
Scopes: []string{ScopeMonitoringRead, ScopeSettingsWrite},
}
clone := original.Clone()
if clone.ID != original.ID {
t.Errorf("ID: got %q, want %q", clone.ID, original.ID)
}
if clone.Name != original.Name {
t.Errorf("Name: got %q, want %q", clone.Name, original.Name)
}
if clone.Hash != original.Hash {
t.Errorf("Hash: got %q, want %q", clone.Hash, original.Hash)
}
if clone.Prefix != original.Prefix {
t.Errorf("Prefix: got %q, want %q", clone.Prefix, original.Prefix)
}
if clone.Suffix != original.Suffix {
t.Errorf("Suffix: got %q, want %q", clone.Suffix, original.Suffix)
}
if !clone.CreatedAt.Equal(original.CreatedAt) {
t.Errorf("CreatedAt: got %v, want %v", clone.CreatedAt, original.CreatedAt)
}
if clone.LastUsedAt == nil {
t.Fatal("LastUsedAt should not be nil")
}
if !clone.LastUsedAt.Equal(*original.LastUsedAt) {
t.Errorf("LastUsedAt: got %v, want %v", *clone.LastUsedAt, *original.LastUsedAt)
}
})
t.Run("LastUsedAt is deep copied", func(t *testing.T) {
now := time.Now().UTC()
original := APITokenRecord{
ID: "test-id",
LastUsedAt: &now,
Scopes: []string{ScopeMonitoringRead},
}
clone := original.Clone()
// Modify the clone's LastUsedAt
newTime := now.Add(time.Hour)
*clone.LastUsedAt = newTime
// Original should be unchanged
if !original.LastUsedAt.Equal(now) {
t.Errorf("modifying clone affected original: got %v, want %v", *original.LastUsedAt, now)
}
})
t.Run("Scopes slice is deep copied", func(t *testing.T) {
original := APITokenRecord{
ID: "test-id",
Scopes: []string{ScopeMonitoringRead, ScopeSettingsWrite},
}
clone := original.Clone()
// Modify the clone's Scopes
clone.Scopes[0] = "modified"
// Original should be unchanged
if original.Scopes[0] != ScopeMonitoringRead {
t.Errorf("modifying clone scopes affected original: got %q, want %q", original.Scopes[0], ScopeMonitoringRead)
}
})
t.Run("nil LastUsedAt stays nil", func(t *testing.T) {
original := APITokenRecord{
ID: "test-id",
LastUsedAt: nil,
Scopes: []string{ScopeMonitoringRead},
}
clone := original.Clone()
if clone.LastUsedAt != nil {
t.Errorf("LastUsedAt should be nil, got %v", clone.LastUsedAt)
}
})
t.Run("empty scopes normalized to wildcard", func(t *testing.T) {
original := APITokenRecord{
ID: "test-id",
Scopes: nil,
}
clone := original.Clone()
if len(clone.Scopes) != 1 || clone.Scopes[0] != ScopeWildcard {
t.Errorf("empty scopes should normalize to wildcard, got %v", clone.Scopes)
}
})
}
func TestNewAPITokenRecord(t *testing.T) {
t.Run("valid token and name", func(t *testing.T) {
token := "my-secret-token-12345"
name := "Test Token"
scopes := []string{ScopeMonitoringRead}
record, err := NewAPITokenRecord(token, name, scopes)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if record.ID == "" {
t.Error("ID should be set")
}
if record.Name != name {
t.Errorf("Name: got %q, want %q", record.Name, name)
}
if record.Hash == "" {
t.Error("Hash should be set")
}
if record.Hash == token {
t.Error("Hash should not equal raw token")
}
if record.Prefix != "my-sec" {
t.Errorf("Prefix: got %q, want %q", record.Prefix, "my-sec")
}
if record.Suffix != "2345" {
t.Errorf("Suffix: got %q, want %q", record.Suffix, "2345")
}
if record.CreatedAt.IsZero() {
t.Error("CreatedAt should be set")
}
if len(record.Scopes) != 1 || record.Scopes[0] != ScopeMonitoringRead {
t.Errorf("Scopes: got %v, want [%s]", record.Scopes, ScopeMonitoringRead)
}
})
t.Run("empty token returns ErrInvalidToken", func(t *testing.T) {
_, err := NewAPITokenRecord("", "Test", nil)
if err != ErrInvalidToken {
t.Errorf("expected ErrInvalidToken, got %v", err)
}
})
t.Run("empty scopes normalized to wildcard", func(t *testing.T) {
record, err := NewAPITokenRecord("some-token", "Test", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(record.Scopes) != 1 || record.Scopes[0] != ScopeWildcard {
t.Errorf("expected wildcard scope, got %v", record.Scopes)
}
})
t.Run("empty slice scopes normalized to wildcard", func(t *testing.T) {
record, err := NewAPITokenRecord("some-token", "Test", []string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(record.Scopes) != 1 || record.Scopes[0] != ScopeWildcard {
t.Errorf("expected wildcard scope, got %v", record.Scopes)
}
})
t.Run("prefix and suffix set from token", func(t *testing.T) {
record, err := NewAPITokenRecord("abcdefghijklmnop", "Test", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if record.Prefix != "abcdef" {
t.Errorf("Prefix: got %q, want %q", record.Prefix, "abcdef")
}
if record.Suffix != "mnop" {
t.Errorf("Suffix: got %q, want %q", record.Suffix, "mnop")
}
})
t.Run("short token handles prefix/suffix", func(t *testing.T) {
record, err := NewAPITokenRecord("abc", "Test", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if record.Prefix != "abc" {
t.Errorf("Prefix: got %q, want %q", record.Prefix, "abc")
}
if record.Suffix != "abc" {
t.Errorf("Suffix: got %q, want %q", record.Suffix, "abc")
}
})
}
func TestNewHashedAPITokenRecord(t *testing.T) {
t.Run("valid hash", func(t *testing.T) {
hash := "abcdef1234567890"
name := "Test Token"
created := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
scopes := []string{ScopeMonitoringRead}
record, err := NewHashedAPITokenRecord(hash, name, created, scopes)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if record.ID == "" {
t.Error("ID should be set")
}
if record.Name != name {
t.Errorf("Name: got %q, want %q", record.Name, name)
}
if record.Hash != hash {
t.Errorf("Hash: got %q, want %q", record.Hash, hash)
}
if !record.CreatedAt.Equal(created) {
t.Errorf("CreatedAt: got %v, want %v", record.CreatedAt, created)
}
})
t.Run("empty hash returns ErrInvalidToken", func(t *testing.T) {
_, err := NewHashedAPITokenRecord("", "Test", time.Now(), nil)
if err != ErrInvalidToken {
t.Errorf("expected ErrInvalidToken, got %v", err)
}
})
t.Run("zero time gets set to now", func(t *testing.T) {
before := time.Now().UTC()
record, err := NewHashedAPITokenRecord("somehash", "Test", time.Time{}, nil)
after := time.Now().UTC()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if record.CreatedAt.Before(before) || record.CreatedAt.After(after) {
t.Errorf("CreatedAt should be between %v and %v, got %v", before, after, record.CreatedAt)
}
})
t.Run("scopes normalized", func(t *testing.T) {
record, err := NewHashedAPITokenRecord("somehash", "Test", time.Now(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(record.Scopes) != 1 || record.Scopes[0] != ScopeWildcard {
t.Errorf("expected wildcard scope, got %v", record.Scopes)
}
})
t.Run("prefix and suffix set from hash", func(t *testing.T) {
record, err := NewHashedAPITokenRecord("abcdefghijklmnop", "Test", time.Now(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if record.Prefix != "abcdef" {
t.Errorf("Prefix: got %q, want %q", record.Prefix, "abcdef")
}
if record.Suffix != "mnop" {
t.Errorf("Suffix: got %q, want %q", record.Suffix, "mnop")
}
})
}
func TestHasAPITokens(t *testing.T) {
t.Run("empty slice returns false", func(t *testing.T) {
cfg := &Config{APITokens: nil}
if cfg.HasAPITokens() {
t.Error("expected false for nil tokens")
}
cfg.APITokens = []APITokenRecord{}
if cfg.HasAPITokens() {
t.Error("expected false for empty tokens")
}
})
t.Run("non-empty slice returns true", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "test", Hash: "hash"},
},
}
if !cfg.HasAPITokens() {
t.Error("expected true for non-empty tokens")
}
})
}
func TestAPITokenCount(t *testing.T) {
t.Run("empty returns 0", func(t *testing.T) {
cfg := &Config{APITokens: nil}
if cfg.APITokenCount() != 0 {
t.Errorf("expected 0, got %d", cfg.APITokenCount())
}
})
t.Run("returns correct count", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "hash1"},
{ID: "2", Hash: "hash2"},
{ID: "3", Hash: "hash3"},
},
}
if cfg.APITokenCount() != 3 {
t.Errorf("expected 3, got %d", cfg.APITokenCount())
}
})
}
func TestActiveAPITokenHashes(t *testing.T) {
t.Run("empty tokens returns empty slice", func(t *testing.T) {
cfg := &Config{APITokens: nil}
hashes := cfg.ActiveAPITokenHashes()
if len(hashes) != 0 {
t.Errorf("expected empty slice, got %v", hashes)
}
})
t.Run("skips records with empty hash", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "hash1"},
{ID: "2", Hash: ""},
{ID: "3", Hash: "hash3"},
},
}
hashes := cfg.ActiveAPITokenHashes()
if len(hashes) != 2 {
t.Fatalf("expected 2 hashes, got %d", len(hashes))
}
if hashes[0] != "hash1" || hashes[1] != "hash3" {
t.Errorf("expected [hash1, hash3], got %v", hashes)
}
})
t.Run("returns all valid hashes", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "hash1"},
{ID: "2", Hash: "hash2"},
},
}
hashes := cfg.ActiveAPITokenHashes()
if len(hashes) != 2 {
t.Fatalf("expected 2 hashes, got %d", len(hashes))
}
})
}
func TestHasAPITokenHash(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "existing-hash"},
{ID: "2", Hash: "another-hash"},
},
}
t.Run("returns true when hash exists", func(t *testing.T) {
if !cfg.HasAPITokenHash("existing-hash") {
t.Error("expected true for existing hash")
}
if !cfg.HasAPITokenHash("another-hash") {
t.Error("expected true for another existing hash")
}
})
t.Run("returns false when hash doesn't exist", func(t *testing.T) {
if cfg.HasAPITokenHash("nonexistent") {
t.Error("expected false for nonexistent hash")
}
})
t.Run("returns false for empty config", func(t *testing.T) {
emptyCfg := &Config{}
if emptyCfg.HasAPITokenHash("any-hash") {
t.Error("expected false for empty config")
}
})
}
func TestPrimaryAPITokenHash(t *testing.T) {
t.Run("empty tokens returns empty string", func(t *testing.T) {
cfg := &Config{APITokens: nil}
if cfg.PrimaryAPITokenHash() != "" {
t.Errorf("expected empty string, got %q", cfg.PrimaryAPITokenHash())
}
})
t.Run("returns first token's hash", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "first-hash"},
{ID: "2", Hash: "second-hash"},
},
}
if cfg.PrimaryAPITokenHash() != "first-hash" {
t.Errorf("expected first-hash, got %q", cfg.PrimaryAPITokenHash())
}
})
}
func TestPrimaryAPITokenHint(t *testing.T) {
t.Run("empty tokens returns empty string", func(t *testing.T) {
cfg := &Config{APITokens: nil}
if cfg.PrimaryAPITokenHint() != "" {
t.Errorf("expected empty string, got %q", cfg.PrimaryAPITokenHint())
}
})
t.Run("returns prefix...suffix format when both exist", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Prefix: "abcdef", Suffix: "wxyz"},
},
}
expected := "abcdef...wxyz"
if cfg.PrimaryAPITokenHint() != expected {
t.Errorf("expected %q, got %q", expected, cfg.PrimaryAPITokenHint())
}
})
t.Run("falls back to hash truncation when no prefix", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "12345678abcdefgh", Prefix: "", Suffix: "wxyz"},
},
}
expected := "1234...efgh"
if cfg.PrimaryAPITokenHint() != expected {
t.Errorf("expected %q, got %q", expected, cfg.PrimaryAPITokenHint())
}
})
t.Run("falls back to hash truncation when no suffix", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "12345678abcdefgh", Prefix: "abcdef", Suffix: ""},
},
}
expected := "1234...efgh"
if cfg.PrimaryAPITokenHint() != expected {
t.Errorf("expected %q, got %q", expected, cfg.PrimaryAPITokenHint())
}
})
t.Run("returns empty for short hash without prefix/suffix", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "short", Prefix: "", Suffix: ""},
},
}
if cfg.PrimaryAPITokenHint() != "" {
t.Errorf("expected empty string for short hash, got %q", cfg.PrimaryAPITokenHint())
}
})
}
func TestValidateAPIToken(t *testing.T) {
rawToken := "my-secret-api-token-123"
hashedToken := auth.HashAPIToken(rawToken)
t.Run("empty token returns nil, false", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: hashedToken, Scopes: []string{ScopeWildcard}},
},
}
record, valid := cfg.ValidateAPIToken("")
if record != nil || valid {
t.Error("expected nil, false for empty token")
}
})
t.Run("invalid token returns nil, false", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: hashedToken, Scopes: []string{ScopeWildcard}},
},
}
record, valid := cfg.ValidateAPIToken("wrong-token")
if record != nil || valid {
t.Error("expected nil, false for invalid token")
}
})
t.Run("valid token returns record, true and updates LastUsedAt", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: hashedToken, Scopes: []string{ScopeMonitoringRead}},
},
}
before := time.Now().UTC()
record, valid := cfg.ValidateAPIToken(rawToken)
after := time.Now().UTC()
if !valid {
t.Fatal("expected valid=true")
}
if record == nil {
t.Fatal("expected non-nil record")
}
if record.ID != "1" {
t.Errorf("expected ID=1, got %q", record.ID)
}
if record.LastUsedAt == nil {
t.Fatal("LastUsedAt should be set")
}
if record.LastUsedAt.Before(before) || record.LastUsedAt.After(after) {
t.Errorf("LastUsedAt should be between %v and %v, got %v", before, after, *record.LastUsedAt)
}
// Verify the config's record was also updated
if cfg.APITokens[0].LastUsedAt == nil {
t.Error("config's record LastUsedAt should be updated")
}
})
t.Run("validates against multiple tokens", func(t *testing.T) {
token2 := "another-token-456"
hash2 := auth.HashAPIToken(token2)
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: hashedToken, Scopes: []string{ScopeWildcard}},
{ID: "2", Hash: hash2, Scopes: []string{ScopeMonitoringRead}},
},
}
record, valid := cfg.ValidateAPIToken(token2)
if !valid {
t.Fatal("expected valid=true for second token")
}
if record.ID != "2" {
t.Errorf("expected ID=2, got %q", record.ID)
}
})
t.Run("empty config returns nil, false", func(t *testing.T) {
cfg := &Config{}
record, valid := cfg.ValidateAPIToken(rawToken)
if record != nil || valid {
t.Error("expected nil, false for empty config")
}
})
}
func TestUpsertAPIToken(t *testing.T) {
t.Run("insert new token", func(t *testing.T) {
cfg := &Config{}
record := APITokenRecord{
ID: "new-id",
Name: "New Token",
Hash: "new-hash",
CreatedAt: time.Now().UTC(),
}
cfg.UpsertAPIToken(record)
if len(cfg.APITokens) != 1 {
t.Fatalf("expected 1 token, got %d", len(cfg.APITokens))
}
if cfg.APITokens[0].ID != "new-id" {
t.Errorf("expected ID=new-id, got %q", cfg.APITokens[0].ID)
}
// Should have normalized scopes
if len(cfg.APITokens[0].Scopes) != 1 || cfg.APITokens[0].Scopes[0] != ScopeWildcard {
t.Errorf("expected wildcard scope, got %v", cfg.APITokens[0].Scopes)
}
})
t.Run("update existing token by ID", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "existing", Name: "Old Name", Hash: "old-hash", CreatedAt: time.Now().UTC()},
},
}
updated := APITokenRecord{
ID: "existing",
Name: "Updated Name",
Hash: "updated-hash",
CreatedAt: time.Now().UTC(),
Scopes: []string{ScopeMonitoringRead},
}
cfg.UpsertAPIToken(updated)
if len(cfg.APITokens) != 1 {
t.Fatalf("expected 1 token after update, got %d", len(cfg.APITokens))
}
if cfg.APITokens[0].Name != "Updated Name" {
t.Errorf("expected name to be updated, got %q", cfg.APITokens[0].Name)
}
if cfg.APITokens[0].Hash != "updated-hash" {
t.Errorf("expected hash to be updated, got %q", cfg.APITokens[0].Hash)
}
})
t.Run("sorting happens after upsert", func(t *testing.T) {
older := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
newer := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC)
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "older", Name: "Older", Hash: "hash1", CreatedAt: older},
},
}
cfg.UpsertAPIToken(APITokenRecord{
ID: "newer",
Name: "Newer",
Hash: "hash2",
CreatedAt: newer,
})
if len(cfg.APITokens) != 2 {
t.Fatalf("expected 2 tokens, got %d", len(cfg.APITokens))
}
// Newest should be first
if cfg.APITokens[0].ID != "newer" {
t.Errorf("expected newest token first, got %q", cfg.APITokens[0].ID)
}
if cfg.APITokens[1].ID != "older" {
t.Errorf("expected older token second, got %q", cfg.APITokens[1].ID)
}
})
}
func TestRemoveAPIToken(t *testing.T) {
t.Run("remove existing returns true", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "keep", Hash: "hash1"},
{ID: "remove", Hash: "hash2"},
{ID: "also-keep", Hash: "hash3"},
},
}
removed := cfg.RemoveAPIToken("remove")
if !removed {
t.Error("expected true when removing existing token")
}
if len(cfg.APITokens) != 2 {
t.Fatalf("expected 2 tokens remaining, got %d", len(cfg.APITokens))
}
// Verify the correct token was removed
for _, token := range cfg.APITokens {
if token.ID == "remove" {
t.Error("removed token should not be in list")
}
}
})
t.Run("remove non-existing returns false", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "existing", Hash: "hash1"},
},
}
removed := cfg.RemoveAPIToken("nonexistent")
if removed {
t.Error("expected false when removing nonexistent token")
}
if len(cfg.APITokens) != 1 {
t.Errorf("token count should be unchanged, got %d", len(cfg.APITokens))
}
})
t.Run("remove from empty returns false", func(t *testing.T) {
cfg := &Config{}
removed := cfg.RemoveAPIToken("any")
if removed {
t.Error("expected false when removing from empty config")
}
})
}
func TestSortAPITokens(t *testing.T) {
t.Run("sorts newest first", func(t *testing.T) {
oldest := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
middle := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC)
newest := time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC)
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "middle", Hash: "hash2", CreatedAt: middle},
{ID: "oldest", Hash: "hash1", CreatedAt: oldest},
{ID: "newest", Hash: "hash3", CreatedAt: newest},
},
}
cfg.SortAPITokens()
if cfg.APITokens[0].ID != "newest" {
t.Errorf("expected newest first, got %q", cfg.APITokens[0].ID)
}
if cfg.APITokens[1].ID != "middle" {
t.Errorf("expected middle second, got %q", cfg.APITokens[1].ID)
}
if cfg.APITokens[2].ID != "oldest" {
t.Errorf("expected oldest last, got %q", cfg.APITokens[2].ID)
}
})
t.Run("updates legacy APIToken field", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "primary-hash", CreatedAt: time.Now().UTC()},
},
}
cfg.SortAPITokens()
if cfg.APIToken != "primary-hash" {
t.Errorf("expected APIToken to be set to primary hash, got %q", cfg.APIToken)
}
if !cfg.APITokenEnabled {
t.Error("expected APITokenEnabled to be true")
}
})
t.Run("empty tokens clears APIToken", func(t *testing.T) {
cfg := &Config{
APIToken: "old-value",
APITokenEnabled: true,
APITokens: []APITokenRecord{},
}
cfg.SortAPITokens()
if cfg.APIToken != "" {
t.Errorf("expected APIToken to be cleared, got %q", cfg.APIToken)
}
})
t.Run("normalizes scopes for all tokens", func(t *testing.T) {
cfg := &Config{
APITokens: []APITokenRecord{
{ID: "1", Hash: "hash1", CreatedAt: time.Now().UTC(), Scopes: nil},
{ID: "2", Hash: "hash2", CreatedAt: time.Now().UTC(), Scopes: []string{}},
},
}
cfg.SortAPITokens()
for i, token := range cfg.APITokens {
if len(token.Scopes) != 1 || token.Scopes[0] != ScopeWildcard {
t.Errorf("token %d: expected wildcard scope, got %v", i, token.Scopes)
}
}
})
}