package main import ( "bytes" "os" "os/exec" "path/filepath" "strings" "testing" "tangled.org/core/spindle/quota" ) func TestCLIInvalidLogFormat(t *testing.T) { cmd := exec.Command(os.Args[0], "-test.run=TestHelperProcess") cmd.Env = append(os.Environ(), "SPINDLE_LOG_FORMAT=invalid", "WANT_HELPER_PROCESS=1") var stderr bytes.Buffer cmd.Stderr = &stderr err := cmd.Run() if err == nil { t.Fatalf("expected command to fail, but it succeeded") } exitErr, ok := err.(*exec.ExitError) if !ok { t.Fatalf("expected exit error, got %v", err) } if code := exitErr.ExitCode(); code != 1 { t.Errorf("expected exit code 1, got %d", code) } output := stderr.String() if !strings.Contains(output, "invalid log format") { t.Errorf("expected error message to contain 'invalid log format', got: %q", output) } } func TestHelperProcess(t *testing.T) { if os.Getenv("WANT_HELPER_PROCESS") != "1" { return } for i, arg := range os.Args { if arg == "--" { os.Args = append([]string{os.Args[0]}, os.Args[i+1:]...) main() return } } main() } func TestCLIQuotaCommands(t *testing.T) { tmpDb := filepath.Join(t.TempDir(), "spindle.db") tests := []struct { name string args []string wantExitCode int wantError string wantOutput string }{ { name: "invalid DID", args: []string{"quota", "set", "--did", "alice", "--resource", "cache_storage_bytes", "--limit", "10"}, wantExitCode: 255, wantError: "invalid did", }, { name: "invalid resource", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", strings.Repeat("a", 65), "--limit", "10"}, wantExitCode: 255, wantError: "invalid resource", }, { name: "conflicting flags limit and unlimited", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "cache_storage_bytes", "--limit", "10", "--unlimited"}, wantExitCode: 255, wantError: "cannot specify both --limit and --unlimited", }, { name: "missing flags limit and unlimited", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "cache_storage_bytes"}, wantExitCode: 255, wantError: "must specify either --limit or --unlimited", }, { name: "cache storage limit overflow", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "cache_storage_bytes", "--limit", "9223372036854775807"}, wantExitCode: 255, wantError: "would overflow bytes", }, { name: "set user cache_storage_bytes limit", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "cache_storage_bytes", "--limit", "10"}, wantExitCode: 0, wantOutput: "quota override set", }, { name: "set user workflows limit", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "workflows", "--limit", "5"}, wantExitCode: 0, wantOutput: "quota override set", }, { name: "set user vcpus limit", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "vcpus", "--limit", "4"}, wantExitCode: 0, wantOutput: "quota override set", }, { name: "set user memory_mib limit", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "memory_mib", "--limit", "2048"}, wantExitCode: 0, wantOutput: "quota override set", }, { name: "set user disk_mib limit", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "disk_mib", "--limit", "4096"}, wantExitCode: 0, wantOutput: "quota override set", }, { name: "set user cache_storage_bytes unlimited", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "cache_storage_bytes", "--unlimited"}, wantExitCode: 0, wantOutput: "quota override set", }, { name: "list quota limits", args: []string{"quota", "list"}, wantExitCode: 0, wantOutput: "LIMIT", }, { name: "usage list", args: []string{"quota", "usage"}, wantExitCode: 0, wantOutput: "LIMIT", }, { name: "unset user cache_storage_bytes limit", args: []string{"quota", "unset", "--did", "did:web:alice", "--resource", "cache_storage_bytes"}, wantExitCode: 0, wantOutput: "quota override removed", }, { name: "unset user workflows limit", args: []string{"quota", "unset", "--did", "did:web:alice", "--resource", "workflows"}, wantExitCode: 0, wantOutput: "quota override removed", }, { name: "set user generic resource limit", args: []string{"quota", "set", "--did", "did:web:alice", "--resource", "custom_gpu", "--limit", "8"}, wantExitCode: 0, wantOutput: "quota override set", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fullArgs := append([]string{"-test.run=TestHelperProcess", "--"}, tt.args...) fullArgs = append(fullArgs, "--db", tmpDb) cmd := exec.Command(os.Args[0], fullArgs...) cmd.Env = append(os.Environ(), "WANT_HELPER_PROCESS=1", "SPINDLE_SERVER_HOSTNAME=spindle.example.com", "SPINDLE_SERVER_OWNER=did:web:spindle.example.com", ) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() exitCode := 0 if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { exitCode = exitErr.ExitCode() } else { t.Fatalf("unexpected run error: %v", err) } } expectedCode := tt.wantExitCode if expectedCode == -1 { expectedCode = 255 } if exitCode != expectedCode { t.Errorf("exitCode = %d, want %d. Stderr: %q, Stdout: %q", exitCode, expectedCode, stderr.String(), stdout.String()) } combined := stdout.String() + "\n" + stderr.String() if tt.wantError != "" && !strings.Contains(combined, tt.wantError) { t.Errorf("expected error/output to contain %q, got combined: %q", tt.wantError, combined) } if tt.wantOutput != "" && !strings.Contains(combined, tt.wantOutput) { t.Errorf("expected output to contain %q, got combined: %q", tt.wantOutput, combined) } }) } } func TestNormalizeDefaultLimit(t *testing.T) { if got := normalizeDefaultLimit(0); got != -1 { t.Fatalf("zero configured limit = %d, want -1", got) } if got := normalizeDefaultLimit(2); got != 2 { t.Fatalf("finite configured limit = %d, want 2", got) } if got := formatLimit(quota.ResourceWorkflows, 0); got != "0 workflows" { t.Fatalf("zero override = %q, want 0 workflows", got) } }