From b6091c859d60384b0664f5f25d4ab46cbfda1174 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Fri, 3 Jul 2026 12:43:21 +1000 Subject: [PATCH] test: cover rpm provider (metadata parse, repodata generation) rpm 2.7% -> 83.6%, pure-go via a real RPM testdata fixture and fake BlobReader/MetadataStore/RPMMetadataReader implementations. --- internal/provider/rpm/rpm_meta_test.go | 178 +++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 internal/provider/rpm/rpm_meta_test.go diff --git a/internal/provider/rpm/rpm_meta_test.go b/internal/provider/rpm/rpm_meta_test.go new file mode 100644 index 0000000..2233839 --- /dev/null +++ b/internal/provider/rpm/rpm_meta_test.go @@ -0,0 +1,178 @@ +package rpm + +import ( + "bytes" + "compress/gzip" + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "git.unkin.net/unkin/artifactapi/internal/provider" + "git.unkin.net/unkin/artifactapi/pkg/models" +) + +const fixtureRPM = "testdata/e2e-testpkg-1.0-1.noarch.rpm" + +type fakeBlobReader struct{ data []byte } + +func (f fakeBlobReader) Download(_ context.Context, _ string) (io.ReadCloser, int64, error) { + return io.NopCloser(bytes.NewReader(f.data)), int64(len(f.data)), nil +} + +type fakeMetaStore struct{ inserted *provider.RPMMetadata } + +func (f *fakeMetaStore) InsertRPMMetadata(_ context.Context, m *provider.RPMMetadata) error { + f.inserted = m + return nil +} + +type fakeRPMReader struct{ metas []provider.RPMMetadata } + +func (f fakeRPMReader) ListRPMMetadataEntries(_ context.Context, _ string) ([]provider.RPMMetadata, error) { + return f.metas, nil +} +func (f fakeRPMReader) ListFilesByPrefix(_ context.Context, _, _ string) ([]provider.FileEntry, error) { + return nil, nil +} +func (f fakeRPMReader) ListPackages(_ context.Context, _ string) ([]string, error) { return nil, nil } + +func TestRPMPureFuncs(t *testing.T) { + p := &Provider{} + if p.Type() != models.PackageRPM { + t.Error("type") + } + if p.Classify("repodata/repomd.xml") != provider.Mutable { + t.Error("repomd should be mutable") + } + if p.Classify("Packages/foo.rpm") != provider.Immutable { + t.Error("rpm should be immutable") + } + if p.ContentType("x.rpm") != "application/x-rpm" { + t.Error("rpm content type") + } + if got := p.UpstreamURL(models.Remote{BaseURL: "https://mirror/"}, "/Packages/x.rpm"); got != "https://mirror/Packages/x.rpm" { + t.Errorf("upstream url %q", got) + } + if out, _ := p.RewriteResponse(nil, models.Remote{}, "http://p"); out != nil { + t.Error("rpm never rewrites") + } + h, _ := p.AuthHeaders(context.Background(), models.Remote{Username: "u", Password: "p"}) + if h.Get("Authorization") == "" { + t.Error("auth header") + } +} + +func TestRPMValidateUpload(t *testing.T) { + p := &Provider{} + sp, ct, err := p.ValidateUpload("dir/foo-1.0.noarch.rpm") + if err != nil || sp != "Packages/foo-1.0.noarch.rpm" || ct != "application/x-rpm" { + t.Errorf("sp=%q ct=%q err=%v", sp, ct, err) + } + if _, _, err := p.ValidateUpload("foo.txt"); err == nil { + t.Error("expected error for non-rpm") + } + resp := p.UploadResponse("Packages/foo.rpm", "sha256:abc", 10) + if resp["content_hash"] != "sha256:abc" { + t.Errorf("upload response %v", resp) + } +} + +func TestRPMAfterUpload(t *testing.T) { + data, err := os.ReadFile(fixtureRPM) + if err != nil { + t.Fatalf("fixture: %v", err) + } + store := &fakeMetaStore{} + (&Provider{}).AfterUpload(context.Background(), "myrepo", "Packages/e2e-testpkg-1.0-1.noarch.rpm", + "sha256:deadbeef", fakeBlobReader{data: data}, store) + + m := store.inserted + if m == nil { + t.Fatal("no metadata inserted") + } + if m.Name != "e2e-testpkg" || m.Version != "1.0" || m.Release != "1" || m.Arch != "noarch" { + t.Errorf("unexpected metadata: %+v", m) + } + if m.RPMSize != int64(len(data)) { + t.Errorf("RPMSize = %d, want %d", m.RPMSize, len(data)) + } + if len(m.Provides) == 0 { + t.Error("expected the package to provide itself") + } +} + +func TestRPMServeRepodata(t *testing.T) { + p := &Provider{} + reader := fakeRPMReader{metas: []provider.RPMMetadata{{ + Name: "e2e-testpkg", Version: "1.0", Release: "1", Arch: "noarch", + Summary: "test & ", + ContentHash: "sha256:abc", + Requires: []provider.RPMDep{{Name: "libc", Flags: "GE", Version: "2.0"}}, + Provides: []provider.RPMDep{{Name: "e2e-testpkg"}}, + Files: []provider.RPMFile{{Path: "/usr/share/e2e/README", Type: "file"}}, + Changelogs: []provider.RPMChangelog{{Author: "e2e", Date: 1, Text: "init"}}, + }}} + + serve := func(path string) *httptest.ResponseRecorder { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/"+path, nil) + if !p.ServeLocalIndex(w, r, reader, "myrepo", path) { + t.Fatalf("ServeLocalIndex returned false for %q", path) + } + return w + } + + if w := serve("repodata/repomd.xml"); w.Code != 200 || !strings.Contains(w.Body.String(), " 404. + if w := serve("repodata/bogus"); w.Code != http.StatusNotFound { + t.Errorf("bogus repodata: code %d", w.Code) + } + // Non-repodata path -> not handled. + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/Packages/x.rpm", nil) + if p.ServeLocalIndex(w, r, reader, "myrepo", "Packages/x.rpm") { + t.Error("expected ServeLocalIndex false for non-repodata path") + } +} + +func TestRPMPrimaryXMLContents(t *testing.T) { + // Exercise xmlEscape and dependency entry writing through the gzip'd XML. + metas := []provider.RPMMetadata{{ + Name: "pkg", Version: "1", Release: "1", Arch: "x86_64", Summary: "a & b", + Requires: []provider.RPMDep{{Name: "dep", Flags: "EQ", Version: "1.0", Epoch: "0"}}, + }} + gz := generatePrimaryXMLGZ(metas) + zr, err := gzip.NewReader(bytes.NewReader(gz)) + if err != nil { + t.Fatal(err) + } + out, _ := io.ReadAll(zr) + s := string(out) + if !strings.Contains(s, "a & b") { + t.Errorf("summary not xml-escaped: %s", s) + } + if !strings.Contains(s, "pkg") { + t.Errorf("package name missing: %s", s) + } +} + +func TestGenerateLocalIndexUnsupported(t *testing.T) { + if _, err := (&Provider{}).GenerateLocalIndex(context.Background(), fakeRPMReader{}, "r", "simple/"); err == nil { + t.Error("expected unsupported error") + } +}