banger/internal/updater/sha256sums_test.go
Thales Maciel fb6d2b1dae
updater: manifest + SHA256SUMS parsing scaffolding
First slice of the `banger update` package. No CLI yet — this just
defines the wire shape and parsers the rest of the flow will plug
into.

  * internal/updater/manifest.go — Manifest / Release types,
    ManifestSchemaVersion = 1, the hardcoded URL
    https://releases.thaloco.com/banger/manifest.json (var instead
    of const so tests can point at httptest), and FetchManifest /
    ParseManifest / Manifest.LookupRelease / Manifest.Latest.
    The manifest only references URLs (tarball, SHA256SUMS, optional
    signature); actual binary hashes come from SHA256SUMS itself,
    so manifest tampering can't substitute a hash for a known-good
    tarball.
    SchemaVersion gates forward-compat: a CLI that doesn't know its
    server's schema_version refuses to update rather than guessing.
  * internal/updater/sha256sums.go — ParseSHA256Sums tolerates both
    GNU `<digest>  <file>` (with optional `*` binary prefix) and
    BSD `SHA256 (file) = <digest>` formats. Comments and blank
    lines are skipped; malformed lines that LOOK like entries are
    rejected (silent skipping is the wrong failure mode for a
    security-relevant input). Digests are lowercased so the caller
    can `==`-compare without worrying about case.

Caps: 1 MiB on the manifest body, 16 KiB on SHA256SUMS, 256 MiB on
release tarballs. Generous-but-bounded; bumping requires a code
change so a server-side mistake can't fill the disk.

Tests: ParseManifest happy path, schema-version-too-new rejection,
five malformed-input cases. ParseSHA256Sums covers GNU + BSD +
star-prefix + comments-and-blanks, six malformed-input rejections,
case-insensitive digest normalisation. FetchManifest end-to-end via
httptest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:24:36 -03:00

98 lines
3.1 KiB
Go

package updater
import (
"strings"
"testing"
)
func TestParseSHA256SumsGNUFormat(t *testing.T) {
body := []byte(`# header comment
0000000000000000000000000000000000000000000000000000000000000001 banger-v0.1.0-linux-amd64.tar.gz
0000000000000000000000000000000000000000000000000000000000000002 banger-v0.1.0-linux-amd64.tar.gz.sig
`)
got, err := ParseSHA256Sums(body)
if err != nil {
t.Fatalf("ParseSHA256Sums: %v", err)
}
if got["banger-v0.1.0-linux-amd64.tar.gz"] != "0000000000000000000000000000000000000000000000000000000000000001" {
t.Fatalf("tarball digest = %q", got["banger-v0.1.0-linux-amd64.tar.gz"])
}
if len(got) != 2 {
t.Fatalf("got %d entries, want 2", len(got))
}
}
func TestParseSHA256SumsBSDFormat(t *testing.T) {
body := []byte(`SHA256 (banger-v0.1.0-linux-amd64.tar.gz) = 0000000000000000000000000000000000000000000000000000000000000001
`)
got, err := ParseSHA256Sums(body)
if err != nil {
t.Fatalf("ParseSHA256Sums: %v", err)
}
if got["banger-v0.1.0-linux-amd64.tar.gz"] != "0000000000000000000000000000000000000000000000000000000000000001" {
t.Fatalf("digest = %q", got["banger-v0.1.0-linux-amd64.tar.gz"])
}
}
func TestParseSHA256SumsBinaryStarPrefix(t *testing.T) {
// `sha256sum -b` emits `<digest> *<filename>`.
body := []byte(`0000000000000000000000000000000000000000000000000000000000000001 *banger-v0.1.0-linux-amd64.tar.gz
`)
got, err := ParseSHA256Sums(body)
if err != nil {
t.Fatalf("ParseSHA256Sums: %v", err)
}
if _, ok := got["banger-v0.1.0-linux-amd64.tar.gz"]; !ok {
t.Fatalf("entries = %v, want star-prefix stripped", got)
}
}
func TestParseSHA256SumsTolerantOfBlankAndComments(t *testing.T) {
body := []byte(`
# top comment
0000000000000000000000000000000000000000000000000000000000000001 a
# inline comment
0000000000000000000000000000000000000000000000000000000000000002 b
`)
got, err := ParseSHA256Sums(body)
if err != nil {
t.Fatalf("ParseSHA256Sums: %v", err)
}
if len(got) != 2 {
t.Fatalf("got %d, want 2", len(got))
}
}
func TestParseSHA256SumsRejectsMalformed(t *testing.T) {
for _, tc := range []struct {
name string
body string
}{
{name: "short_digest", body: "abc file"},
{name: "non_hex_digest", body: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz file"},
{name: "no_filename", body: "0000000000000000000000000000000000000000000000000000000000000001"},
{name: "empty_body", body: ""},
{name: "only_comments", body: "# comment\n# more\n"},
{name: "bsd_no_eq", body: "SHA256 (file) 0000000000000000000000000000000000000000000000000000000000000001"},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if _, err := ParseSHA256Sums([]byte(tc.body)); err == nil {
t.Fatalf("expected error for %s", tc.name)
}
})
}
}
func TestParseSHA256SumsLowercasesDigest(t *testing.T) {
body := []byte(`ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890 upper
`)
got, err := ParseSHA256Sums(body)
if err != nil {
t.Fatalf("ParseSHA256Sums: %v", err)
}
if got["upper"] != strings.ToLower("ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890") {
t.Fatalf("digest not lowercased: %q", got["upper"])
}
}