diff --git a/cmd/sandbox/helpers.go b/cmd/sandbox/helpers.go index 029a81c..5865afe 100644 --- a/cmd/sandbox/helpers.go +++ b/cmd/sandbox/helpers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "io/fs" "regexp" "strings" @@ -62,6 +63,56 @@ func notFoundError(message, help string) error { Wrap(errors.New(message)) } +// Idempotent: returns err unchanged when nil or already useful, so call +// sites can apply it without losing more precise pre-classified errors. +func wrapUseful(err error, code, help string) error { + if err == nil { + return nil + } + if _, ok := usefulerror.AsUsefulError(err); ok { + return err + } + return usefulerror.Useful(). + WithCode(code). + WithHumanError(err.Error()). + WithHelp(help). + Wrap(err) +} + +func profileLoadError(err error) error { + if err == nil { + return nil + } + if _, ok := usefulerror.AsUsefulError(err); ok { + return err + } + switch { + case errors.Is(err, pmgsandbox.ErrProfileNotFound): + return wrapUseful(err, usefulerror.ErrCodeNotFound, + "Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.") + case errors.Is(err, pmgsandbox.ErrProfileInvalid): + return wrapUseful(err, usefulerror.ErrCodeInvalidArgument, + "Check the profile YAML for syntax/schema issues and verify any 'inherits:' parent name.") + } + return wrapUseful(err, usefulerror.ErrCodeUnknown, + "Failed to load the sandbox profile. Run with --verbose for the underlying cause.") +} + +func registryInitError(err error) error { + return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown), + "Failed to initialise the sandbox profile registry. Run with --verbose for details.") +} + +func ioErrorCode(err error, fallback string) string { + switch { + case errors.Is(err, fs.ErrPermission): + return usefulerror.ErrCodePermissionDenied + case errors.Is(err, fs.ErrNotExist): + return usefulerror.ErrCodeNotFound + } + return fallback +} + func writeJSONIndent(out io.Writer, v any) error { enc := json.NewEncoder(out) enc.SetIndent("", " ") diff --git a/cmd/sandbox/profile_diff.go b/cmd/sandbox/profile_diff.go index b3fce6b..c75b57c 100644 --- a/cmd/sandbox/profile_diff.go +++ b/cmd/sandbox/profile_diff.go @@ -9,6 +9,7 @@ import ( "github.com/pmezard/go-difflib/difflib" pmgsandbox "github.com/safedep/pmg/sandbox" "github.com/safedep/pmg/sandbox/platform" + "github.com/safedep/pmg/usefulerror" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -68,7 +69,7 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts * registry, err := factory() if err != nil { - return &diffOpError{err: err} + return &diffOpError{err: registryInitError(err)} } dataA, err := materialize(registry, nameA, opts) @@ -82,7 +83,8 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts * if bytes.Equal(dataA, dataB) { if _, err := fmt.Fprintln(errOut, "profiles are identical"); err != nil { - return &diffOpError{err: err} + return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown, + "Failed to write diff output. Check that stderr is writable.")} } return nil } @@ -96,16 +98,20 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts * } text, err := difflib.GetUnifiedDiffString(diff) if err != nil { - return &diffOpError{err: fmt.Errorf("failed to render diff: %w", err)} + return &diffOpError{err: wrapUseful(fmt.Errorf("failed to render diff: %w", err), + usefulerror.ErrCodeUnknown, + "Could not render the unified diff. Re-run with --verbose for the underlying cause.")} } if _, err := io.WriteString(out, text); err != nil { - return &diffOpError{err: err} + return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown, + "Failed to write diff output. Check that stdout is writable.")} } if !strings.HasSuffix(text, "\n") { if _, err := fmt.Fprintln(out); err != nil { - return &diffOpError{err: err} + return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown, + "Failed to write diff output. Check that stdout is writable.")} } } @@ -118,11 +124,16 @@ func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profile Home: opts.home, }) if err != nil { - return nil, err + return nil, profileLoadError(err) } if opts.driver != "" { - return platform.Render(pmgsandbox.DriverName(opts.driver), policy) + rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy) + if err != nil { + return nil, wrapUseful(err, usefulerror.ErrCodeInvalidArgument, + "Could not render the policy for the requested driver. Verify the driver is supported on this host.") + } + return rendered, nil } // Strip inherits since both sides are post-resolution. @@ -130,7 +141,9 @@ func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profile data, err := yaml.Marshal(policy) if err != nil { - return nil, fmt.Errorf("failed to marshal resolved policy %s: %w", name, err) + return nil, wrapUseful(fmt.Errorf("failed to marshal resolved policy %s: %w", name, err), + usefulerror.ErrCodeUnknown, + "Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.") } return data, nil } diff --git a/cmd/sandbox/profile_diff_test.go b/cmd/sandbox/profile_diff_test.go index aef80ce..3d6f93a 100644 --- a/cmd/sandbox/profile_diff_test.go +++ b/cmd/sandbox/profile_diff_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/safedep/pmg/usefulerror" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -96,6 +97,9 @@ func TestProfileDiffUnknownProfile(t *testing.T) { assert.Empty(t, stdout) assert.Empty(t, stderr) assert.Contains(t, err.Error(), "no-such-profile-xyz") + usefulErr, ok := usefulerror.AsUsefulError(err) + require.True(t, ok, "diff error should expose a UsefulError through Unwrap") + assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code()) } func TestProfileDiffUnknownDriver(t *testing.T) { @@ -107,6 +111,9 @@ func TestProfileDiffUnknownDriver(t *testing.T) { assert.Empty(t, stdout) assert.Empty(t, stderr) assert.Contains(t, err.Error(), "unknown driver") + usefulErr, ok := usefulerror.AsUsefulError(err) + require.True(t, ok, "diff error should expose a UsefulError through Unwrap") + assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code()) } func TestProfileDiffMissingProfileShowsUsage(t *testing.T) { diff --git a/cmd/sandbox/profile_init.go b/cmd/sandbox/profile_init.go index 3342cec..1e7f6e8 100644 --- a/cmd/sandbox/profile_init.go +++ b/cmd/sandbox/profile_init.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "github.com/safedep/pmg/usefulerror" "github.com/spf13/cobra" ) @@ -59,7 +60,7 @@ func runProfileInit(out io.Writer, name string, opts *profileInitOptions, factor registry, err := factory() if err != nil { - return err + return registryInitError(err) } userDir := registry.UserProfileDir() @@ -93,17 +94,23 @@ func runProfileInit(out io.Writer, name string, opts *profileInitOptions, factor "Choose a different profile name, or edit the existing profile file to merge changes.", ) } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to stat %s: %w", target, err) + return wrapUseful(fmt.Errorf("failed to stat %s: %w", target, err), + ioErrorCode(err, usefulerror.ErrCodeUnknown), + "Could not stat the target profile path. Check the user profile directory permissions.") } if err := os.MkdirAll(userDir, 0o755); err != nil { - return fmt.Errorf("failed to create user profile directory %s: %w", userDir, err) + return wrapUseful(fmt.Errorf("failed to create user profile directory %s: %w", userDir, err), + ioErrorCode(err, usefulerror.ErrCodePermissionDenied), + "Could not create the user profile directory. Check filesystem permissions for "+userDir+".") } content := renderScaffold(name, opts.description, opts.from, pms, placeholderPM) if err := os.WriteFile(target, []byte(content), 0o644); err != nil { - return fmt.Errorf("failed to write %s: %w", target, err) + return wrapUseful(fmt.Errorf("failed to write %s: %w", target, err), + ioErrorCode(err, usefulerror.ErrCodePermissionDenied), + "Could not write the scaffolded profile. Check filesystem permissions for "+target+".") } _, err = fmt.Fprintln(out, target) diff --git a/cmd/sandbox/profile_lint.go b/cmd/sandbox/profile_lint.go index b362798..4ea07e1 100644 --- a/cmd/sandbox/profile_lint.go +++ b/cmd/sandbox/profile_lint.go @@ -56,12 +56,12 @@ func (e *lintFailError) ExitCode() int { return ExitCodeLintFail } func runProfileLint(out io.Writer, name string, opts *profileLintOptions, factory registryFactory) error { registry, err := factory() if err != nil { - return err + return registryInitError(err) } policy, resolvedName, err := resolveProfileForLint(name, registry) if err != nil { - return err + return profileLoadError(err) } issues := pmgsandbox.LintProfile(policy) @@ -89,7 +89,7 @@ func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*p if _, ok := registry.BuiltinProfileYAML(name); ok { policy, err := registry.GetProfile(name) if err != nil { - return nil, "", err + return nil, "", profileLoadError(err) } return policy, name, nil } @@ -102,7 +102,7 @@ func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*p if s.Source == pmgsandbox.ProfileSourceUser && s.Name == name { policy, loadErr := registry.LoadCustomProfile(s.Path) if loadErr != nil { - return nil, "", loadErr + return nil, "", profileLoadError(loadErr) } return policy, s.Path, nil } @@ -111,7 +111,7 @@ func resolveProfileForLint(name string, registry pmgsandbox.ProfileRegistry) (*p if _, statErr := os.Stat(name); statErr == nil { policy, loadErr := registry.LoadCustomProfile(name) if loadErr != nil { - return nil, "", loadErr + return nil, "", profileLoadError(loadErr) } return policy, name, nil } diff --git a/cmd/sandbox/profile_lint_test.go b/cmd/sandbox/profile_lint_test.go index 4354e6c..f92cdd0 100644 --- a/cmd/sandbox/profile_lint_test.go +++ b/cmd/sandbox/profile_lint_test.go @@ -196,6 +196,24 @@ func TestProfileLintMissingTargetShowsUsage(t *testing.T) { assert.Contains(t, stdout.String(), "pmg sandbox profile lint npm-restrictive") } +func TestProfileLint_InvalidYAMLReturnsInvalidArgument(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "broken.yml") + require.NoError(t, os.WriteFile(path, []byte("name: broken\npackage_managers:\n - npm\n invalid: : :\n"), 0o644)) + + cmd := newProfileLintCommand(newTestRegistry(t, dir)) + var stdout, stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"broken"}) + + err := cmd.Execute() + require.Error(t, err) + usefulErr, ok := usefulerror.AsUsefulError(err) + require.True(t, ok) + assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code()) +} + func TestProfileLint_LiteralPath(t *testing.T) { dir := t.TempDir() path := writeUserProfileLint(t, dir, "literal", `name: literal diff --git a/cmd/sandbox/profile_list.go b/cmd/sandbox/profile_list.go index 82a43c8..b0a6b48 100644 --- a/cmd/sandbox/profile_list.go +++ b/cmd/sandbox/profile_list.go @@ -7,6 +7,7 @@ import ( "github.com/safedep/pmg/internal/ui" pmgsandbox "github.com/safedep/pmg/sandbox" + "github.com/safedep/pmg/usefulerror" "github.com/spf13/cobra" ) @@ -39,12 +40,13 @@ func newProfileListCommand(factory registryFactory) *cobra.Command { func runProfileList(out io.Writer, opts *profileListOptions, factory registryFactory) error { registry, err := factory() if err != nil { - return err + return registryInitError(err) } summaries, err := registry.ListProfiles() if err != nil { - return err + return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown), + "Failed to enumerate sandbox profiles. Check the user profile directory permissions.") } if opts.jsonOut { diff --git a/cmd/sandbox/profile_list_test.go b/cmd/sandbox/profile_list_test.go index 3ea8497..a1fd5c0 100644 --- a/cmd/sandbox/profile_list_test.go +++ b/cmd/sandbox/profile_list_test.go @@ -3,6 +3,9 @@ package sandbox import ( "bytes" "encoding/json" + "errors" + "fmt" + "io/fs" "os" "path/filepath" "testing" @@ -11,6 +14,7 @@ import ( "github.com/stretchr/testify/require" pmgsandbox "github.com/safedep/pmg/sandbox" + "github.com/safedep/pmg/usefulerror" ) func newTestRegistry(t *testing.T, userDir string) registryFactory { @@ -100,6 +104,43 @@ func TestProfileListShadowedTag(t *testing.T) { 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 diff --git a/cmd/sandbox/profile_show.go b/cmd/sandbox/profile_show.go index 9ea7ed4..b7e1084 100644 --- a/cmd/sandbox/profile_show.go +++ b/cmd/sandbox/profile_show.go @@ -8,6 +8,7 @@ import ( "github.com/safedep/dry/log" pmgsandbox "github.com/safedep/pmg/sandbox" "github.com/safedep/pmg/sandbox/platform" + "github.com/safedep/pmg/usefulerror" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -53,7 +54,7 @@ func runProfileShow(out io.Writer, name string, opts *profileShowOptions, factor registry, err := factory() if err != nil { - return err + return registryInitError(err) } if opts.driver != "" { @@ -70,7 +71,7 @@ func runProfileShow(out io.Writer, name string, opts *profileShowOptions, factor func runProfileShowRaw(out io.Writer, name string, opts *profileShowOptions, registry pmgsandbox.ProfileRegistry) error { source, path, data, err := loadProfileSource(name, registry) if err != nil { - return err + return profileLoadError(err) } if opts.jsonOut { @@ -93,7 +94,7 @@ func runProfileShowResolved(out io.Writer, name string, opts *profileShowOptions Home: opts.home, }) if err != nil { - return err + return profileLoadError(err) } if opts.jsonOut { @@ -106,7 +107,9 @@ func runProfileShowResolved(out io.Writer, name string, opts *profileShowOptions data, err := yaml.Marshal(policy) if err != nil { - return fmt.Errorf("failed to marshal resolved policy: %w", err) + return wrapUseful(fmt.Errorf("failed to marshal resolved policy: %w", err), + usefulerror.ErrCodeUnknown, + "Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.") } _, err = out.Write(data) @@ -119,12 +122,13 @@ func runProfileShowDriver(out io.Writer, name string, opts *profileShowOptions, Home: opts.home, }) if err != nil { - return err + return profileLoadError(err) } rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy) if err != nil { - return err + return wrapUseful(err, usefulerror.ErrCodeInvalidArgument, + "Could not render the policy for the requested driver. Verify the driver is supported on this host.") } if opts.jsonOut { diff --git a/cmd/sandbox/profile_show_test.go b/cmd/sandbox/profile_show_test.go index 4008721..df675bc 100644 --- a/cmd/sandbox/profile_show_test.go +++ b/cmd/sandbox/profile_show_test.go @@ -148,4 +148,7 @@ func TestProfileShowDriverNonNativeErrors(t *testing.T) { require.Error(t, err) assert.Empty(t, stderr.String()) assert.Contains(t, err.Error(), "not available") + usefulErr, ok := usefulerror.AsUsefulError(err) + require.True(t, ok, "non-native driver error should be reported as a UsefulError") + assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code()) } diff --git a/sandbox/errors.go b/sandbox/errors.go new file mode 100644 index 0000000..5da2e06 --- /dev/null +++ b/sandbox/errors.go @@ -0,0 +1,10 @@ +package sandbox + +import "errors" + +// Wrap these with %w so cmd-layer code can classify via errors.Is +// instead of inspecting message text. +var ( + ErrProfileNotFound = errors.New("sandbox profile not found") + ErrProfileInvalid = errors.New("sandbox profile invalid") +) diff --git a/sandbox/registry.go b/sandbox/registry.go index bf6e3c3..84322f7 100644 --- a/sandbox/registry.go +++ b/sandbox/registry.go @@ -66,12 +66,12 @@ func (r *defaultProfileRegistry) loadBuiltinProfiles() error { policy, err := parsePolicy(data) if err != nil { - return fmt.Errorf("failed to parse profile %s: %w", entry.Name(), err) + return fmt.Errorf("failed to parse profile %s: %w: %w", entry.Name(), ErrProfileInvalid, err) } // Basic validation (without inheritance resolution) if err := policy.Validate(); err != nil { - return fmt.Errorf("invalid profile %s: %w", entry.Name(), err) + return fmt.Errorf("invalid profile %s: %w: %w", entry.Name(), ErrProfileInvalid, err) } r.mu.Lock() @@ -93,7 +93,7 @@ func (r *defaultProfileRegistry) loadBuiltinProfiles() error { // Validate after inheritance resolution if err := policy.ValidateResolved(); err != nil { - return fmt.Errorf("invalid profile %s after inheritance: %w", name, err) + return fmt.Errorf("invalid profile %s after inheritance: %w: %w", name, ErrProfileInvalid, err) } } } @@ -112,12 +112,12 @@ func (r *defaultProfileRegistry) resolveInheritance(child *SandboxPolicy) error // Look up parent profile (must be a built-in profile) parent, exists := r.profiles[child.Inherits] if !exists { - return fmt.Errorf("parent profile '%s' not found (only built-in profiles can be inherited)", child.Inherits) + return fmt.Errorf("%w: parent profile '%s' (only built-in profiles can be inherited)", ErrProfileNotFound, child.Inherits) } // Prevent inheritance chains (parent must not itself inherit) if parent.Inherits != "" { - return fmt.Errorf("inheritance chains not allowed: parent profile '%s' inherits from '%s'", parent.Name, parent.Inherits) + return fmt.Errorf("%w: inheritance chains not allowed: parent profile '%s' inherits from '%s'", ErrProfileInvalid, parent.Name, parent.Inherits) } // Merge parent into child @@ -153,7 +153,7 @@ func (r *defaultProfileRegistry) GetProfile(name string) (*SandboxPolicy, error) return r.LoadCustomProfile(name) } - return nil, fmt.Errorf("sandbox profile not found: %s (not a built-in profile, no matching user profile, and file does not exist)", name) + return nil, fmt.Errorf("%w: %s (not a built-in profile, no matching user profile, and file does not exist)", ErrProfileNotFound, name) } // findUserProfileByName looks for `.yml` then `.yaml` under @@ -221,12 +221,12 @@ func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy, policy, err := parsePolicy(data) if err != nil { - return nil, fmt.Errorf("failed to parse custom profile %s: %w", path, err) + return nil, fmt.Errorf("failed to parse custom profile %s: %w: %w", path, ErrProfileInvalid, err) } // Basic validation if err := policy.Validate(); err != nil { - return nil, fmt.Errorf("invalid custom profile %s: %w", path, err) + return nil, fmt.Errorf("invalid custom profile %s: %w: %w", path, ErrProfileInvalid, err) } // Resolve inheritance if present @@ -236,12 +236,12 @@ func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy, r.mu.RUnlock() if !exists { - return nil, fmt.Errorf("custom profile %s inherits from unknown profile '%s' (only built-in profiles can be inherited)", path, policy.Inherits) + return nil, fmt.Errorf("%w: custom profile %s inherits from unknown profile '%s' (only built-in profiles can be inherited)", ErrProfileNotFound, path, policy.Inherits) } // Prevent inheritance chains if parent.Inherits != "" { - return nil, fmt.Errorf("custom profile %s: parent profile '%s' inherits from '%s' (chains not allowed)", path, parent.Name, parent.Inherits) + return nil, fmt.Errorf("%w: custom profile %s parent '%s' inherits from '%s' (chains not allowed)", ErrProfileInvalid, path, parent.Name, parent.Inherits) } // Merge parent into child @@ -251,7 +251,7 @@ func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy, // Validate after inheritance resolution if err := policy.ValidateResolved(); err != nil { - return nil, fmt.Errorf("invalid custom profile %s after inheritance: %w", path, err) + return nil, fmt.Errorf("invalid custom profile %s after inheritance: %w: %w", path, ErrProfileInvalid, err) } r.mu.Lock() diff --git a/sandbox/registry_test.go b/sandbox/registry_test.go index 3a168ea..4a90691 100644 --- a/sandbox/registry_test.go +++ b/sandbox/registry_test.go @@ -1,6 +1,7 @@ package sandbox import ( + "errors" "os" "testing" @@ -24,6 +25,33 @@ func TestNewDefaultProfileRegistry(t *testing.T) { assert.NotNil(t, pypiRestrictive) } +func TestGetProfileUnknownWrapsErrProfileNotFound(t *testing.T) { + registry, err := newDefaultProfileRegistry() + assert.NoError(t, err) + + _, err = registry.GetProfile("definitely-not-a-real-profile-name") + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrProfileNotFound), + "unknown profile should wrap ErrProfileNotFound, got %T: %v", err, err) +} + +func TestLoadCustomProfileInvalidYAMLWrapsErrProfileInvalid(t *testing.T) { + registry, err := newDefaultProfileRegistry() + assert.NoError(t, err) + + tempFile, err := os.CreateTemp(t.TempDir(), "bad-policy-*.yml") + assert.NoError(t, err) + defer func() { _ = tempFile.Close() }() + + _, err = tempFile.WriteString("name: bad\npackage_managers:\n - npm\n invalid: : :\n") + assert.NoError(t, err) + + _, err = registry.LoadCustomProfile(tempFile.Name()) + assert.Error(t, err) + assert.True(t, errors.Is(err, ErrProfileInvalid), + "YAML parse failure should wrap ErrProfileInvalid, got %T: %v", err, err) +} + func TestLoadCustomProfile(t *testing.T) { cases := []struct { name string @@ -188,6 +216,8 @@ func TestLoadCustomProfileWithInheritance(t *testing.T) { assert.Error(t, err) assert.Nil(t, policy) assert.Contains(t, err.Error(), "inherits from unknown profile") + assert.True(t, errors.Is(err, ErrProfileNotFound), + "missing parent profile should wrap ErrProfileNotFound") }, }, {