Skip to content
quarry Artifact [4b63287c28]
Login

Artifact [4b63287c28]

Artifact 4b63287c287edfd93d26d3a2b7de4ea63ced2c0199b9e94ffb9866a31c7d95e6:


package api

import (
	"errors"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/ops"
)

func (g uvRig) releases(t *testing.T) releaseListOut {
	t.Helper()
	out, err := g.r.Invoke(as("reader", "o"), "release.list", releaseListIn{Repo: "test"})
	if err != nil {
		t.Fatalf("FAIL: release.list: %v", err)
	}
	return out.(releaseListOut)
}

func releaseNames(l releaseListOut) []string {
	var names []string
	for _, r := range l.Releases {
		names = append(names, r.Name)
	}
	return names
}

// Folders that read as versions are ordered as versions, newest first,
// whatever order their files were written in; a release candidate sits
// below its release. Loose files under releases/ and files outside it
// make no group, and a removed file is not listed.
func TestReleasesGroupByFolderNewestVersionFirst(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	pub := as("pub", "y")
	for _, f := range []struct {
		name  string
		mtime int64
	}{
		{"releases/0.2.0/quarry-0.2.0.tar.gz", 100},
		{"releases/0.10.0/quarry-0.10.0.tar.gz", 50},
		{"releases/0.9.1/quarry-0.9.1.tar.gz", 200},
		{"releases/0.10.0-rc1/quarry.tar.gz", 300},
		{"releases/0.9.1/gone.bin", 10},
		{"releases/README", 400},
		{"download.html", 500},
	} {
		if _, err := g.put(pub, f.name, f.mtime, f.name); err != nil {
			t.Fatalf("FAIL: put %s: %v", f.name, err)
		}
	}
	if _, err := g.r.Invoke(pub, "uv.rm", uvRmIn{Repo: "test", Name: "releases/0.9.1/gone.bin", Mtime: 20}); err != nil {
		t.Fatal(err)
	}
	l := g.releases(t)
	if got := strings.Join(releaseNames(l), " "); got != "0.10.0 0.10.0-rc1 0.9.1 0.2.0" {
		t.Errorf("FAIL: order = %s", got)
	}
	for i, r := range l.Releases {
		if r.Newest != (i == 0) {
			t.Errorf("FAIL: %s newest = %v", r.Name, r.Newest)
		}
		for _, f := range r.Files {
			if f.Deleted || f.Name == "releases/0.9.1/gone.bin" {
				t.Errorf("FAIL: %s lists the removed %s", r.Name, f.Name)
			}
		}
	}
	if len(l.Releases) == 4 && (len(l.Releases[2].Files) != 1 || l.Releases[2].Mtime != 200) {
		t.Errorf("FAIL: 0.9.1 = %+v", l.Releases[2])
	}
	t.Log("ok: versions order newest first, one newest, removed and loose files left out")
}

// Folder names that are not all versions fall back to the newest file
// in each group.
func TestReleasesFallBackToDate(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	pub := as("pub", "y")
	for name, mtime := range map[string]int64{
		"releases/nightly/a.bin": 10, "releases/0.1.0/a.bin": 5,
		"releases/beta/a.bin": 2, "releases/beta/b.bin": 20,
	} {
		if _, err := g.put(pub, name, mtime, name); err != nil {
			t.Fatal(err)
		}
	}
	if got := strings.Join(releaseNames(g.releases(t)), " "); got != "beta nightly 0.1.0" {
		t.Errorf("FAIL: order = %s", got)
	}
	t.Log("ok: unversioned folder names order by their newest file")
}

const zitManifest = `{
  "artifacts": [
    {"binary_sha256": "bb", "bytes": 1234, "name": "zit-0.4.0-linux-x86_64.tar.gz",
     "qualification": "passed", "sha256": "aa11", "target": "linux-x86_64"},
    {"binary_sha256": "cc", "bytes": 99, "name": "zit-0.4.0-macos-aarch64.tar.gz",
     "qualification": "passed", "sha256": "bb22", "target": "macos-aarch64"}
  ],
  "omitted_targets": ["windows-x86_64", {"target": "freebsd", "reason": "no runner"}],
  "qualification": {"macos-aarch64": ["unit", "e2e"], "linux-x86_64": ["unit", "dst seed 1 x1000"]},
  "qualification_source": {"bytes": 1, "files": 2, "name": "src", "sha256": "x", "source_tree_sha256": "y"},
  "release": {"date": "2026-09-30", "fossil_check_in": "abcdef0123456789", "fossil_tag": "version-0.4.0", "version": "0.4.0"},
  "schema_version": 1,
  "source": {"bytes": 555, "fossil_check_in": "abcdef0123456789", "name": "zit-0.4.0-source.tar.gz",
             "sha256": "dd44", "target": "source"}
}`

// A group holding release-manifest.json is described by it: zit's
// schema, read through the bucket. Without one, SHA256SUMS is named; a
// manifest that does not parse says so and leaves the files listed.
func TestReleaseManifestIsRead(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	pub := as("pub", "y")
	for name, body := range map[string]string{
		"releases/0.4.0/release-manifest.json":         zitManifest,
		"releases/0.4.0/zit-0.4.0-linux-x86_64.tar.gz": "bin",
		"releases/0.4.0/SHA256SUMS":                    "aa11  zit-0.4.0-linux-x86_64.tar.gz\n",
		"releases/0.3.0/zit-0.3.0-linux-x86_64.tar.gz": "old",
		"releases/0.3.0/SHA256SUMS":                    "ff  zit-0.3.0-linux-x86_64.tar.gz\n",
		"releases/0.2.0/release-manifest.json":         "{not json",
		"releases/0.2.0/zit-0.2.0-linux-x86_64.tar.gz": "older",
	} {
		if _, err := g.put(pub, name, 1, body); err != nil {
			t.Fatal(err)
		}
	}
	l := g.releases(t)
	if len(l.Releases) != 3 {
		t.Fatalf("FAIL: groups = %v", releaseNames(l))
	}
	m := l.Releases[0].Manifest
	if m == nil {
		t.Fatalf("FAIL: 0.4.0 has no manifest: %+v", l.Releases[0])
	}
	if m.Version != "0.4.0" || m.Date != "2026-09-30" || m.Checkin != "abcdef0123456789" || m.Tag != "version-0.4.0" {
		t.Errorf("FAIL: release fields = %+v", m)
	}
	if len(m.Artifacts) != 2 || m.Artifacts[0].Target != "linux-x86_64" || m.Artifacts[0].Size != 1234 ||
		m.Artifacts[0].SHA256 != "aa11" || m.Artifacts[0].Qualification != "passed" ||
		m.Artifacts[0].File != "releases/0.4.0/zit-0.4.0-linux-x86_64.tar.gz" || m.Artifacts[1].File != "" {
		t.Errorf("FAIL: artifacts = %+v", m.Artifacts)
	}
	if m.Source == nil || m.Source.SHA256 != "dd44" || m.Source.Size != 555 {
		t.Errorf("FAIL: source = %+v", m.Source)
	}
	if len(m.Omitted) != 2 || m.Omitted[0] != (releaseOmitted{Target: "windows-x86_64"}) ||
		m.Omitted[1] != (releaseOmitted{Target: "freebsd", Reason: "no runner"}) {
		t.Errorf("FAIL: omitted = %q", m.Omitted)
	}
	if len(m.Qualification) != 2 || m.Qualification[0].Target != "linux-x86_64" ||
		strings.Join(m.Qualification[0].Notes, ",") != "unit,dst seed 1 x1000" {
		t.Errorf("FAIL: qualification = %+v", m.Qualification)
	}
	if l.Releases[0].Sums != "releases/0.4.0/SHA256SUMS" {
		t.Errorf("FAIL: 0.4.0 sums = %q", l.Releases[0].Sums)
	}
	if r := l.Releases[1]; r.Manifest != nil || r.Sums != "releases/0.3.0/SHA256SUMS" || len(r.Files) != 2 {
		t.Errorf("FAIL: 0.3.0 = %+v", r)
	}
	if r := l.Releases[2]; r.Manifest != nil || r.ManifestErr == "" || len(r.Files) != 2 {
		t.Errorf("FAIL: 0.2.0 = %+v", r)
	}
	t.Log("ok: the manifest is read in zit's schema, SHA256SUMS named, a bad manifest said so")
}

// The changelog section is the one whose heading names the release as
// a word of its own, so 0.2 does not claim 0.2.0's notes.
func TestChangelogSectionNamesTheRelease(t *testing.T) {
	md := "# Changelog\n\n## Unreleased\n\nnext\n\n## 0.2.0 — 2026-10-01\n\nthe second\n\n### Fixed\n\nthings\n\n## v0.1.0\n\nthe first\n"
	heading, body, ok := changelogSection([]byte(md), "0.2.0")
	if !ok || heading != "0.2.0 — 2026-10-01" || !strings.Contains(body, "the second") ||
		!strings.Contains(body, "### Fixed") || strings.Contains(body, "the first") || strings.Contains(body, "## 0.2.0") {
		t.Errorf("FAIL: 0.2.0 = %q %q %v", heading, body, ok)
	}
	if heading, body, ok := changelogSection([]byte(md), "0.1.0"); !ok || heading != "v0.1.0" || !strings.Contains(body, "the first") {
		t.Errorf("FAIL: 0.1.0 = %q %q %v", heading, body, ok)
	}
	if _, _, ok := changelogSection([]byte(md), "0.2"); ok {
		t.Errorf("FAIL: 0.2 matched")
	}
	t.Log("ok: the section is the one whose heading names the release")
}

// The bar's question and the Files page's block come from the catalog
// alone: no bucket read, ordered as the releases page orders them.
func TestReleaseSummaryFromTheCatalog(t *testing.T) {
	g := newUVRig(t)
	d := Deps{Repos: NewRepos(map[string]*Repo{"test": {}}), Buckets: g.buckets, UV: g.db}
	if d.hasReleases("test") {
		t.Errorf("FAIL: releases before any file")
	}
	g.bind(1 << 20)
	pub := as("pub", "y")
	for name, mtime := range map[string]int64{"releases/0.9.0/a.bin": 50, "releases/0.10.0/a.bin": 10, "download.html": 90} {
		if _, err := g.put(pub, name, mtime, name); err != nil {
			t.Fatal(err)
		}
	}
	if !d.hasReleases("test") {
		t.Errorf("FAIL: no releases with two folders")
	}
	lists, gets := g.objects.lists, g.objects.gets
	out, err := g.r.Invoke(as("reader", "o"), "release.summary", releaseSummaryIn{Repo: "test"})
	if s := out.(releaseSummaryOut); err != nil || s.Count != 2 || s.Latest != "0.10.0" || s.Mtime != 10 {
		t.Errorf("FAIL: summary = %+v %v", s, err)
	}
	if g.objects.lists != lists || g.objects.gets != gets {
		t.Errorf("FAIL: the summary touched the bucket")
	}
	if _, err := g.r.Invoke(pub, "uv.rm", uvRmIn{Repo: "test", Name: "releases/0.10.0/a.bin", Mtime: 20}); err != nil {
		t.Fatal(err)
	}
	if out, _ := g.r.Invoke(as("reader", "o"), "release.summary", releaseSummaryIn{Repo: "test"}); out.(releaseSummaryOut).Latest != "0.9.0" {
		t.Errorf("FAIL: a removed release is still the latest")
	}
	if l := g.list(t); l.Limit != 1<<20 {
		t.Errorf("FAIL: uv.list limit = %d", l.Limit)
	}
	t.Log("ok: the bar and the block read the catalog alone")
}

// With a name, release.list answers that release alone, still marked
// newest when it is; a name that is a path is refused.
func TestReleaseListByName(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	pub := as("pub", "y")
	for _, n := range []string{"releases/1.0.0/a.bin", "releases/2.0.0/a.bin"} {
		if _, err := g.put(pub, n, 1, n); err != nil {
			t.Fatal(err)
		}
	}
	for name, newest := range map[string]bool{"2.0.0": true, "1.0.0": false} {
		out, err := g.r.Invoke(as("reader", "o"), "release.list", releaseListIn{Repo: "test", Name: name})
		if l := out.(releaseListOut); err != nil || len(l.Releases) != 1 || l.Releases[0].Name != name || l.Releases[0].Newest != newest {
			t.Errorf("FAIL: %s = %+v %v", name, out, err)
		}
	}
	if _, err := g.r.Invoke(as("reader", "o"), "release.list", releaseListIn{Repo: "test", Name: "3.0.0"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a missing release = %v", err)
	}
	for _, bad := range []string{".", "..", "1.0.0/a.bin", `a\b`} {
		if _, err := g.r.Invoke(as("reader", "o"), "release.list", releaseListIn{Repo: "test", Name: bad}); !errors.Is(err, ops.ErrInvalid) {
			t.Errorf("FAIL: name %q = %v", bad, err)
		}
	}
	t.Log("ok: one release by its folder name, paths refused")
}

// A manifest is read from the bucket once per content: the second page
// view is served from the cache, and a replaced manifest is read again.
func TestReleaseManifestIsCachedByContent(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	pub := as("pub", "y")
	if _, err := g.put(pub, "releases/9.0.0/release-manifest.json", 1, zitManifest); err != nil {
		t.Fatal(err)
	}
	g.releases(t)
	before := g.objects.gets
	if m := g.releases(t).Releases[0].Manifest; m == nil || g.objects.gets != before {
		t.Errorf("FAIL: second view read the bucket again (%d -> %d) or lost the manifest", before, g.objects.gets)
	}
	if _, err := g.put(pub, "releases/9.0.0/release-manifest.json", 2, strings.Replace(zitManifest, "2026-09-30", "2026-10-01", 1)); err != nil {
		t.Fatal(err)
	}
	if m := g.releases(t).Releases[0].Manifest; m == nil || m.Date != "2026-10-01" || g.objects.gets != before+1 {
		t.Errorf("FAIL: a replaced manifest was not read again: %+v gets %d", m, g.objects.gets)
	}
	t.Log("ok: one bucket read per manifest content")
}

// An oversize manifest is not fetched and says so; the files stay
// listed. A check-in that is not a hash is dropped, so nothing links it.
func TestReleaseManifestBoundsAndCheckin(t *testing.T) {
	g := newUVRig(t)
	g.bind(2 << 20)
	pub := as("pub", "y")
	big := `{"schema_version": 1, "pad": "` + strings.Repeat("x", releaseManifestMax) + `"}`
	if _, err := g.put(pub, "releases/2.0/release-manifest.json", 1, big); err != nil {
		t.Fatal(err)
	}
	if _, err := g.put(pub, "releases/2.0/a.bin", 1, "a"); err != nil {
		t.Fatal(err)
	}
	odd := strings.NewReplacer(`"fossil_check_in": "abcdef0123456789"`, `"fossil_check_in": "../../admin"`).Replace(zitManifest)
	if _, err := g.put(pub, "releases/1.0/release-manifest.json", 1, odd); err != nil {
		t.Fatal(err)
	}
	gets := g.objects.gets
	l := g.releases(t)
	if r := l.Releases[0]; r.Manifest != nil || !strings.Contains(r.ManifestErr, "bytes") || len(r.Files) != 2 {
		t.Errorf("FAIL: oversize = %+v", r)
	}
	if g.objects.gets != gets+1 {
		t.Errorf("FAIL: %d reads; the oversize manifest was fetched", g.objects.gets-gets)
	}
	if m := l.Releases[1].Manifest; m == nil || m.Checkin != "" {
		t.Errorf("FAIL: a non-hash check-in survived: %+v", m)
	}
	t.Log("ok: oversize manifests are refused unread, odd check-ins dropped")
}

// A catalog with release files but no bucket of its own has no
// releases anywhere: the bar, the Files box and the list agree.
func TestReleasesNeedTheBindingEverywhere(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	if _, err := g.put(as("pub", "y"), "releases/1.0/a.bin", 1, "a"); err != nil {
		t.Fatal(err)
	}
	delete(g.buckets.rows, "test")
	d := Deps{Repos: NewRepos(map[string]*Repo{"test": {}}), Buckets: g.buckets, UV: g.db}
	if d.hasReleases("test") {
		t.Errorf("FAIL: the bar offers releases with no bucket")
	}
	out, err := g.r.Invoke(as("reader", "o"), "release.summary", releaseSummaryIn{Repo: "test"})
	if s := out.(releaseSummaryOut); err != nil || s.Count != 0 || s.Latest != "" {
		t.Errorf("FAIL: summary with no bucket = %+v %v", s, err)
	}
	if l := g.releases(t); l.Bound || len(l.Releases) != 0 {
		t.Errorf("FAIL: list with no bucket = %+v", l)
	}
	t.Log("ok: no bucket, no releases, in all three")
}

// A manifest whose bytes are not the catalog's is remembered as such,
// not fetched again on every view.
func TestManifestMismatchIsCached(t *testing.T) {
	g := newUVRig(t)
	g.bind(1 << 20)
	if _, err := g.put(as("pub", "y"), "releases/7.7.7/release-manifest.json", 1, zitManifest+" "); err != nil {
		t.Fatal(err)
	}
	g.objects.mu.Lock()
	o := g.objects.objects["files/unversioned/releases/7.7.7/release-manifest.json"]
	o.body = []byte(zitManifest)
	g.objects.objects["files/unversioned/releases/7.7.7/release-manifest.json"] = o
	g.objects.mu.Unlock()
	if r := g.releases(t).Releases[0]; r.Manifest != nil || r.ManifestErr == "" {
		t.Fatalf("FAIL: a mismatched manifest was used: %+v", r)
	}
	gets := g.objects.gets
	if r := g.releases(t).Releases[0]; r.ManifestErr == "" || g.objects.gets != gets {
		t.Errorf("FAIL: the mismatch was fetched again (%d reads)", g.objects.gets-gets)
	}
	t.Log("ok: a mismatch is cached under the claimed hash")
}