package sandbox import ( "bytes" "encoding/json" "strings" "testing" "github.com/safedep/dry/usefulerror" "github.com/safedep/pmg/errcodes" pmgsandbox "github.com/safedep/pmg/sandbox" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestCacheFactory(t *testing.T) (cacheFactory, *pmgsandbox.ViolationCache) { t.Helper() dir := t.TempDir() cache := pmgsandbox.NewViolationCache(dir) return func() *pmgsandbox.ViolationCache { return cache }, cache } func sampleViolationsReport(target string) *pmgsandbox.ViolationReport { return &pmgsandbox.ViolationReport{ SandboxName: pmgsandbox.DriverSeatbelt, PolicyName: "npm-restrictive", Violations: []pmgsandbox.Violation{ {Kind: pmgsandbox.ViolationKindFSWrite, Target: target, RuleLabel: "file-write"}, }, } } func runList(t *testing.T, factory cacheFactory, args ...string) (string, string, error) { t.Helper() cmd := newViolationsListCommand(factory) var out, errOut bytes.Buffer cmd.SetOut(&out) cmd.SetErr(&errOut) cmd.SetArgs(args) err := cmd.Execute() return out.String(), errOut.String(), err } func TestViolationsList_EmptyCache(t *testing.T) { factory, _ := newTestCacheFactory(t) stdout, stderr, err := runList(t, factory) require.NoError(t, err) assert.Empty(t, stdout) assert.Contains(t, stderr, "no violations cached") } func TestViolationsList_TwoEntriesNewestFirst(t *testing.T) { factory, cache := newTestCacheFactory(t) _, err := cache.Write(sampleViolationsReport("/Users/dev/older")) require.NoError(t, err) _, err = cache.Write(sampleViolationsReport("/Users/dev/newer")) require.NoError(t, err) stdout, _, err := runList(t, factory) require.NoError(t, err) assert.Contains(t, stdout, "RECORDED") assert.Contains(t, stdout, "SANDBOX") assert.Contains(t, stdout, "seatbelt") assert.Contains(t, stdout, "npm-restrictive") assert.Contains(t, stdout, "/Users/dev/older") assert.Contains(t, stdout, "/Users/dev/newer") newerIdx := strings.Index(stdout, "/Users/dev/newer") olderIdx := strings.Index(stdout, "/Users/dev/older") require.NotEqual(t, -1, newerIdx) require.NotEqual(t, -1, olderIdx) assert.Less(t, newerIdx, olderIdx, "newest entry should appear first") } func TestViolationsList_LimitOne(t *testing.T) { factory, cache := newTestCacheFactory(t) _, err := cache.Write(sampleViolationsReport("/Users/dev/a")) require.NoError(t, err) _, err = cache.Write(sampleViolationsReport("/Users/dev/b")) require.NoError(t, err) stdout, _, err := runList(t, factory, "--limit", "1") require.NoError(t, err) assert.Contains(t, stdout, "/Users/dev/b") assert.NotContains(t, stdout, "/Users/dev/a") } func TestViolationsListRejectsUnexpectedArgs(t *testing.T) { factory, _ := newTestCacheFactory(t) stdout, stderr, err := runList(t, factory, "extra") require.Error(t, err) assert.Contains(t, stderr, "unknown command") assert.Contains(t, stdout, "Usage:") assert.Contains(t, stdout, "list [flags]") assert.Contains(t, stdout, "pmg sandbox violations list --limit 20") } func TestViolationsListRejectsNegativeLimit(t *testing.T) { factory, _ := newTestCacheFactory(t) _, _, err := runList(t, factory, "--limit", "-1") require.Error(t, err) assert.Contains(t, err.Error(), "invalid --limit") usefulErr, ok := usefulerror.AsUsefulError(err) require.True(t, ok) assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code()) } func TestViolationsList_LimitZeroReturnsAll(t *testing.T) { factory, cache := newTestCacheFactory(t) for _, target := range []string{"/Users/dev/a", "/Users/dev/b", "/Users/dev/c"} { _, err := cache.Write(sampleViolationsReport(target)) require.NoError(t, err) } stdout, _, err := runList(t, factory, "--limit", "0") require.NoError(t, err) assert.Contains(t, stdout, "/Users/dev/a") assert.Contains(t, stdout, "/Users/dev/b") assert.Contains(t, stdout, "/Users/dev/c") } func TestViolationsList_JSON(t *testing.T) { factory, cache := newTestCacheFactory(t) _, err := cache.Write(sampleViolationsReport("/Users/dev/x")) require.NoError(t, err) _, err = cache.Write(sampleViolationsReport("/Users/dev/y")) require.NoError(t, err) stdout, _, err := runList(t, factory, "--json") require.NoError(t, err) var payload struct { Entries []struct { Path string `json:"path"` RecordedAt string `json:"recorded_at"` SandboxName string `json:"sandbox_name"` PolicyName string `json:"policy_name"` Primary *struct { Kind string `json:"kind"` Target string `json:"target"` RuleLabel string `json:"rule_label"` } `json:"primary,omitempty"` ViolationCount int `json:"violation_count"` } `json:"entries"` } require.NoError(t, json.Unmarshal([]byte(stdout), &payload)) require.Len(t, payload.Entries, 2) assert.Equal(t, "seatbelt", payload.Entries[0].SandboxName) assert.Equal(t, "npm-restrictive", payload.Entries[0].PolicyName) require.NotNil(t, payload.Entries[0].Primary) assert.Equal(t, "fs_write", payload.Entries[0].Primary.Kind) assert.Equal(t, 1, payload.Entries[0].ViolationCount) } func TestViolationsList_NoViolations_RendersDash(t *testing.T) { factory, cache := newTestCacheFactory(t) _, err := cache.Write(&pmgsandbox.ViolationReport{ SandboxName: pmgsandbox.DriverSeatbelt, PolicyName: "npm-restrictive", Violations: nil, }) require.NoError(t, err) stdout, _, err := runList(t, factory) require.NoError(t, err) assert.Contains(t, stdout, "seatbelt") assert.Contains(t, stdout, "—") } func TestViolationsList_NoViolations_JSONOmitsPrimary(t *testing.T) { factory, cache := newTestCacheFactory(t) _, err := cache.Write(&pmgsandbox.ViolationReport{ SandboxName: pmgsandbox.DriverSeatbelt, PolicyName: "npm-restrictive", Violations: nil, }) require.NoError(t, err) stdout, _, err := runList(t, factory, "--json") require.NoError(t, err) assert.NotContains(t, stdout, "\"primary\"") var payload map[string]any require.NoError(t, json.Unmarshal([]byte(stdout), &payload)) entries, ok := payload["entries"].([]any) require.True(t, ok) require.Len(t, entries, 1) }