Files
artifactapi/internal/virtual/deb_merger_test.go
T
unkin-agent ed6216d78b
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Serve deb virtual indexes by hash and bound cached suites
2026-10-17 12:46:14 +11:00

180 lines
7.4 KiB
Go

package virtual
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"maps"
"path"
"slices"
"strings"
"testing"
)
func debStanza(pkg, ver, arch, filename string) string {
return fmt.Sprintf("Package: %s\nVersion: %s\nArchitecture: %s\nDescription: test\n multi-line body\nFilename: %s\nSize: 1\n", pkg, ver, arch, filename)
}
func flatDebMember(name, date string, stanzas ...string) DebMember {
rel := "Date: " + date + "\nArchitectures: amd64\nAcquire-By-Hash: no\nSHA256:\n x 1 Packages\n x 1 Packages.gz\n"
return DebMember{RemoteName: name, Release: []byte(rel), Packages: map[string][]byte{"": []byte(strings.Join(stanzas, "\n"))}}
}
func TestMergeDebDedupesByMemberOrder(t *testing.T) {
repo := MergeDeb([]DebMember{
flatDebMember("a", "Mon, 05 Oct 2026 10:00:00 +0000",
debStanza("foo", "1.0", "amd64", "pool/foo_1.0_amd64.deb"),
debStanza("bar", "2.0", "amd64", "/pool/bar.deb"),
debStanza("dot", "1.0", "amd64", "./dot.deb"),
"Package: noversion\nFilename: nv1.deb\n",
"Package: noversion\nFilename: nv2.deb\n"),
flatDebMember("b", "Tue, 06 Oct 2026 10:00:00 UTC",
debStanza("foo", "1.0", "amd64", "pool/foo-from-b.deb"),
debStanza("foo", "1.0", "arm64", "pool/foo_arm64.deb"),
debStanza("foo", "1.1", "amd64", "o/r/releases/download/v1/foo.deb")),
})
pk := string(repo.Files["Packages"])
stanzas := splitStanzas(repo.Files["Packages"])
if len(stanzas) != 7 {
t.Fatalf("want 7 stanzas (one duplicate dropped), got %d:\n%s", len(stanzas), pk)
}
for _, want := range []string{
"Filename: a/pool/foo_1.0_amd64.deb",
"Filename: a/pool/bar.deb",
"Filename: a/dot.deb",
"Filename: a/nv1.deb",
"Filename: a/nv2.deb",
"Filename: b/pool/foo_arm64.deb",
"Filename: b/o/r/releases/download/v1/foo.deb",
" multi-line body",
} {
if !strings.Contains(pk, want) {
t.Errorf("merged Packages missing %q:\n%s", want, pk)
}
}
if strings.Contains(pk, "foo-from-b") {
t.Fatalf("second member's duplicate must lose:\n%s", pk)
}
if !bytes.Equal(gunzip(t, repo.Files["Packages.gz"]), repo.Files["Packages"]) {
t.Fatal("Packages.gz does not match Packages")
}
if !strings.Contains(string(repo.Release), "Date: Tue, 06 Oct 2026 10:00:00 +0000\n") {
t.Fatalf("Date should be the newest member date:\n%s", repo.Release)
}
}
func TestMergeDebReleaseHashesFiles(t *testing.T) {
repo := MergeDeb([]DebMember{flatDebMember("a", "", debStanza("foo", "1", "amd64", "pool/foo.deb"))})
rel := parseDebRelease(repo.Release)
if got := slices.Sorted(slices.Values(rel.files)); !slices.Equal(got, []string{"Packages", "Packages.gz"}) {
t.Fatalf("Release lists %v", got)
}
inSHA := false
checked := 0
for line := range strings.SplitSeq(string(repo.Release), "\n") {
if !strings.HasPrefix(line, " ") {
inSHA = line == "SHA256:"
continue
}
var sum, name string
var size int
if _, err := fmt.Sscan(line, &sum, &size, &name); err != nil {
t.Fatal(err)
}
body := repo.Files[name]
want := sha256Hex(body)
if !inSHA {
h := md5.Sum(body)
want = hex.EncodeToString(h[:])
}
if sum != want || size != len(body) {
t.Errorf("%s: Release has %s %d, file is %s %d", name, sum, size, want, len(body))
}
checked++
}
if checked != 4 {
t.Fatalf("checked %d Release entries, want 4:\n%s", checked, repo.Release)
}
if !bytes.Equal(MergeDeb([]DebMember{flatDebMember("a", "", debStanza("foo", "1", "amd64", "pool/foo.deb"))}).Release, repo.Release) {
t.Fatal("merge must be deterministic")
}
}
func TestMergeDebSuiteLayout(t *testing.T) {
rel := func(comps string) []byte {
return []byte("Suite: stable\nCodename: trixie\nArchitectures: amd64 arm64\nComponents: " + comps + "\nSHA256:\n x 1 main/binary-amd64/Packages.xz\n")
}
repo := MergeDeb([]DebMember{
{RemoteName: "deb1", Release: rel("main"), Packages: map[string][]byte{"main/binary-amd64/": []byte(debStanza("foo", "1", "amd64", "pool/main/f/foo.deb"))}},
{RemoteName: "deb2", Release: rel("main contrib"), Packages: map[string][]byte{
"main/binary-amd64/": []byte(debStanza("bar", "1", "amd64", "pool/main/b/bar.deb")),
"contrib/binary-amd64/": []byte(debStanza("baz", "1", "amd64", "pool/contrib/b/baz.deb")),
}},
})
for _, want := range []string{"Suite: stable\n", "Codename: trixie\n", "Components: main contrib\n", "Architectures: amd64 arm64\n"} {
if !strings.Contains(string(repo.Release), want) {
t.Errorf("Release missing %q:\n%s", want, repo.Release)
}
}
want := []string{"contrib/binary-amd64/Packages", "contrib/binary-amd64/Packages.gz", "main/binary-amd64/Packages", "main/binary-amd64/Packages.gz"}
if got := slices.Sorted(slices.Values(parseDebRelease(repo.Release).files)); !slices.Equal(got, want) {
t.Fatalf("Release lists %v, want %v", got, want)
}
if !strings.Contains(string(repo.Release), "Acquire-By-Hash: yes\n") {
t.Fatalf("Release must advertise by-hash:\n%s", repo.Release)
}
for _, n := range want {
byHash := path.Dir(n) + "/by-hash/SHA256/" + sha256Hex(repo.Files[n])
if !bytes.Equal(repo.Files[byHash], repo.Files[n]) {
t.Errorf("%s not served at %s", n, byHash)
}
}
if len(repo.Files) != 2*len(want) {
t.Fatalf("files %v, want each index plus its by-hash copy", slices.Sorted(maps.Keys(repo.Files)))
}
if n := len(splitStanzas(repo.Files["main/binary-amd64/Packages"])); n != 2 {
t.Fatalf("main/binary-amd64 should merge both members, got %d stanzas", n)
}
}
func TestDebIndexFiles(t *testing.T) {
rel := parseDebRelease([]byte("Origin: Debian\nMD5Sum:\n a 1 main/binary-amd64/Packages\n a 1 main/binary-amd64/Packages.gz\nSHA256:\n" +
" a 1 main/binary-amd64/Packages\n a 1 main/binary-amd64/Packages.xz\n a 1 main/binary-amd64/Packages.gz\n" +
" a 1 main/debian-installer/binary-amd64/Packages.xz\n a 1 main/i18n/Translation-en.xz\n a 1 contrib/binary-all/Packages\n"))
got := debIndexFiles(rel)
want := map[string]string{"main/binary-amd64/": "main/binary-amd64/Packages.xz", "contrib/binary-all/": "contrib/binary-all/Packages"}
if !maps.Equal(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
if flat := debIndexFiles(parseDebRelease([]byte("SHA256:\n a 1 Packages\n a 1 Packages.gz\n"))); flat[""] != "Packages.gz" {
t.Fatalf("flat repo should fetch Packages.gz, got %v", flat)
}
}
func TestDebIndexPath(t *testing.T) {
for _, tc := range []struct {
in, base, name string
ok bool
}{
{"Release", "", "Release", true},
{"Packages.gz", "", "Packages.gz", true},
{"dists/trixie/Release", "dists/trixie/", "Release", true},
{"dists/trixie/main/binary-amd64/Packages", "dists/trixie/", "main/binary-amd64/Packages", true},
{"by-hash/SHA256/abababababababababababababababababababababababababababababababab", "", "by-hash/SHA256/abababababababababababababababababababababababababababababababab", true},
{"dists/trixie/main/binary-amd64/by-hash/SHA256/abababababababababababababababababababababababababababababababab", "dists/trixie/", "main/binary-amd64/by-hash/SHA256/abababababababababababababababababababababababababababababababab", true},
{"dists/trixie/main/binary-amd64/by-hash/MD5Sum/abababababababababababababababab", "", "", false},
{"InRelease", "", "", false},
{"Release.gpg", "", "", false},
{"dists/trixie/InRelease", "", "", false},
{"dists/trixie/main/i18n/Translation-en", "", "", false},
{"dists/Release", "", "", false},
{"pool/foo.deb", "", "", false},
} {
base, name, ok := debIndexPath(tc.in)
if base != tc.base || name != tc.name || ok != tc.ok {
t.Errorf("%s: got (%q, %q, %v), want (%q, %q, %v)", tc.in, base, name, ok, tc.base, tc.name, tc.ok)
}
}
}