284 lines
8.5 KiB
Go
284 lines
8.5 KiB
Go
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(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
fmt.Fprintf(&buf, `<metadata xmlns="http://linux.duke.edu/metadata/common" packages="%d">`, len(pkgs))
|
|
for _, p := range pkgs {
|
|
fmt.Fprintf(&buf,
|
|
`<package type="rpm"><name>%s</name><location href="%s"/></package>`,
|
|
p.name, p.href)
|
|
}
|
|
buf.WriteString(`</metadata>`)
|
|
|
|
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)
|
|
}
|
|
}
|