78d24f7752
- merge member Packages indexes (flat and dists/<suite>) into one apt repo with a generated unsigned Release - prefix Filename with the member name and 302 downloads to the member route - share the rpm merge cache, singleflight and fail-closed member fetch
158 lines
6.2 KiB
Go
158 lines
6.2 KiB
Go
package virtual
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"maps"
|
|
"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")),
|
|
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) != 4 {
|
|
t.Fatalf("want 4 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: 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(maps.Keys(repo.Files)); !slices.Equal(got, want) {
|
|
t.Fatalf("files %v, want %v", got, want)
|
|
}
|
|
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},
|
|
{"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)
|
|
}
|
|
}
|
|
}
|