package rpm import ( "bytes" "compress/gzip" "encoding/xml" "fmt" "os" "path/filepath" "testing" ) // buildPrimaryGz creates a gzipped primary.xml with the given packages. // Each entry is {name, locationHref}. func buildPrimaryGz(t *testing.T, pkgs []struct{ name, href string }) []byte { t.Helper() var buf bytes.Buffer buf.WriteString(``) fmt.Fprintf(&buf, ``, len(pkgs)) for _, p := range pkgs { fmt.Fprintf(&buf, `%s`, p.name, p.href) } buf.WriteString(``) var gz bytes.Buffer gzw := gzip.NewWriter(&gz) gzw.Write(buf.Bytes()) gzw.Close() return gz.Bytes() } // buildRepoMD creates a minimal repomd.xml pointing to the given primary href. func buildRepoMD(t *testing.T, primaryHref string) []byte { t.Helper() repomd := RepoMD{ Data: []RepoMDEntry{{ Type: "primary", Location: RepoMDLocation{Href: primaryHref}, Checksum: RepoMDChecksum{Type: "sha256", Value: "placeholder"}, OpenChecksum: RepoMDChecksum{Type: "sha256", Value: "placeholder"}, }}, } out, err := xml.MarshalIndent(repomd, "", " ") if err != nil { t.Fatalf("marshal repomd: %v", err) } return append([]byte(xml.Header), out...) } // setupRepo creates a local repo dir with a primary.xml.gz, a .primary-source // marker and a repomd.xml. It creates the given rpm files on disk. func setupRepo(t *testing.T, pkgs []struct{ name, href string }, presentOnDisk []string) string { t.Helper() dir := t.TempDir() repodata := filepath.Join(dir, "repodata") os.MkdirAll(repodata, 0o755) // Upstream primary at a hashed path (mimics real layout). upstreamPrimary := "repodata/upstream-primary.xml.gz" if err := os.WriteFile(filepath.Join(dir, upstreamPrimary), buildPrimaryGz(t, pkgs), 0o644); err != nil { t.Fatalf("write upstream primary: %v", err) } // .primary-source marker. if err := os.WriteFile(filepath.Join(repodata, PrimarySourceFile), []byte(upstreamPrimary), 0o644); err != nil { t.Fatalf("write .primary-source: %v", err) } // repomd.xml pointing at local primary (which doesn't exist yet — regen will create it). if err := os.WriteFile(filepath.Join(repodata, "repomd.xml"), buildRepoMD(t, localPrimaryHref), 0o644); err != nil { t.Fatalf("write repomd.xml: %v", err) } // Create selected rpm files on disk. for _, href := range presentOnDisk { dest := filepath.Join(dir, filepath.FromSlash(href)) os.MkdirAll(filepath.Dir(dest), 0o755) if err := os.WriteFile(dest, []byte("fake-rpm"), 0o644); err != nil { t.Fatalf("write rpm %s: %v", href, err) } } return dir } // parsePrimaryGz decompresses and parses a primary.xml.gz, returning package hrefs. func parsePrimaryGz(t *testing.T, path string) []string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read primary.xml.gz: %v", err) } gz, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { t.Fatalf("open gzip: %v", err) } defer gz.Close() var meta filterableMetadata if err := xml.NewDecoder(gz).Decode(&meta); err != nil { t.Fatalf("parse primary.xml: %v", err) } hrefs := make([]string, len(meta.Packages)) for i, p := range meta.Packages { hrefs[i] = p.Location.Href } return hrefs } // --- InitEmptyRepo --- func TestInitEmptyRepo_createsStructure(t *testing.T) { dir := t.TempDir() if err := InitEmptyRepo(dir); err != nil { t.Fatalf("InitEmptyRepo: %v", err) } for _, path := range []string{ filepath.Join(dir, "repodata"), filepath.Join(dir, "Packages"), filepath.Join(dir, "repodata", "repomd.xml"), filepath.Join(dir, localPrimaryHref), } { if _, err := os.Stat(path); err != nil { t.Errorf("expected %s to exist: %v", path, err) } } } func TestInitEmptyRepo_repomdIsValid(t *testing.T) { dir := t.TempDir() InitEmptyRepo(dir) data, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml")) var repomd RepoMD if err := xml.Unmarshal(data, &repomd); err != nil { t.Fatalf("repomd.xml is not valid XML: %v", err) } if len(repomd.Data) == 0 { t.Error("expected at least one entry in repomd.xml") } if repomd.Data[0].Type != "primary" { t.Errorf("first entry type: got %q, want primary", repomd.Data[0].Type) } } func TestInitEmptyRepo_primaryIsEmpty(t *testing.T) { dir := t.TempDir() InitEmptyRepo(dir) hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref)) if len(hrefs) != 0 { t.Errorf("expected 0 packages in empty repo, got %d", len(hrefs)) } } func TestInitEmptyRepo_idempotent(t *testing.T) { dir := t.TempDir() InitEmptyRepo(dir) repomdBefore, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml")) InitEmptyRepo(dir) // second call repomdAfter, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml")) if !bytes.Equal(repomdBefore, repomdAfter) { t.Error("second InitEmptyRepo modified repomd.xml") } } // --- RegenerateMetadata --- func TestRegenerateMetadata_keepsOnlyPresentFiles(t *testing.T) { allPkgs := []struct{ name, href string }{ {"curl", "Packages/curl-7.88-1.x86_64.rpm"}, {"wget", "Packages/wget-1.21-1.x86_64.rpm"}, {"vim", "Packages/vim-9.0-1.x86_64.rpm"}, } // Only curl and vim are on disk. presentOnDisk := []string{"Packages/curl-7.88-1.x86_64.rpm", "Packages/vim-9.0-1.x86_64.rpm"} dir := setupRepo(t, allPkgs, presentOnDisk) if err := RegenerateMetadata(dir); err != nil { t.Fatalf("RegenerateMetadata: %v", err) } hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref)) if len(hrefs) != 2 { t.Fatalf("expected 2 packages, got %d: %v", len(hrefs), hrefs) } hrefSet := map[string]bool{hrefs[0]: true, hrefs[1]: true} if !hrefSet["Packages/curl-7.88-1.x86_64.rpm"] { t.Error("expected curl in output") } if !hrefSet["Packages/vim-9.0-1.x86_64.rpm"] { t.Error("expected vim in output") } if hrefSet["Packages/wget-1.21-1.x86_64.rpm"] { t.Error("wget should have been filtered out (not on disk)") } } func TestRegenerateMetadata_allAbsent(t *testing.T) { allPkgs := []struct{ name, href string }{ {"curl", "Packages/curl-7.88-1.x86_64.rpm"}, {"wget", "Packages/wget-1.21-1.x86_64.rpm"}, } dir := setupRepo(t, allPkgs, nil) // nothing on disk if err := RegenerateMetadata(dir); err != nil { t.Fatalf("RegenerateMetadata: %v", err) } hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref)) if len(hrefs) != 0 { t.Errorf("expected 0 packages, got %d", len(hrefs)) } } func TestRegenerateMetadata_updatesRepoMD(t *testing.T) { allPkgs := []struct{ name, href string }{ {"curl", "Packages/curl-7.88-1.x86_64.rpm"}, } dir := setupRepo(t, allPkgs, []string{"Packages/curl-7.88-1.x86_64.rpm"}) if err := RegenerateMetadata(dir); err != nil { t.Fatalf("RegenerateMetadata: %v", err) } data, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml")) var repomd RepoMD xml.Unmarshal(data, &repomd) var primary *RepoMDEntry for i := range repomd.Data { if repomd.Data[i].Type == "primary" { primary = &repomd.Data[i] } } if primary == nil { t.Fatal("no primary entry in repomd.xml after regen") } if primary.Location.Href != localPrimaryHref { t.Errorf("Location.Href: got %q, want %q", primary.Location.Href, localPrimaryHref) } if primary.Checksum.Type != "sha256" { t.Errorf("Checksum.Type: got %q, want sha256", primary.Checksum.Type) } if primary.Checksum.Value == "placeholder" { t.Error("checksum was not updated from placeholder") } if primary.Size == 0 { t.Error("Size should be non-zero after regen") } } func TestRegenerateMetadata_alwaysFiltersFromUpstream(t *testing.T) { // Simulates two successive approvals — regen must always read upstream, not the // previously-filtered local primary. allPkgs := []struct{ name, href string }{ {"curl", "Packages/curl-7.88-1.x86_64.rpm"}, {"wget", "Packages/wget-1.21-1.x86_64.rpm"}, {"vim", "Packages/vim-9.0-1.x86_64.rpm"}, } dir := setupRepo(t, allPkgs, []string{"Packages/curl-7.88-1.x86_64.rpm"}) // First regen: only curl on disk. RegenerateMetadata(dir) hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref)) if len(hrefs) != 1 { t.Fatalf("after first regen: expected 1, got %d", len(hrefs)) } // Add wget to disk, regen again — vim still absent. dest := filepath.Join(dir, "Packages", "wget-1.21-1.x86_64.rpm") os.WriteFile(dest, []byte("fake"), 0o644) RegenerateMetadata(dir) hrefs = parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref)) if len(hrefs) != 2 { t.Fatalf("after second regen: expected 2, got %d: %v", len(hrefs), hrefs) } }