rdev/internal/auth/scopes_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

156 lines
4.1 KiB
Go

package auth
import (
"testing"
)
func TestScopeIsValid(t *testing.T) {
tests := []struct {
name string
scope Scope
want bool
}{
{"projects:read", ScopeProjectsRead, true},
{"projects:execute", ScopeProjectsExecute, true},
{"keys:read", ScopeKeysRead, true},
{"keys:write", ScopeKeysWrite, true},
{"admin", ScopeAdmin, true},
{"invalid", Scope("invalid"), false},
{"empty", Scope(""), false},
{"similar", Scope("projects:write"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.scope.IsValid(); got != tt.want {
t.Errorf("Scope(%q).IsValid() = %v, want %v", tt.scope, got, tt.want)
}
})
}
}
func TestScopesFromStrings(t *testing.T) {
input := []string{"projects:read", "keys:write"}
got := ScopesFromStrings(input)
if len(got) != 2 {
t.Fatalf("ScopesFromStrings() returned %d scopes, want 2", len(got))
}
if got[0] != ScopeProjectsRead {
t.Errorf("got[0] = %v, want %v", got[0], ScopeProjectsRead)
}
if got[1] != ScopeKeysWrite {
t.Errorf("got[1] = %v, want %v", got[1], ScopeKeysWrite)
}
}
func TestScopesToStrings(t *testing.T) {
input := []Scope{ScopeProjectsRead, ScopeKeysWrite}
got := ScopesToStrings(input)
if len(got) != 2 {
t.Fatalf("ScopesToStrings() returned %d strings, want 2", len(got))
}
if got[0] != "projects:read" {
t.Errorf("got[0] = %q, want %q", got[0], "projects:read")
}
if got[1] != "keys:write" {
t.Errorf("got[1] = %q, want %q", got[1], "keys:write")
}
}
func TestValidateScopes(t *testing.T) {
tests := []struct {
name string
scopes []Scope
want bool
}{
{"all valid", []Scope{ScopeProjectsRead, ScopeKeysWrite}, true},
{"single valid", []Scope{ScopeAdmin}, true},
{"empty", []Scope{}, true},
{"one invalid", []Scope{ScopeProjectsRead, Scope("invalid")}, false},
{"all invalid", []Scope{Scope("foo"), Scope("bar")}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ValidateScopes(tt.scopes); got != tt.want {
t.Errorf("ValidateScopes() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasScope(t *testing.T) {
tests := []struct {
name string
scopes []Scope
required Scope
want bool
}{
{"has exact scope", []Scope{ScopeProjectsRead, ScopeKeysRead}, ScopeProjectsRead, true},
{"admin grants all", []Scope{ScopeAdmin}, ScopeProjectsRead, true},
{"admin grants keys", []Scope{ScopeAdmin}, ScopeKeysWrite, true},
{"missing scope", []Scope{ScopeProjectsRead}, ScopeKeysWrite, false},
{"empty scopes", []Scope{}, ScopeProjectsRead, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasScope(tt.scopes, tt.required); got != tt.want {
t.Errorf("HasScope() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasAnyScope(t *testing.T) {
tests := []struct {
name string
scopes []Scope
required []Scope
want bool
}{
{"has first", []Scope{ScopeProjectsRead}, []Scope{ScopeProjectsRead, ScopeKeysRead}, true},
{"has second", []Scope{ScopeKeysRead}, []Scope{ScopeProjectsRead, ScopeKeysRead}, true},
{"has neither", []Scope{ScopeKeysWrite}, []Scope{ScopeProjectsRead, ScopeKeysRead}, false},
{"admin grants any", []Scope{ScopeAdmin}, []Scope{ScopeProjectsRead, ScopeKeysRead}, true},
{"empty required", []Scope{ScopeProjectsRead}, []Scope{}, false},
{"empty scopes", []Scope{}, []Scope{ScopeProjectsRead}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasAnyScope(tt.scopes, tt.required...); got != tt.want {
t.Errorf("HasAnyScope() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasProjectAccess(t *testing.T) {
tests := []struct {
name string
allowed []string
project string
want bool
}{
{"nil allows all", nil, "any-project", true},
{"in list", []string{"proj-a", "proj-b"}, "proj-a", true},
{"not in list", []string{"proj-a", "proj-b"}, "proj-c", false},
{"empty list denies", []string{}, "proj-a", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasProjectAccess(tt.allowed, tt.project); got != tt.want {
t.Errorf("HasProjectAccess() = %v, want %v", got, tt.want)
}
})
}
}