package rpm import ( "bytes" "compress/gzip" "encoding/xml" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "git.unkin.net/unkin/artifactapi/internal/provider" ) func gunzip(t *testing.T, data []byte) string { t.Helper() zr, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { t.Fatalf("gzip reader: %v", err) } out, err := io.ReadAll(zr) if err != nil { t.Fatalf("gunzip: %v", err) } return string(out) } // sampleMetas returns a fixed two-package repo state whose upload timestamps are // pinned, so any nondeterminism must come from the generators themselves. func sampleMetas() []provider.RPMMetadata { base := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) return []provider.RPMMetadata{ { Name: "alpha", Version: "1.0", Release: "1", Arch: "x86_64", Summary: "a", Description: "d", ContentHash: "sha256:aaa", FilePath: "Packages/alpha-1.0-1.x86_64.rpm", RPMSize: 10, InstalledSize: 20, Provides: []provider.RPMDep{{Name: "alpha"}}, Requires: []provider.RPMDep{{Name: "libc", Flags: "GE", Version: "2.0"}}, CreatedAt: base, }, { Name: "beta", Version: "2.0", Release: "3", Arch: "noarch", Summary: "b", Description: "d2", ContentHash: "sha256:bbb", FilePath: "Packages/beta-2.0-3.noarch.rpm", RPMSize: 30, InstalledSize: 40, CreatedAt: base.Add(time.Hour), }, } } // TestRepodataGeneratorsDeterministic is the direct regression guard for #117: // generating each metadata document twice from identical state must yield // byte-identical output (hence an identical sha256). The old code embedded // time.Now() inside primary.xml.gz, so its bytes/hash drifted every second. func TestRepodataGeneratorsDeterministic(t *testing.T) { metas := sampleMetas() gens := map[string]func([]provider.RPMMetadata) []byte{ "primary": generatePrimaryXMLGZ, "filelists": generateFilelistsXMLGZ, "other": generateOtherXMLGZ, } for name, gen := range gens { a := gen(metas) b := gen(metas) if sha256Hex(a) != sha256Hex(b) { t.Errorf("%s: sha256 differs between two generations (nondeterministic): %s != %s", name, sha256Hex(a), sha256Hex(b)) } } // repomd.xml itself must also be byte-stable across regenerations. r1 := generateRepomd(repomdRevision(metas), sha256Hex(generatePrimaryXMLGZ(metas)), 1, "f", 2, "o", 3) r2 := generateRepomd(repomdRevision(metas), sha256Hex(generatePrimaryXMLGZ(metas)), 1, "f", 2, "o", 3) if string(r1) != string(r2) { t.Error("repomd.xml differs between two generations") } } // TestPrimaryTimeUsesPersistedCreatedAt proves the