package apt import ( "bytes" "compress/gzip" "os" "path/filepath" "strings" "testing" ) var testCfg = Config{ Suite: "bookworm", Components: []string{"main"}, Architectures: []string{"amd64"}, } // buildPackagesGz creates a gzipped Packages file from a list of stanzas. func buildPackagesGz(t *testing.T, stanzas []string) []byte { t.Helper() plain := []byte(strings.Join(stanzas, "\n\n") + "\n") var buf bytes.Buffer gzw := gzip.NewWriter(&buf) gzw.Write(plain) gzw.Close() return buf.Bytes() } func makeStanza(pkg, version, filename, sha256 string, size int) string { return strings.Join([]string{ "Package: " + pkg, "Version: " + version, "Architecture: amd64", "Filename: " + filename, "SHA256: " + sha256, "Size: " + itoa(size), }, "\n") } func itoa(n int) string { if n == 0 { return "0" } buf := make([]byte, 0, 10) for n > 0 { buf = append([]byte{byte('0' + n%10)}, buf...) n /= 10 } return string(buf) } // setupAptRepo creates the directory structure and writes the upstream // Packages.gz for component/arch. It creates the given deb files on disk. func setupAptRepo(t *testing.T, stanzas []string, presentOnDisk []string) string { t.Helper() dir := t.TempDir() binDir := filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64") os.MkdirAll(binDir, 0o755) os.MkdirAll(filepath.Join(dir, "pool"), 0o755) // Write upstream Packages.gz. gzData := buildPackagesGz(t, stanzas) if err := os.WriteFile(filepath.Join(binDir, upstreamPackagesGz), gzData, 0o644); err != nil { t.Fatalf("write upstream Packages.gz: %v", err) } // Create selected deb files on disk. for _, filename := range presentOnDisk { dest := filepath.Join(dir, filepath.FromSlash(filename)) os.MkdirAll(filepath.Dir(dest), 0o755) if err := os.WriteFile(dest, []byte("fake-deb"), 0o644); err != nil { t.Fatalf("write deb %s: %v", filename, err) } } return dir } // readLocalPackages reads and parses the local Packages file (plain text). func readLocalPackages(t *testing.T, dir string) []DebPackage { t.Helper() path := filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64", "Packages") data, err := os.ReadFile(path) if err != nil { t.Fatalf("read Packages: %v", err) } pkgs, err := ParsePackages(data) if err != nil { t.Fatalf("parse Packages: %v", err) } return pkgs } // --- InitEmptyRepo --- func TestInitEmptyRepo_createsStructure(t *testing.T) { dir := t.TempDir() if err := InitEmptyRepo(dir, testCfg); err != nil { t.Fatalf("InitEmptyRepo: %v", err) } for _, path := range []string{ filepath.Join(dir, "pool"), filepath.Join(dir, "dists", testCfg.Suite), filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64"), filepath.Join(dir, "dists", testCfg.Suite, "Release"), } { if _, err := os.Stat(path); err != nil { t.Errorf("expected %s to exist: %v", path, err) } } } func TestInitEmptyRepo_releaseIsParseable(t *testing.T) { dir := t.TempDir() InitEmptyRepo(dir, testCfg) data, _ := os.ReadFile(filepath.Join(dir, "dists", testCfg.Suite, "Release")) rf, err := ParseRelease(data) if err != nil { t.Fatalf("ParseRelease on generated file: %v", err) } if rf.Suite != testCfg.Suite { t.Errorf("Suite: got %q, want %q", rf.Suite, testCfg.Suite) } } func TestInitEmptyRepo_idempotent(t *testing.T) { dir := t.TempDir() InitEmptyRepo(dir, testCfg) releaseBefore, _ := os.ReadFile(filepath.Join(dir, "dists", testCfg.Suite, "Release")) InitEmptyRepo(dir, testCfg) // second call releaseAfter, _ := os.ReadFile(filepath.Join(dir, "dists", testCfg.Suite, "Release")) if !bytes.Equal(releaseBefore, releaseAfter) { t.Error("second InitEmptyRepo modified Release file") } } // --- RegenerateMetadata --- func TestRegenerateMetadata_keepsOnlyPresentFiles(t *testing.T) { stanzas := []string{ makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100), makeStanza("wget", "1.21", "pool/main/w/wget/wget_1.21_amd64.deb", "bbb", 200), makeStanza("vim", "9.0", "pool/main/v/vim/vim_9.0_amd64.deb", "ccc", 300), } // Only curl and vim are present. present := []string{ "pool/main/c/curl/curl_7.88_amd64.deb", "pool/main/v/vim/vim_9.0_amd64.deb", } dir := setupAptRepo(t, stanzas, present) if err := RegenerateMetadata(dir, testCfg); err != nil { t.Fatalf("RegenerateMetadata: %v", err) } pkgs := readLocalPackages(t, dir) if len(pkgs) != 2 { t.Fatalf("expected 2 packages, got %d", len(pkgs)) } names := map[string]bool{pkgs[0].Package: true, pkgs[1].Package: true} if !names["curl"] { t.Error("expected curl in output") } if !names["vim"] { t.Error("expected vim in output") } if names["wget"] { t.Error("wget should be filtered out (not on disk)") } } func TestRegenerateMetadata_allAbsent(t *testing.T) { stanzas := []string{ makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100), } dir := setupAptRepo(t, stanzas, nil) // nothing on disk if err := RegenerateMetadata(dir, testCfg); err != nil { t.Fatalf("RegenerateMetadata: %v", err) } pkgs := readLocalPackages(t, dir) if len(pkgs) != 0 { t.Errorf("expected 0 packages, got %d", len(pkgs)) } } func TestRegenerateMetadata_writesRelease(t *testing.T) { stanzas := []string{ makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100), } dir := setupAptRepo(t, stanzas, []string{"pool/main/c/curl/curl_7.88_amd64.deb"}) if err := RegenerateMetadata(dir, testCfg); err != nil { t.Fatalf("RegenerateMetadata: %v", err) } releasePath := filepath.Join(dir, "dists", testCfg.Suite, "Release") data, err := os.ReadFile(releasePath) if err != nil { t.Fatalf("Release not written: %v", err) } rf, err := ParseRelease(data) if err != nil { t.Fatalf("ParseRelease: %v", err) } if rf.Suite != testCfg.Suite { t.Errorf("Suite: got %q, want %q", rf.Suite, testCfg.Suite) } // Release must reference both Packages and Packages.gz. paths := map[string]bool{} for _, e := range rf.SHA256 { paths[e.Path] = true } if !paths["main/binary-amd64/Packages"] { t.Error("Release missing main/binary-amd64/Packages entry") } if !paths["main/binary-amd64/Packages.gz"] { t.Error("Release missing main/binary-amd64/Packages.gz entry") } } func TestRegenerateMetadata_writesPackagesGz(t *testing.T) { stanzas := []string{ makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100), } dir := setupAptRepo(t, stanzas, []string{"pool/main/c/curl/curl_7.88_amd64.deb"}) RegenerateMetadata(dir, testCfg) gzPath := filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64", "Packages.gz") if _, err := os.Stat(gzPath); err != nil { t.Errorf("Packages.gz not written: %v", err) } } func TestRegenerateMetadata_noUpstream_skips(t *testing.T) { // No .upstream-Packages.gz — should return nil (skip silently). dir := t.TempDir() os.MkdirAll(filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64"), 0o755) if err := RegenerateMetadata(dir, testCfg); err != nil { t.Errorf("expected no error when upstream missing, got: %v", err) } } func TestRegenerateMetadata_alwaysFiltersFromUpstream(t *testing.T) { // Two successive regens must always start from the upstream list. stanzas := []string{ makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100), makeStanza("wget", "1.21", "pool/main/w/wget/wget_1.21_amd64.deb", "bbb", 200), } dir := setupAptRepo(t, stanzas, []string{"pool/main/c/curl/curl_7.88_amd64.deb"}) // First regen: only curl. RegenerateMetadata(dir, testCfg) if len(readLocalPackages(t, dir)) != 1 { t.Fatal("first regen: expected 1 package") } // Add wget, regen again. dest := filepath.Join(dir, "pool/main/w/wget/wget_1.21_amd64.deb") os.MkdirAll(filepath.Dir(dest), 0o755) os.WriteFile(dest, []byte("fake"), 0o644) RegenerateMetadata(dir, testCfg) pkgs := readLocalPackages(t, dir) if len(pkgs) != 2 { t.Fatalf("second regen: expected 2 packages, got %d", len(pkgs)) } }