//nolint:testpackage // white-box tests exercise unexported internals package cli import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // errMsgCase is one pinned user-visible error message. type errMsgCase struct { name string err error want string } const ( msgFpA = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" msgFpB = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" ) func checkErrMsgCases(t *testing.T, cases []errMsgCase) { t.Helper() for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() assert.Equal(t, tc.want, tc.err.Error()) }) } } // TestErrorMessagesVerbatim pins the exact rendered text of the CLI's // user-visible error messages. // // These strings are an interface: they are grepped for in CI pipelines // and quoted in bug reports. The messages are assembled by wrapping // static sentinels, and it is easy to change what a user sees while // only meaning to make an error matchable with errors.Is - which is // precisely what happened once already. Any change to a string below is // therefore a deliberate, separately stated change, never a side effect // of a refactor. func TestErrorMessagesVerbatim(t *testing.T) { t.Parallel() checkErrMsgCases(t, []errMsgCase{ { name: "check: no manifest found", err: fmt.Errorf("%w in %s (looked for index.mf and .index.mf)", errNoManifestFound, "/tmp/x"), want: "no manifest found in /tmp/x " + "(looked for index.mf and .index.mf)", }, { name: "check: invalid fingerprint length", err: fmt.Errorf("%w, got %d", errInvalidFingerprint, 8), want: "invalid fingerprint: must be exactly 40 hex characters, got 8", }, { name: "check: manifest not signed", err: fmt.Errorf("%w, but signature from %s is required", errManifestNotSigned, msgFpA), want: "manifest is not signed, but signature from " + msgFpA + " is required", }, { name: "check: signer mismatch", err: fmt.Errorf("embedded signing key fingerprint %s %w %s", msgFpA, errSignerMismatch, msgFpB), want: "embedded signing key fingerprint " + msgFpA + " does not match required " + msgFpB, }, { name: "gen: path does not exist", err: fmt.Errorf("%w: %s", errPathNotExist, "nope"), want: "path does not exist: nope", }, { name: "gen: output file exists", err: fmt.Errorf("output file %s %w", "index.mf", errOutputExists), want: "output file index.mf already exists " + "(use --force to overwrite)", }, { name: "mfer: unknown command", err: fmt.Errorf("%w %q", errUnknownCommand, "bogus"), want: `unknown command "bogus"`, }, }) } // TestFetchErrorMessagesVerbatim pins the fetch and manifest-loader // messages; see TestErrorMessagesVerbatim for why. func TestFetchErrorMessagesVerbatim(t *testing.T) { t.Parallel() checkErrMsgCases(t, []errMsgCase{ { name: "manifest_loader: http status", err: fmt.Errorf("failed to fetch %s: %w %d", "https://example.com/index.mf", errHTTPStatus, 404), want: "failed to fetch https://example.com/index.mf: HTTP 404", }, { name: "fetch: manifest http status", err: fmt.Errorf("failed to fetch manifest: %w %d", errHTTPStatus, 404), want: "failed to fetch manifest: HTTP 404", }, { name: "fetch: file http status", err: fmt.Errorf("%w %d", errHTTPStatus, 500), want: "HTTP 500", }, { name: "fetch: empty path", err: errEmptyPath, want: "empty path", }, { name: "fetch: absolute path", err: fmt.Errorf("%w: %s", errAbsolutePath, "/etc/passwd"), want: "absolute path not allowed: /etc/passwd", }, { name: "fetch: path traversal", err: fmt.Errorf("%w: %s", errPathTraversal, "../x"), want: "path traversal not allowed: ../x", }, { name: "fetch: size mismatch", err: fmt.Errorf("%w: expected %d bytes, got %d", errSizeMismatch, 10, 9), want: "size mismatch: expected 10 bytes, got 9", }, { name: "fetch: url required", err: errURLRequired, want: "URL argument required", }, { name: "fetch: hash mismatch", err: errHashMismatch, want: "hash mismatch", }, }) } // TestSentinelsAreMatchable checks that the wrapped forms of the // messages above remain matchable with errors.Is, which is the reason // the sentinels exist at all. func TestSentinelsAreMatchable(t *testing.T) { t.Parallel() wrapped := fmt.Errorf("embedded signing key fingerprint %s %w %s", "a", errSignerMismatch, "b") require.ErrorIs(t, wrapped, errSignerMismatch) wrapped = fmt.Errorf("output file %s %w", "index.mf", errOutputExists) require.ErrorIs(t, wrapped, errOutputExists) wrapped = fmt.Errorf("failed to fetch manifest: %w %d", errHTTPStatus, 404) require.ErrorIs(t, wrapped, errHTTPStatus) assert.NotErrorIs(t, errHashMismatch, errSizeMismatch) }