Files
pmg/cmd/sandbox/profile_list_test.go
T
b59c3358e8 fix(sandbox): classify helper-tool errors with usefulerror (#272)
* fix(sandbox): classify helper-tool errors with usefulerror

Sandbox helper commands (profile lint/diff/show/init/list) used to bubble
up plain fmt.Errorf chains from the registry layer, which the TUI then
classified as Unknown and decorated with a bug-report link. Wrap each
error path at the cmd/sandbox boundary so the TUI prints NotFound,
InvalidArgument, or PermissionDenied with actionable hints instead.

Closes #269

* refactor(sandbox): classify registry errors via sentinel wrapping

Replace the fragile substring match in profileLoadError with errors.Is
against new sandbox.ErrProfileNotFound / sandbox.ErrProfileInvalid
sentinels. Every fmt.Errorf in registry.go that previously communicated
"missing" or "malformed" by message text now wraps the corresponding
sentinel, so the cmd layer can classify without inspecting strings.

* fix(sandbox): detect IO error class when wrapping helper errors

Replace static ErrCodeUnknown / ErrCodePermissionDenied wrappings with
ioErrorCode, which inspects the error chain for fs.ErrPermission and
fs.ErrNotExist before falling back. Applied to runProfileList (where an
unreadable user profile directory now classifies as PermissionDenied),
registryInitError, and the stat/MkdirAll/WriteFile paths in profile init.

Also drop redundant doc comments on helpers whose names are self-evident.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 19:34:29 +05:30

158 lines
3.9 KiB
Go

package sandbox
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
)
func newTestRegistry(t *testing.T, userDir string) registryFactory {
t.Helper()
return func() (pmgsandbox.ProfileRegistry, error) {
opts := []pmgsandbox.RegistryOption{}
if userDir != "" {
opts = append(opts, pmgsandbox.WithUserProfileDir(userDir))
}
return pmgsandbox.NewProfileRegistry(opts...)
}
}
func writeTestUserProfile(t *testing.T, dir, name string) {
t.Helper()
body := "name: " + name + `
description: user ` + name + `
package_managers:
- npm
filesystem:
allow_read:
- /tmp
allow_write:
- /tmp
deny_read: []
deny_write: []
`
require.NoError(t, os.WriteFile(filepath.Join(dir, name+".yml"), []byte(body), 0o644))
}
func TestProfileListHuman(t *testing.T) {
cmd := newProfileListCommand(newTestRegistry(t, ""))
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{})
require.NoError(t, cmd.Execute())
out := stdout.String()
assert.Contains(t, out, "Sandbox Profiles")
assert.Contains(t, out, "npm-restrictive")
assert.Contains(t, out, "builtin")
}
func TestProfileListJSON(t *testing.T) {
dir := t.TempDir()
writeTestUserProfile(t, dir, "my-custom")
cmd := newProfileListCommand(newTestRegistry(t, dir))
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"--json"})
require.NoError(t, cmd.Execute())
var report jsonProfileListReport
require.NoError(t, json.Unmarshal(stdout.Bytes(), &report))
require.NotEmpty(t, report.Profiles)
var foundBuiltin, foundUser bool
for _, p := range report.Profiles {
if p.Source == "builtin" {
foundBuiltin = true
}
if p.Source == "user" && p.Name == "my-custom" {
foundUser = true
assert.NotEmpty(t, p.Path)
}
}
assert.True(t, foundBuiltin)
assert.True(t, foundUser)
}
func TestProfileListShadowedTag(t *testing.T) {
dir := t.TempDir()
writeTestUserProfile(t, dir, "npm-restrictive")
cmd := newProfileListCommand(newTestRegistry(t, dir))
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{})
require.NoError(t, cmd.Execute())
assert.Contains(t, stdout.String(), "SHADOWED")
}
func TestProfileListRegistryFailureReturnsUseful(t *testing.T) {
factory := func() (pmgsandbox.ProfileRegistry, error) {
return nil, errors.New("boom")
}
cmd := newProfileListCommand(factory)
var stdout, stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{})
err := cmd.Execute()
require.Error(t, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeUnknown, usefulErr.Code())
assert.Contains(t, err.Error(), "boom")
}
func TestProfileListRegistryPermissionErrorReturnsPermissionDenied(t *testing.T) {
factory := func() (pmgsandbox.ProfileRegistry, error) {
return nil, fmt.Errorf("read dir: %w", fs.ErrPermission)
}
cmd := newProfileListCommand(factory)
var stdout, stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{})
err := cmd.Execute()
require.Error(t, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodePermissionDenied, usefulErr.Code())
}
func TestProfileListRejectsUnexpectedArgs(t *testing.T) {
cmd := newProfileListCommand(newTestRegistry(t, ""))
var stdout, stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs([]string{"extra"})
err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, stderr.String(), "unknown command")
assert.Contains(t, stdout.String(), "Usage:")
assert.Contains(t, stdout.String(), "list [flags]")
assert.Contains(t, stdout.String(), "pmg sandbox profile list")
}