Some checks failed
check / check (push) Has been cancelled
Adopts golangci-lint v2.12.2 and the canonical .golangci.yml (default: all), and fixes all resulting findings across the tree. Two intended behavior changes: absent MFFilePath.Mtime is handled explicitly in freshen, list and export rather than dereferenced (main panicked); gpg positional key IDs now follow an explicit -- end-of-options marker. All twelve reworded user-visible error messages restored to byte-identical parity with main and pinned by tests.
169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
//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)
|
|
}
|