rdev/internal/cmdlimit/cmdlimit_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

415 lines
9.9 KiB
Go

package cmdlimit
import (
"context"
"sync"
"testing"
"time"
)
func TestNew(t *testing.T) {
t.Run("default config", func(t *testing.T) {
l := New(Config{})
if l.cfg.MaxConcurrentPerProject != 5 {
t.Errorf("MaxConcurrentPerProject = %d, want 5", l.cfg.MaxConcurrentPerProject)
}
if l.cfg.MaxConcurrentTotal != 20 {
t.Errorf("MaxConcurrentTotal = %d, want 20", l.cfg.MaxConcurrentTotal)
}
if l.cfg.CommandTimeout != 30*time.Minute {
t.Errorf("CommandTimeout = %v, want 30m", l.cfg.CommandTimeout)
}
})
t.Run("custom config", func(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 10,
MaxConcurrentTotal: 50,
CommandTimeout: time.Hour,
})
if l.cfg.MaxConcurrentPerProject != 10 {
t.Errorf("MaxConcurrentPerProject = %d, want 10", l.cfg.MaxConcurrentPerProject)
}
if l.cfg.MaxConcurrentTotal != 50 {
t.Errorf("MaxConcurrentTotal = %d, want 50", l.cfg.MaxConcurrentTotal)
}
})
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
if cfg.MaxConcurrentPerProject != 5 {
t.Errorf("MaxConcurrentPerProject = %d, want 5", cfg.MaxConcurrentPerProject)
}
if cfg.MaxConcurrentTotal != 20 {
t.Errorf("MaxConcurrentTotal = %d, want 20", cfg.MaxConcurrentTotal)
}
if cfg.CommandTimeout != 30*time.Minute {
t.Errorf("CommandTimeout = %v, want 30m", cfg.CommandTimeout)
}
}
func TestAcquire(t *testing.T) {
t.Run("allows commands within limit", func(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 3,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
// Should allow 3 commands for project-a
releases := make([]func(), 0)
for i := 0; i < 3; i++ {
release, err := l.Acquire(ctx, "project-a", "cmd-"+string(rune('a'+i)))
if err != nil {
t.Fatalf("Acquire %d failed: %v", i, err)
}
releases = append(releases, release)
}
// Clean up
for _, r := range releases {
r()
}
})
t.Run("rejects when per-project limit reached", func(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 2,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
// Acquire 2 slots
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
defer release1()
release2, _ := l.Acquire(ctx, "project-a", "cmd-2")
defer release2()
// Third should fail
_, err := l.Acquire(ctx, "project-a", "cmd-3")
if err != ErrLimitExceeded {
t.Errorf("Acquire() error = %v, want ErrLimitExceeded", err)
}
})
t.Run("allows other projects when one is at limit", func(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 1,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
// Fill project-a
releaseA, _ := l.Acquire(ctx, "project-a", "cmd-a")
defer releaseA()
// project-b should still work
releaseB, err := l.Acquire(ctx, "project-b", "cmd-b")
if err != nil {
t.Errorf("Acquire(project-b) error = %v, want nil", err)
}
defer releaseB()
})
t.Run("rejects when total limit reached", func(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 5,
MaxConcurrentTotal: 3,
CommandTimeout: time.Minute,
})
ctx := context.Background()
// Fill total limit across different projects
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
defer release1()
release2, _ := l.Acquire(ctx, "project-b", "cmd-2")
defer release2()
release3, _ := l.Acquire(ctx, "project-c", "cmd-3")
defer release3()
// Fourth should fail even for new project
_, err := l.Acquire(ctx, "project-d", "cmd-4")
if err != ErrLimitExceeded {
t.Errorf("Acquire() error = %v, want ErrLimitExceeded", err)
}
})
t.Run("release allows new commands", func(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 1,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
// Acquire and release
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
release1()
// Should be able to acquire again
release2, err := l.Acquire(ctx, "project-a", "cmd-2")
if err != nil {
t.Errorf("Acquire() after release error = %v, want nil", err)
}
release2()
})
}
func TestStats(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 5,
MaxConcurrentTotal: 20,
CommandTimeout: time.Minute,
})
ctx := context.Background()
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
release2, _ := l.Acquire(ctx, "project-a", "cmd-2")
release3, _ := l.Acquire(ctx, "project-b", "cmd-3")
stats := l.Stats()
if stats.TotalActive != 3 {
t.Errorf("TotalActive = %d, want 3", stats.TotalActive)
}
if stats.MaxTotal != 20 {
t.Errorf("MaxTotal = %d, want 20", stats.MaxTotal)
}
if stats.ProjectCounts["project-a"] != 2 {
t.Errorf("ProjectCounts[project-a] = %d, want 2", stats.ProjectCounts["project-a"])
}
if stats.ProjectCounts["project-b"] != 1 {
t.Errorf("ProjectCounts[project-b] = %d, want 1", stats.ProjectCounts["project-b"])
}
if len(stats.ActiveCommandIDs) != 3 {
t.Errorf("ActiveCommandIDs length = %d, want 3", len(stats.ActiveCommandIDs))
}
release1()
release2()
release3()
}
func TestIsProjectAtLimit(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 2,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
if l.IsProjectAtLimit("project-a") {
t.Error("IsProjectAtLimit should be false initially")
}
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
release2, _ := l.Acquire(ctx, "project-a", "cmd-2")
if !l.IsProjectAtLimit("project-a") {
t.Error("IsProjectAtLimit should be true after reaching limit")
}
release1()
release2()
}
func TestIsTotalAtLimit(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 5,
MaxConcurrentTotal: 2,
CommandTimeout: time.Minute,
})
ctx := context.Background()
if l.IsTotalAtLimit() {
t.Error("IsTotalAtLimit should be false initially")
}
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
release2, _ := l.Acquire(ctx, "project-b", "cmd-2")
if !l.IsTotalAtLimit() {
t.Error("IsTotalAtLimit should be true after reaching limit")
}
release1()
release2()
}
func TestActiveCount(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 5,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
if l.ActiveCount("project-a") != 0 {
t.Error("ActiveCount should be 0 initially")
}
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
if l.ActiveCount("project-a") != 1 {
t.Errorf("ActiveCount = %d, want 1", l.ActiveCount("project-a"))
}
release1()
if l.ActiveCount("project-a") != 0 {
t.Errorf("ActiveCount after release = %d, want 0", l.ActiveCount("project-a"))
}
}
func TestTotalActiveCount(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 5,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
if l.TotalActiveCount() != 0 {
t.Error("TotalActiveCount should be 0 initially")
}
release1, _ := l.Acquire(ctx, "project-a", "cmd-1")
release2, _ := l.Acquire(ctx, "project-b", "cmd-2")
if l.TotalActiveCount() != 2 {
t.Errorf("TotalActiveCount = %d, want 2", l.TotalActiveCount())
}
release1()
release2()
if l.TotalActiveCount() != 0 {
t.Errorf("TotalActiveCount after release = %d, want 0", l.TotalActiveCount())
}
}
func TestConcurrentAccess(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 10,
MaxConcurrentTotal: 100,
CommandTimeout: time.Minute,
})
ctx := context.Background()
var wg sync.WaitGroup
var successCount, failCount int64
var mu sync.Mutex
// Spawn many goroutines trying to acquire
for i := 0; i < 150; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
release, err := l.Acquire(ctx, "project-a", "cmd-"+string(rune(idx)))
mu.Lock()
if err == nil {
successCount++
mu.Unlock()
time.Sleep(10 * time.Millisecond)
release()
} else {
failCount++
mu.Unlock()
}
}(i)
}
wg.Wait()
// Should have had some successes and some failures
if successCount == 0 {
t.Error("Expected some successful acquires")
}
if failCount == 0 {
t.Error("Expected some failed acquires (limit exceeded)")
}
}
func TestAutoReleaseOnTimeout(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 1,
MaxConcurrentTotal: 10,
CommandTimeout: 100 * time.Millisecond,
})
ctx := context.Background()
// Acquire a slot
_, err := l.Acquire(ctx, "project-a", "cmd-1")
if err != nil {
t.Fatalf("Acquire failed: %v", err)
}
// Should be at limit
if !l.IsProjectAtLimit("project-a") {
t.Error("Should be at limit after acquire")
}
// Wait for timeout
time.Sleep(150 * time.Millisecond)
// Should be auto-released
if l.IsProjectAtLimit("project-a") {
t.Error("Should not be at limit after auto-release")
}
}
func TestDoubleRelease(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 5,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx := context.Background()
release, _ := l.Acquire(ctx, "project-a", "cmd-1")
// Release twice - should not panic or cause issues
release()
release()
// Count should be 0, not negative
if l.ActiveCount("project-a") != 0 {
t.Errorf("ActiveCount after double release = %d, want 0", l.ActiveCount("project-a"))
}
}
func TestContextCancellation(t *testing.T) {
l := New(Config{
MaxConcurrentPerProject: 1,
MaxConcurrentTotal: 10,
CommandTimeout: time.Minute,
})
ctx, cancel := context.WithCancel(context.Background())
// Acquire a slot
_, err := l.Acquire(ctx, "project-a", "cmd-1")
if err != nil {
t.Fatalf("Acquire failed: %v", err)
}
// Cancel the context
cancel()
// Give goroutine time to process
time.Sleep(50 * time.Millisecond)
// Should be auto-released due to context cancellation
if l.IsProjectAtLimit("project-a") {
t.Error("Should be released after context cancellation")
}
}