180 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|