rdev/internal/auth/keys_test.go
jordan 538ea57ed4 feat: Add claude-config API, security hardening, and testing infrastructure
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>
2026-01-25 01:29:13 -07:00

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)
}
})
}
}