206 lines
7.6 KiB
Go
206 lines
7.6 KiB
Go
package virtual
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/proxy"
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
func puppetRel(slug, version string) map[string]any {
|
|
return map[string]any{
|
|
"slug": slug + "-" + version, "version": version,
|
|
"file_uri": "/v3/files/" + slug + "-" + version + ".tar.gz",
|
|
"module": map[string]any{"slug": slug, "uri": "/v3/modules/" + slug},
|
|
}
|
|
}
|
|
|
|
func versionsOf(rs []map[string]any) string {
|
|
var out []string
|
|
for _, r := range rs {
|
|
out = append(out, r["version"].(string))
|
|
}
|
|
return strings.Join(out, ",")
|
|
}
|
|
|
|
func TestMergePuppetReleases(t *testing.T) {
|
|
absolute := puppetRel("unkin-hello", "0.3.0")
|
|
absolute["file_uri"] = "https://forge.example/api/v1/remote/forge/v3/files/unkin-hello-0.3.0.tar.gz"
|
|
internalDup := puppetRel("unkin-hello", "0.2.0")
|
|
internalDup["tag"] = "internal"
|
|
wrongFile := puppetRel("unkin-hello", "9.0.0")
|
|
wrongFile["file_uri"] = "/v3/files/unkin-other-9.0.0.tar.gz"
|
|
|
|
d, err := MergePuppetReleases("unkin-hello", []PuppetMember{
|
|
{RemoteName: "internal", Found: true, Releases: []map[string]any{puppetRel("unkin-hello", "0.1.0"), internalDup}},
|
|
{RemoteName: "missing"},
|
|
{RemoteName: "forge", Found: true, Releases: []map[string]any{absolute, puppetRel("unkin-hello", "0.2.0"), puppetRel("unkin-hello", "0.10.0"), wrongFile, puppetRel("unkin-other", "1.0.0")}},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := versionsOf(d.Releases); got != "0.10.0,0.3.0,0.2.0,0.1.0" {
|
|
t.Fatalf("merged = %s, want newest semver first without foreign or misnamed releases", got)
|
|
}
|
|
for v, owner := range map[string]string{"0.1.0": "internal", "0.2.0": "internal", "0.3.0": "forge", "0.10.0": "forge"} {
|
|
if d.Owners[v] != owner {
|
|
t.Errorf("owner of %s = %q, want %q", v, d.Owners[v], owner)
|
|
}
|
|
}
|
|
if d.Releases[2]["tag"] != "internal" {
|
|
t.Errorf("duplicate 0.2.0 not taken from the first member: %v", d.Releases[2])
|
|
}
|
|
if d.Releases[1]["file_uri"] != "/v3/files/unkin-hello-0.3.0.tar.gz" {
|
|
t.Errorf("file_uri = %v, want routed via the virtual", d.Releases[1]["file_uri"])
|
|
}
|
|
|
|
mod, err := PuppetModule(d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mod["slug"] != "unkin-hello" || mod["current_release"].(map[string]any)["version"] != "0.10.0" || len(mod["releases"].([]map[string]any)) != 4 {
|
|
t.Fatalf("module = %v", mod)
|
|
}
|
|
|
|
if _, err := MergePuppetReleases("unkin-hello", []PuppetMember{{RemoteName: "a"}, {RemoteName: "b"}}); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("no member has it: want ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
// forgePages answers a member's /v3/releases like the Forge, two per page.
|
|
func forgePages(slug string, versions []string, p string) ([]byte, error) {
|
|
_, raw, _ := strings.Cut(p, "?")
|
|
q, _ := url.ParseQuery(raw)
|
|
offset, _ := strconv.Atoi(q.Get("offset"))
|
|
end := min(offset+2, len(versions))
|
|
var results []map[string]any
|
|
for _, v := range versions[min(offset, end):end] {
|
|
results = append(results, puppetRel(slug, v))
|
|
}
|
|
return json.Marshal(map[string]any{"pagination": map[string]int{"total": len(versions)}, "results": results})
|
|
}
|
|
|
|
func puppetEngine(doc func(remote models.Remote, path string) ([]byte, error)) *Engine {
|
|
return &Engine{
|
|
mergeTTL: time.Minute,
|
|
getRemote: func(_ context.Context, name string) (*models.Remote, error) {
|
|
rt := models.RepoTypeRemote
|
|
if name == "internal" {
|
|
rt = models.RepoTypeLocal
|
|
}
|
|
return &models.Remote{Name: name, RepoType: rt}, nil
|
|
},
|
|
puppetDoc: func(_ context.Context, remote models.Remote, path string) ([]byte, error) { return doc(remote, path) },
|
|
}
|
|
}
|
|
|
|
func TestPuppetReleasesFollowsMemberPages(t *testing.T) {
|
|
var mu sync.Mutex
|
|
var asked []string
|
|
e := puppetEngine(func(remote models.Remote, p string) ([]byte, error) {
|
|
mu.Lock()
|
|
asked = append(asked, remote.Name+" "+p)
|
|
mu.Unlock()
|
|
switch remote.Name {
|
|
case "internal":
|
|
return forgePages("puppetlabs-stdlib", []string{"9.6.0"}, p)
|
|
case "gone":
|
|
return nil, &proxy.ProxyError{Status: 404}
|
|
}
|
|
return forgePages("puppetlabs-stdlib", []string{"9.6.0", "9.5.0", "9.4.0", "8.0.0", "7.0.0"}, p)
|
|
})
|
|
virt := models.Virtual{Name: "puppet", PackageType: models.PackagePuppet, Members: []string{"internal", "gone", "forge"}}
|
|
d, err := e.PuppetReleases(context.Background(), virt, "puppetlabs-stdlib")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if versionsOf(d.Releases) != "9.6.0,9.5.0,9.4.0,8.0.0,7.0.0" || d.Owners["9.6.0"] != "internal" || d.Owners["7.0.0"] != "forge" {
|
|
t.Fatalf("merged = %s %v", versionsOf(d.Releases), d.Owners)
|
|
}
|
|
if len(asked) != 5 {
|
|
t.Fatalf("want 1 internal + 1 gone + 3 forge pages, asked %v", asked)
|
|
}
|
|
for _, a := range asked {
|
|
if strings.HasPrefix(a, "forge ") && !strings.Contains(a, "module=puppetlabs-stdlib") {
|
|
t.Errorf("member page without module filter: %q", a)
|
|
}
|
|
}
|
|
if _, err := e.PuppetReleases(context.Background(), virt, "puppetlabs-stdlib"); err != nil || len(asked) != 5 {
|
|
t.Fatalf("merge not cached: %v, %d fetches", err, len(asked))
|
|
}
|
|
}
|
|
|
|
func TestPuppetReleasesFailsClosed(t *testing.T) {
|
|
for name, doc := range map[string]func(models.Remote, string) ([]byte, error){
|
|
"member error": func(r models.Remote, p string) ([]byte, error) {
|
|
if r.Name == "forge" {
|
|
return nil, &proxy.ProxyError{Status: 503}
|
|
}
|
|
return forgePages("unkin-hello", []string{"0.1.0"}, p)
|
|
},
|
|
"short member list": func(models.Remote, string) ([]byte, error) {
|
|
return []byte(`{"pagination":{"total":3},"results":[]}`), nil
|
|
},
|
|
} {
|
|
e := puppetEngine(doc)
|
|
virt := models.Virtual{Name: "puppet", PackageType: models.PackagePuppet, Members: []string{"internal", "forge"}}
|
|
if _, err := e.PuppetReleases(context.Background(), virt, "unkin-hello"); err == nil || errors.Is(err, ErrNotFound) {
|
|
t.Errorf("%s: want a non-404 error, got %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPuppetRedirect(t *testing.T) {
|
|
e := puppetEngine(func(r models.Remote, p string) ([]byte, error) {
|
|
if r.Name == "internal" {
|
|
return forgePages("unkin-hello", []string{"0.1.0"}, p)
|
|
}
|
|
return forgePages("unkin-hello", []string{"0.2.0", "0.1.0"}, p)
|
|
})
|
|
virt := models.Virtual{Name: "puppet", PackageType: models.PackagePuppet, Members: []string{"internal", "forge"}}
|
|
for p, want := range map[string]string{
|
|
"v3/files/unkin-hello-0.1.0.tar.gz": "https://a.example/api/v1/local/internal/v3/files/unkin-hello-0.1.0.tar.gz",
|
|
"v3/releases/unkin-hello-0.2.0": "https://a.example/api/v1/remote/forge/v3/releases/unkin-hello-0.2.0",
|
|
} {
|
|
if loc, err := e.PuppetRedirect(context.Background(), virt, p, "https://a.example"); err != nil || loc != want {
|
|
t.Errorf("%s -> %q %v, want %q", p, loc, err, want)
|
|
}
|
|
}
|
|
for _, p := range []string{"v3/files/unkin-hello-0.3.0.tar.gz", "v3/files/unkin-hello-0.1.0.zip", "v3/releases/nope", "v3/users/unkin"} {
|
|
if _, err := e.PuppetRedirect(context.Background(), virt, p, "https://a.example"); !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("%s: want ErrNotFound, got %v", p, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPuppetReleasesCachesMissingModule(t *testing.T) {
|
|
var mu sync.Mutex
|
|
asked := map[string]int{}
|
|
e := puppetEngine(func(r models.Remote, p string) ([]byte, error) {
|
|
mu.Lock()
|
|
asked[r.Name]++
|
|
mu.Unlock()
|
|
if r.Name == "internal" {
|
|
return nil, &proxy.ProxyError{Status: 404}
|
|
}
|
|
return forgePages("unkin-nope", nil, p)
|
|
})
|
|
virt := models.Virtual{Name: "puppet", PackageType: models.PackagePuppet, Members: []string{"internal", "forge"}}
|
|
for range 2 {
|
|
if _, err := e.PuppetReleases(context.Background(), virt, "unkin-nope"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("want ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
if asked["internal"] != 1 || asked["forge"] != 1 {
|
|
t.Fatalf("a module no member has must be cached, asked %v", asked)
|
|
}
|
|
}
|