Claude Config API (v0.6): - Add CRUD endpoints for commands, skills, and agents - Commands/skills/agents stored in /workspace/.claude/ (per-project, in git) - Credentials shared via PVC at /root/.claude/ (shared across pods) - Use base64 encoding for file writes (prevents shell injection) - Add content size limits (1MB max) Security Hardening: - Add sanitize package for command/prompt validation - Add rate limiting middleware (token bucket algorithm) - Add concurrent command limiting - Add input sanitization to all command handlers - Gitignore secrets.yaml and credentials.yaml - Add *.example templates for secrets Testing Infrastructure: - Add testutil package with mocks and fixtures - Add unit tests for auth package (63% coverage) - Add unit tests for executor (47% coverage) - Add handler integration tests (40% coverage) - Add 100% coverage for sanitize, cmdlimit packages - Add 96% coverage for ratelimit package Infrastructure: - Shared Claude credentials PVC (ReadWriteMany) - Reduced workspace PVC size from 20Gi to 5Gi - Add init container cleanup before git clone - Document Longhorn RWX requirements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
5.3 KiB
Go
204 lines
5.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseExpiration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want time.Duration
|
|
wantErr bool
|
|
}{
|
|
{"30d", "30d", Expiration30Days, false},
|
|
{"30", "30", Expiration30Days, false},
|
|
{"60d", "60d", Expiration60Days, false},
|
|
{"60", "60", Expiration60Days, false},
|
|
{"90d", "90d", Expiration90Days, false},
|
|
{"90", "90", Expiration90Days, false},
|
|
{"1y", "1y", Expiration1Year, false},
|
|
{"1year", "1year", Expiration1Year, false},
|
|
{"365d", "365d", Expiration1Year, false},
|
|
{"never", "never", ExpirationNoLimit, false},
|
|
{"none", "none", ExpirationNoLimit, false},
|
|
{"empty", "", ExpirationNoLimit, false},
|
|
{"case insensitive", "30D", Expiration30Days, false},
|
|
{"invalid", "invalid", 0, true},
|
|
{"7d", "7d", 0, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := ParseExpiration(tt.input)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParseExpiration(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
|
return
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("ParseExpiration(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpiresAt(t *testing.T) {
|
|
t.Run("returns nil for zero duration", func(t *testing.T) {
|
|
got := ExpiresAt(0)
|
|
if got != nil {
|
|
t.Errorf("ExpiresAt(0) = %v, want nil", got)
|
|
}
|
|
})
|
|
|
|
t.Run("returns future time for positive duration", func(t *testing.T) {
|
|
before := time.Now()
|
|
got := ExpiresAt(24 * time.Hour)
|
|
after := time.Now()
|
|
|
|
if got == nil {
|
|
t.Fatal("ExpiresAt(24h) returned nil")
|
|
}
|
|
|
|
expectedMin := before.Add(24 * time.Hour)
|
|
expectedMax := after.Add(24 * time.Hour)
|
|
|
|
if got.Before(expectedMin) || got.After(expectedMax) {
|
|
t.Errorf("ExpiresAt(24h) = %v, want between %v and %v", *got, expectedMin, expectedMax)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGenerateKey(t *testing.T) {
|
|
t.Run("generates unique keys", func(t *testing.T) {
|
|
key1, id1, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey() error = %v", err)
|
|
}
|
|
|
|
key2, id2, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey() error = %v", err)
|
|
}
|
|
|
|
if key1 == key2 {
|
|
t.Error("Generated keys should be unique")
|
|
}
|
|
|
|
if id1 == id2 {
|
|
t.Error("Generated identifiers should be unique")
|
|
}
|
|
})
|
|
|
|
t.Run("key has correct format", func(t *testing.T) {
|
|
key, id, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey() error = %v", err)
|
|
}
|
|
|
|
if !ValidateKeyFormat(key) {
|
|
t.Errorf("Generated key %q has invalid format", key)
|
|
}
|
|
|
|
if len(id) != KeyIdentifierLength {
|
|
t.Errorf("Identifier length = %d, want %d", len(id), KeyIdentifierLength)
|
|
}
|
|
})
|
|
|
|
t.Run("key contains prefix and identifier", func(t *testing.T) {
|
|
key, id, err := GenerateKey()
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey() error = %v", err)
|
|
}
|
|
|
|
expectedPrefix := KeyPrefix + id + "_"
|
|
if key[:len(expectedPrefix)] != expectedPrefix {
|
|
t.Errorf("Key %q should start with %q", key, expectedPrefix)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHashKey(t *testing.T) {
|
|
t.Run("produces consistent hash", func(t *testing.T) {
|
|
key := "rdev_sk_abc12345_0123456789abcdef0123456789abcdef"
|
|
hash1 := HashKey(key)
|
|
hash2 := HashKey(key)
|
|
|
|
if hash1 != hash2 {
|
|
t.Error("Same key should produce same hash")
|
|
}
|
|
})
|
|
|
|
t.Run("different keys produce different hashes", func(t *testing.T) {
|
|
hash1 := HashKey("key1")
|
|
hash2 := HashKey("key2")
|
|
|
|
if hash1 == hash2 {
|
|
t.Error("Different keys should produce different hashes")
|
|
}
|
|
})
|
|
|
|
t.Run("hash is hex encoded", func(t *testing.T) {
|
|
hash := HashKey("test")
|
|
// SHA-256 produces 32 bytes = 64 hex characters
|
|
if len(hash) != 64 {
|
|
t.Errorf("Hash length = %d, want 64", len(hash))
|
|
}
|
|
|
|
for _, c := range hash {
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
|
t.Errorf("Hash contains non-hex character: %c", c)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateKeyFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
want bool
|
|
}{
|
|
{"valid key", "rdev_sk_abc12345_0123456789abcdef0123456789abcdef", true},
|
|
{"missing prefix", "abc12345_0123456789abcdef0123456789abcdef", false},
|
|
{"wrong prefix", "api_sk_abc12345_0123456789abcdef0123456789abcdef", false},
|
|
{"short identifier", "rdev_sk_abc1234_0123456789abcdef0123456789abcdef", false},
|
|
{"long identifier", "rdev_sk_abc123456_0123456789abcdef0123456789abcdef", false},
|
|
{"short random", "rdev_sk_abc12345_0123456789abcdef", false},
|
|
{"long random", "rdev_sk_abc12345_0123456789abcdef0123456789abcdef00", false},
|
|
{"missing underscore", "rdev_sk_abc123450123456789abcdef0123456789abcdef", false},
|
|
{"extra underscore", "rdev_sk_abc12345_0123_456789abcdef0123456789abcdef", false},
|
|
{"empty", "", false},
|
|
{"only prefix", "rdev_sk_", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := ValidateKeyFormat(tt.key); got != tt.want {
|
|
t.Errorf("ValidateKeyFormat(%q) = %v, want %v", tt.key, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
want string
|
|
}{
|
|
{"valid key", "rdev_sk_abc12345_0123456789abcdef0123456789abcdef", "abc12345"},
|
|
{"missing prefix", "abc12345_random", ""},
|
|
{"only rdev_sk_", "rdev_sk_", ""},
|
|
{"empty", "", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := ExtractPrefix(tt.key); got != tt.want {
|
|
t.Errorf("ExtractPrefix(%q) = %q, want %q", tt.key, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|