test: raise core-package unit coverage to 90% (#98)
Raises statement coverage of the core packages (all of `internal/` except the interactive `tui/`, plus `pkg/`) from **8.7% to 90.1%**. ## Approach - **Pure-go unit tests** for all providers, virtual mergers, classifier, config, auth, models, and the API client (httptest). - **Testcontainers-backed** tests (new `internal/testsupport` helper: Postgres/Redis/MinIO, Ryuk disabled) for database, storage, cache, the proxy engine, the GC, and a full-stack `server` test that drives the whole HTTP API. These `t.Skip` when Docker is absent so `go test` still runs locally without it. ## Measuring ``` go test -coverpkg=./internal/...,./pkg/... -coverprofile=cover.out ./internal/... ./pkg/... grep -v /internal/tui/ cover.out | go tool cover -func=/dev/stdin | tail -1 # 90.1% ``` Run with `-p 1` (containers are heavy). ## Notes - The interactive `tui/` package and `cmd/main` are excluded from the target per the agreed scope. - Some defensive error branches are covered via fault injection (closed DB pool, killing MinIO mid-upload). Reviewed-on: #98 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
This commit was merged in pull request #98.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package alpine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
if (&Provider{}).Type() != models.PackageAlpine {
|
||||
t.Fatal("wrong type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassify(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.Classify("v3.19/main/x86_64/APKINDEX.tar.gz") != provider.Mutable {
|
||||
t.Error("APKINDEX should be mutable")
|
||||
}
|
||||
if p.Classify("v3.19/main/x86_64/curl-8.0-r0.apk") != provider.Immutable {
|
||||
t.Error("apk should be immutable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
p := &Provider{}
|
||||
cases := map[string]string{
|
||||
"pkg.apk": "application/vnd.android.package-archive",
|
||||
"APKINDEX.tar.gz": "application/gzip",
|
||||
"something.random": "application/octet-stream",
|
||||
}
|
||||
for path, want := range cases {
|
||||
if got := p.ContentType(path); got != want {
|
||||
t.Errorf("ContentType(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamURL(t *testing.T) {
|
||||
p := &Provider{}
|
||||
got := p.UpstreamURL(models.Remote{BaseURL: "https://dl-cdn.alpinelinux.org/alpine/"}, "/v3.19/main/x86_64/curl.apk")
|
||||
if got != "https://dl-cdn.alpinelinux.org/alpine/v3.19/main/x86_64/curl.apk" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteResponse(t *testing.T) {
|
||||
if out, err := (&Provider{}).RewriteResponse([]byte("x"), models.Remote{}, "http://proxy"); out != nil || err != nil {
|
||||
t.Error("alpine never rewrites")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHeaders(t *testing.T) {
|
||||
h, _ := (&Provider{}).AuthHeaders(context.Background(), models.Remote{Username: "u", Password: "p"})
|
||||
if h.Get("Authorization") == "" {
|
||||
t.Error("expected auth header")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestDockerClassifyBranches(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.Classify("library/nginx/tags/list") != provider.Mutable {
|
||||
t.Error("tags/list should be mutable")
|
||||
}
|
||||
if p.Classify("library/nginx/manifests/latest") != provider.Mutable {
|
||||
t.Error("tag manifest should be mutable")
|
||||
}
|
||||
if p.Classify("library/nginx/manifests/sha256:abcdef") != provider.Immutable {
|
||||
t.Error("digest manifest should be immutable")
|
||||
}
|
||||
if p.Classify("library/nginx/blobs/sha256:abc") != provider.Immutable {
|
||||
t.Error("blob should be immutable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerContentType(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.ContentType("x/blobs/sha256:abc") != "application/octet-stream" {
|
||||
t.Error("blob content type")
|
||||
}
|
||||
if p.ContentType("x/manifests/latest") != "application/vnd.docker.distribution.manifest.v2+json" {
|
||||
t.Error("manifest content type")
|
||||
}
|
||||
if p.ContentType("x/tags/list") != "application/json" {
|
||||
t.Error("default content type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerRewriteAndAuth(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if out, err := p.RewriteResponse([]byte("x"), models.Remote{}, "http://p"); out != nil || err != nil {
|
||||
t.Error("docker never rewrites")
|
||||
}
|
||||
h, _ := p.AuthHeaders(context.Background(), models.Remote{Username: "u", Password: "p"})
|
||||
if h.Get("Authorization") == "" {
|
||||
t.Error("expected basic auth header")
|
||||
}
|
||||
h, _ = p.AuthHeaders(context.Background(), models.Remote{})
|
||||
if h.Get("Authorization") != "" {
|
||||
t.Error("no creds, no header")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestGenericRewriteResponse(t *testing.T) {
|
||||
if out, err := (&Provider{}).RewriteResponse([]byte("x"), models.Remote{}, "http://p"); out != nil || err != nil {
|
||||
t.Error("generic never rewrites")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package goproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestGoProxyURLAuthRewrite(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if got := p.UpstreamURL(models.Remote{BaseURL: "https://proxy.golang.org/"}, "/mod/@v/list"); got != "https://proxy.golang.org/mod/@v/list" {
|
||||
t.Errorf("upstream url %q", got)
|
||||
}
|
||||
if out, err := p.RewriteResponse([]byte("x"), models.Remote{}, "http://p"); out != nil || err != nil {
|
||||
t.Error("goproxy never rewrites")
|
||||
}
|
||||
if h, _ := p.AuthHeaders(context.Background(), models.Remote{Username: "u", Password: "p"}); h.Get("Authorization") == "" {
|
||||
t.Error("expected basic auth header")
|
||||
}
|
||||
if got := p.ContentType("mod/@v/v1.0.0.info"); got != "application/json" {
|
||||
t.Errorf("info content type %q", got)
|
||||
}
|
||||
if got := p.ContentType("mod/@v/v1.0.0.mod"); got != "text/plain" {
|
||||
t.Errorf("mod content type %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package helm
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHelmContentTypeBranches(t *testing.T) {
|
||||
p := &Provider{}
|
||||
for path, want := range map[string]string{
|
||||
"charts/x-1.0.0.tgz": "application/gzip",
|
||||
"x.tar.gz": "application/gzip",
|
||||
"index.yaml": "text/yaml",
|
||||
"x.yml": "text/yaml",
|
||||
"other": "application/octet-stream",
|
||||
} {
|
||||
if got := p.ContentType(path); got != want {
|
||||
t.Errorf("ContentType(%q)=%q want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package npm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
if (&Provider{}).Type() != models.PackageNPM {
|
||||
t.Fatal("wrong type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassify(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.Classify("pkg/-/pkg-1.0.0.tgz") != provider.Immutable {
|
||||
t.Error("tgz should be immutable")
|
||||
}
|
||||
if p.Classify("pkg") != provider.Mutable {
|
||||
t.Error("metadata should be mutable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.ContentType("pkg/-/pkg-1.0.0.tgz") != "application/gzip" {
|
||||
t.Error("tgz content type")
|
||||
}
|
||||
if p.ContentType("pkg") != "application/json" {
|
||||
t.Error("metadata content type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamURL(t *testing.T) {
|
||||
p := &Provider{}
|
||||
got := p.UpstreamURL(models.Remote{BaseURL: "https://registry.npmjs.org/"}, "/pkg")
|
||||
if got != "https://registry.npmjs.org/pkg" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteResponse(t *testing.T) {
|
||||
p := &Provider{}
|
||||
remote := models.Remote{Name: "npmjs", BaseURL: "https://registry.npmjs.org"}
|
||||
|
||||
if out, _ := p.RewriteResponse([]byte(`{"a":1}`), remote, ""); out != nil {
|
||||
t.Error("empty proxyBaseURL should be a no-op")
|
||||
}
|
||||
if out, _ := p.RewriteResponse([]byte("not json"), remote, "http://proxy"); out != nil {
|
||||
t.Error("invalid json should be a no-op")
|
||||
}
|
||||
body := []byte(`{"tarball":"https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz"}`)
|
||||
out, err := p.RewriteResponse(body, remote, "http://proxy")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out) != `{"tarball":"http://proxy/api/v1/remote/npmjs/pkg/-/pkg-1.0.0.tgz"}` {
|
||||
t.Errorf("rewrite: %s", out)
|
||||
}
|
||||
if out, _ := p.RewriteResponse([]byte(`{"x":"unrelated"}`), remote, "http://proxy"); out != nil {
|
||||
t.Error("no matching base URL should be a no-op")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHeaders(t *testing.T) {
|
||||
p := &Provider{}
|
||||
h, _ := p.AuthHeaders(context.Background(), models.Remote{Username: "u", Password: "pw"})
|
||||
if h.Get("Authorization") == "" {
|
||||
t.Error("expected auth header when credentials set")
|
||||
}
|
||||
h, _ = p.AuthHeaders(context.Background(), models.Remote{})
|
||||
if h.Get("Authorization") != "" {
|
||||
t.Error("expected no auth header without credentials")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package puppet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
if (&Provider{}).Type() != models.PackagePuppet {
|
||||
t.Fatal("wrong type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassify(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.Classify("v3/modules/puppetlabs-stdlib") != provider.Mutable {
|
||||
t.Error("modules should be mutable")
|
||||
}
|
||||
if p.Classify("v3/releases?module=x") != provider.Mutable {
|
||||
t.Error("releases should be mutable")
|
||||
}
|
||||
if p.Classify("v3/files/puppetlabs-stdlib-1.0.0.tar.gz") != provider.Immutable {
|
||||
t.Error("files should be immutable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.ContentType("x/mod-1.0.0.tar.gz") != "application/gzip" {
|
||||
t.Error("tar.gz")
|
||||
}
|
||||
if p.ContentType("v3/modules/x") != "application/json" {
|
||||
t.Error("v3 json")
|
||||
}
|
||||
if p.ContentType("other") != "application/octet-stream" {
|
||||
t.Error("default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamURL(t *testing.T) {
|
||||
got := (&Provider{}).UpstreamURL(models.Remote{BaseURL: "https://forgeapi.puppet.com/"}, "/v3/modules/x")
|
||||
if got != "https://forgeapi.puppet.com/v3/modules/x" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteResponse(t *testing.T) {
|
||||
p := &Provider{}
|
||||
remote := models.Remote{Name: "forge", BaseURL: "https://forgeapi.puppet.com"}
|
||||
|
||||
if out, _ := p.RewriteResponse([]byte("x"), remote, ""); out != nil {
|
||||
t.Error("empty proxyBaseURL is a no-op")
|
||||
}
|
||||
|
||||
body := []byte(`{"file_uri":"/v3/files/mod.tar.gz","home":"https://forgeapi.puppet.com/x"}`)
|
||||
out, err := p.RewriteResponse(body, remote, "http://proxy")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(out)
|
||||
if !strings.Contains(s, "http://proxy/api/v1/remote/forge/v3/files/mod.tar.gz") {
|
||||
t.Errorf("v3/files not rewritten: %s", s)
|
||||
}
|
||||
if !strings.Contains(s, "http://proxy/api/v1/remote/forge/x") {
|
||||
t.Errorf("base URL not rewritten: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHeaders(t *testing.T) {
|
||||
h, _ := (&Provider{}).AuthHeaders(context.Background(), models.Remote{})
|
||||
if h.Get("Authorization") != "" {
|
||||
t.Error("no credentials, no header")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package pypi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
// fakeFileStore is an in-memory provider.FileStore for exercising local index
|
||||
// generation without a database.
|
||||
type fakeFileStore struct {
|
||||
packages []string
|
||||
files map[string][]provider.FileEntry
|
||||
}
|
||||
|
||||
func (f *fakeFileStore) ListPackages(_ context.Context, _ string) ([]string, error) {
|
||||
return f.packages, nil
|
||||
}
|
||||
|
||||
func (f *fakeFileStore) ListFilesByPrefix(_ context.Context, _, prefix string) ([]provider.FileEntry, error) {
|
||||
return f.files[prefix], nil
|
||||
}
|
||||
|
||||
func TestTypeClassifyContentType(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if p.Type() != models.PackagePyPI {
|
||||
t.Fatal("type")
|
||||
}
|
||||
if p.Classify("simple/foo/") != provider.Mutable {
|
||||
t.Error("simple index should be mutable")
|
||||
}
|
||||
if p.Classify("packages/foo-1.0.whl") != provider.Immutable {
|
||||
t.Error("wheel should be immutable")
|
||||
}
|
||||
cases := map[string]string{
|
||||
"foo-1.0-py3-none-any.whl": "application/zip",
|
||||
"foo-1.0.zip": "application/zip",
|
||||
"foo-1.0.tar.gz": "application/gzip",
|
||||
"simple/foo/": "text/html",
|
||||
"weird": "application/octet-stream",
|
||||
}
|
||||
for path, want := range cases {
|
||||
if got := p.ContentType(path); got != want {
|
||||
t.Errorf("ContentType(%q)=%q want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamURL(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if got := p.UpstreamURL(models.Remote{BaseURL: "https://files.example.com"}, "packages/foo.whl"); got != "https://files.example.com/packages/foo.whl" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
if got := p.UpstreamURL(models.Remote{BaseURL: "https://x"}, "simple/foo/"); got != "https://pypi.org/simple/foo/" {
|
||||
t.Errorf("simple should hit pypi.org, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUpload(t *testing.T) {
|
||||
p := &Provider{}
|
||||
sp, ct, err := p.ValidateUpload("numpy-1.26.0-cp311-cp311-linux_x86_64.whl")
|
||||
if err != nil || sp != "numpy/numpy-1.26.0-cp311-cp311-linux_x86_64.whl" || ct != "application/zip" {
|
||||
t.Errorf("wheel: sp=%q ct=%q err=%v", sp, ct, err)
|
||||
}
|
||||
sp, ct, err = p.ValidateUpload("requests-2.31.0.tar.gz")
|
||||
if err != nil || sp != "requests/requests-2.31.0.tar.gz" || ct != "application/gzip" {
|
||||
t.Errorf("sdist: sp=%q ct=%q err=%v", sp, ct, err)
|
||||
}
|
||||
if _, _, err := p.ValidateUpload("not-a-package.txt"); err == nil {
|
||||
t.Error("expected error for bad extension")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageNameParsing(t *testing.T) {
|
||||
if got := packageFromWheel("Foo_Bar-1.0-py3-none-any.whl"); got != "foo-bar" {
|
||||
t.Errorf("wheel name = %q", got)
|
||||
}
|
||||
if got := packageFromWheel("noseparator.whl"); got != "" {
|
||||
t.Errorf("expected empty for unparseable wheel, got %q", got)
|
||||
}
|
||||
if got := packageFromSdist("My.Pkg-2.0.tar.gz"); got != "my-pkg" {
|
||||
t.Errorf("sdist name = %q", got)
|
||||
}
|
||||
if got := packageFromSdist("noseparator.zip"); got != "" {
|
||||
t.Errorf("expected empty, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadResponse(t *testing.T) {
|
||||
resp := (&Provider{}).UploadResponse("foo/foo-1.0.whl", "sha256:abc", 123)
|
||||
if resp["filename"] != "foo-1.0.whl" || resp["package"] != "foo" || resp["content_hash"] != "sha256:abc" {
|
||||
t.Errorf("unexpected upload response: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteResponse(t *testing.T) {
|
||||
p := &Provider{}
|
||||
if out, _ := p.RewriteResponse([]byte("x"), models.Remote{Name: "pypi"}, ""); out != nil {
|
||||
t.Error("empty proxyBaseURL is a no-op")
|
||||
}
|
||||
body := []byte(`<a href="https://files.pythonhosted.org/packages/foo.whl">foo.whl</a>`)
|
||||
out, err := p.RewriteResponse(body, models.Remote{Name: "pypi"}, "http://proxy")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(out), "http://proxy/api/v1/remote/pypi/") {
|
||||
t.Errorf("not rewritten: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateLocalIndex(t *testing.T) {
|
||||
p := &Provider{}
|
||||
fs := &fakeFileStore{
|
||||
packages: []string{"foo", "bar"},
|
||||
files: map[string][]provider.FileEntry{
|
||||
"foo/": {{FilePath: "foo/foo-1.0-py3-none-any.whl", ContentHash: "sha256:aaa"}},
|
||||
},
|
||||
}
|
||||
list, err := p.GenerateLocalIndex(context.Background(), fs, "local", "simple/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(list), "foo") || !strings.Contains(string(list), "bar") {
|
||||
t.Errorf("package list missing entries: %s", list)
|
||||
}
|
||||
|
||||
files, err := p.GenerateLocalIndex(context.Background(), fs, "local", "simple/foo/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(files), "foo-1.0-py3-none-any.whl") {
|
||||
t.Errorf("file list missing wheel: %s", files)
|
||||
}
|
||||
|
||||
if _, err := p.GenerateLocalIndex(context.Background(), fs, "local", "notsimple"); err == nil {
|
||||
t.Error("expected error for non-simple path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeLocalIndexHTTP(t *testing.T) {
|
||||
p := &Provider{}
|
||||
fs := &fakeFileStore{
|
||||
packages: []string{"foo"},
|
||||
files: map[string][]provider.FileEntry{
|
||||
"foo/": {{FilePath: "foo/foo-1.0-py3-none-any.whl", ContentHash: "sha256:aaa"}},
|
||||
},
|
||||
}
|
||||
serve := func(path string) (*httptest.ResponseRecorder, bool) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/"+path, nil)
|
||||
handled := p.ServeLocalIndex(w, r, fs, "local", path)
|
||||
return w, handled
|
||||
}
|
||||
|
||||
if w, ok := serve("simple/"); !ok || w.Code != 200 || !strings.Contains(w.Body.String(), "foo") {
|
||||
t.Errorf("simple index: handled=%v code=%d body=%s", ok, w.Code, w.Body.String())
|
||||
}
|
||||
if w, ok := serve("simple/foo/"); !ok || w.Code != 200 || !strings.Contains(w.Body.String(), "foo-1.0-py3-none-any.whl") {
|
||||
t.Errorf("package index: handled=%v code=%d body=%s", ok, w.Code, w.Body.String())
|
||||
}
|
||||
// Non-simple paths are not handled.
|
||||
if _, ok := serve("packages/foo.whl"); ok {
|
||||
t.Error("non-index path should not be handled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHeaders(t *testing.T) {
|
||||
h, _ := (&Provider{}).AuthHeaders(context.Background(), models.Remote{Username: "u", Password: "p"})
|
||||
if h.Get("Authorization") == "" {
|
||||
t.Error("expected auth header")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package rpm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
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 := testsupport.MinimalRPM("e2e-testpkg", "1.0", "1", "noarch")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
type errBlobReader struct{}
|
||||
|
||||
func (errBlobReader) Download(_ context.Context, _ string) (io.ReadCloser, int64, error) {
|
||||
return nil, 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
func TestRPMAfterUploadErrors(t *testing.T) {
|
||||
// Download failure: no metadata inserted, no panic.
|
||||
store := &fakeMetaStore{}
|
||||
(&Provider{}).AfterUpload(context.Background(), "r", "p", "sha256:x", errBlobReader{}, store)
|
||||
if store.inserted != nil {
|
||||
t.Error("no metadata should be inserted on download error")
|
||||
}
|
||||
// Parse failure: garbage bytes are not a valid RPM.
|
||||
store2 := &fakeMetaStore{}
|
||||
(&Provider{}).AfterUpload(context.Background(), "r", "p", "sha256:x", fakeBlobReader{data: []byte("not an rpm")}, store2)
|
||||
if store2.inserted != nil {
|
||||
t.Error("no metadata should be inserted on parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPMServeRepodata(t *testing.T) {
|
||||
p := &Provider{}
|
||||
reader := fakeRPMReader{metas: []provider.RPMMetadata{{
|
||||
Name: "e2e-testpkg", Version: "1.0", Release: "1", Arch: "noarch",
|
||||
Summary: "test & <special>",
|
||||
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(), "<repomd") {
|
||||
t.Errorf("repomd: code=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
for _, name := range []string{"repodata/h-primary.xml.gz", "repodata/h-filelists.xml.gz", "repodata/h-other.xml.gz"} {
|
||||
w := serve(name)
|
||||
if w.Code != 200 {
|
||||
t.Errorf("%s: code %d", name, w.Code)
|
||||
}
|
||||
if _, err := gzip.NewReader(bytes.NewReader(w.Body.Bytes())); err != nil {
|
||||
t.Errorf("%s: not gzip: %v", name, err)
|
||||
}
|
||||
}
|
||||
// Unknown repodata file -> 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")
|
||||
}
|
||||
}
|
||||
|
||||
type errRPMReader struct{}
|
||||
|
||||
func (errRPMReader) ListRPMMetadataEntries(context.Context, string) ([]provider.RPMMetadata, error) {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
func (errRPMReader) ListFilesByPrefix(context.Context, string, string) ([]provider.FileEntry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (errRPMReader) ListPackages(context.Context, string) ([]string, error) { return nil, nil }
|
||||
|
||||
func TestRPMServeMetadataError(t *testing.T) {
|
||||
p := &Provider{}
|
||||
for _, path := range []string{"repodata/repomd.xml", "repodata/h-primary.xml.gz", "repodata/h-filelists.xml.gz", "repodata/h-other.xml.gz"} {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/"+path, nil)
|
||||
p.ServeLocalIndex(w, r, errRPMReader{}, "repo", path)
|
||||
if w.Code != 500 {
|
||||
t.Errorf("%s with failing reader = %d, want 500", path, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPMFullMetadataXML(t *testing.T) {
|
||||
// A fully-populated entry exercises every optional-field branch in the
|
||||
// primary/filelists/other XML generators.
|
||||
metas := []provider.RPMMetadata{{
|
||||
Name: "full", Epoch: 1, Version: "2.0", Release: "3", Arch: "x86_64",
|
||||
Summary: "s", Description: "d", License: "MIT", Vendor: "acme",
|
||||
Group: "System", BuildHost: "build.example.com", SourceRPM: "full-2.0.src.rpm",
|
||||
URL: "https://example.com", Packager: "pkgr", ContentHash: "sha256:abc",
|
||||
RPMSize: 100, InstalledSize: 200,
|
||||
Requires: []provider.RPMDep{{Name: "libc", Flags: "GE", Epoch: "0", Version: "2.0", Release: "1"}},
|
||||
Provides: []provider.RPMDep{{Name: "full", Flags: "EQ", Version: "2.0"}},
|
||||
Files: []provider.RPMFile{{Path: "/usr/bin/full", Type: "file"}, {Path: "/etc/full", Type: "dir"}},
|
||||
Changelogs: []provider.RPMChangelog{{Author: "a", Date: 100, Text: "changed"}},
|
||||
}}
|
||||
for _, gen := range []func([]provider.RPMMetadata) []byte{generatePrimaryXMLGZ, generateFilelistsXMLGZ, generateOtherXMLGZ} {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(gen(metas)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.ReadAll(zr); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, "<name>pkg</name>") {
|
||||
t.Errorf("package name missing: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPMContentTypeAndHelpers(t *testing.T) {
|
||||
p := &Provider{}
|
||||
for path, want := range map[string]string{
|
||||
"x.rpm": "application/x-rpm",
|
||||
"repodata/repomd.xml": "application/xml",
|
||||
"repodata/h-primary.xml.gz": "application/xml",
|
||||
"repodata/h-primary.xml.xz": "application/xml",
|
||||
"Packages/other": "application/octet-stream",
|
||||
} {
|
||||
if got := p.ContentType(path); got != want {
|
||||
t.Errorf("ContentType(%q)=%q want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
for flag, want := range map[int]string{
|
||||
0x08 | 0x04: "GE",
|
||||
0x02 | 0x04: "LE",
|
||||
0x08: "GT",
|
||||
0x02: "LT",
|
||||
0x04: "EQ",
|
||||
0x00: "",
|
||||
} {
|
||||
if got := rpmFlagString(flag); got != want {
|
||||
t.Errorf("rpmFlagString(%d)=%q want %q", flag, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if firstGroup(nil) != "Unspecified" {
|
||||
t.Error("empty groups should be Unspecified")
|
||||
}
|
||||
if firstGroup([]string{"System", "Base"}) != "System" {
|
||||
t.Error("firstGroup should return the first")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateLocalIndexUnsupported(t *testing.T) {
|
||||
if _, err := (&Provider{}).GenerateLocalIndex(context.Background(), fakeRPMReader{}, "r", "simple/"); err == nil {
|
||||
t.Error("expected unsupported error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user