Skip to content
quarry Artifact [d1c2fd2731]
Login

Artifact [d1c2fd2731]

Artifact d1c2fd27318dd35c452f84584bed95eb950970a04acc50a0f623ef942c811a8c:


package api

import (
	"context"
	"path/filepath"
	"testing"

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

// activityIndex is the merge shape RFC 0013 ยง3 is about: dan commits,
// eve commits on a branch, dan merges her work in. The merge's three
// paths are eve's, and the op has to say so.
func activityIndex(t *testing.T) *index.Index {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	if err := ix.FoldNow([]index.Parsed{
		{Hash: "a1", Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-06-01T09:00:00", User: "dan", Comment: "one"}},
		{Hash: "b1", Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-06-02T10:00:00", User: "eve", Comment: "two",
			Parents: []string{"a1"}}},
		{Hash: "m1", Art: &artifact.Artifact{Type: artifact.Manifest,
			Date: "2026-06-03T11:00:00", User: "dan", Comment: "merge",
			Parents: []string{"a1", "b1"}}},
	}); err != nil {
		t.Fatal(err)
	}
	if err := ix.FoldChangesBulk([]index.BulkChanges{
		{Hash: "a1", Art: &artifact.Artifact{Date: "2026-06-01T09:00:00"},
			Changes: []string{"src/one.go"}},
		{Hash: "b1", Art: &artifact.Artifact{Date: "2026-06-02T10:00:00"},
			Changes: []string{"src/two.go"}},
		{Hash: "m1", Art: &artifact.Artifact{Date: "2026-06-03T11:00:00"},
			Changes: []string{"src/two.go", "src/three.go", "doc/four.md"}},
	}); err != nil {
		t.Fatal(err)
	}
	return ix
}

func activityRegistry(t *testing.T) *ops.Registry {
	t.Helper()
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(activityIndex(t))}); err != nil {
		t.Fatal(err)
	}
	return r
}

// Both routes exist and are anonymous reads, because the activity page
// is a door a visitor walks through before they have an account.
func TestActivityOpsAreRegisteredAsAnonymousReads(t *testing.T) {
	r := activityRegistry(t)
	spec, err := ops.Spec(r, "quarry", "v")
	if err != nil {
		t.Fatal(err)
	}
	for _, path := range []string{"/api/v1/r/{repo}/activity", "/api/v1/r/{repo}/activity/days"} {
		p, ok := spec.Paths[path]
		if !ok || p.Get == nil {
			t.Fatalf("%s missing from the generated spec", path)
		}
	}
}

func TestRepoActivityOpCarriesTheMergeRule(t *testing.T) {
	r := activityRegistry(t)
	got, err := r.Invoke(context.Background(), "repo.activity",
		repoActivityIn{Repo: "test", Since: "2026-06-01", Until: "2026-06-30"})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(repoActivityOut)
	if out.Window != "given" || out.Since != "2026-06-01" || out.Until != "2026-06-30" {
		t.Fatalf("window = %s %s..%s", out.Window, out.Since, out.Until)
	}
	if len(out.Rows) != 3 {
		t.Fatalf("got %d rows, want 3: %+v", len(out.Rows), out.Rows)
	}
	var merge activityRow
	for _, row := range out.Rows {
		if row.Hash == "m1" {
			merge = row
		}
	}
	if merge.Merge != 1 || merge.Files != 0 {
		t.Fatalf("merge row = %+v, want Merge 1 Files 0", merge)
	}
	inherited := 0
	for _, tc := range out.Touches {
		if tc.Inherited == 1 {
			inherited++
		}
	}
	if inherited != 3 {
		t.Fatalf("%d inherited touches, want 3: %+v", inherited, out.Touches)
	}
	if len(out.Authors) != 2 || out.Authors[0].Name != "dan" || out.Authors[0].Files != 1 {
		t.Fatalf("authors = %+v, want dan leading with one authored file", out.Authors)
	}
	for _, a := range out.Authors {
		if a.Hue < 0 || a.Hue >= 360 {
			t.Fatalf("%s has no usable hue: %+v", a.Name, a)
		}
	}
	for _, p := range out.Paths {
		if p == "src" || p == "doc" {
			t.Fatalf("a directory reached Paths: %v", out.Paths)
		}
	}
}

func TestRepoActivityOpFiltersByBranchAndUser(t *testing.T) {
	r := activityRegistry(t)
	got, err := r.Invoke(context.Background(), "repo.activity",
		repoActivityIn{Repo: "test", User: "eve", Since: "2026-06-01", Until: "2026-06-30"})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(repoActivityOut)
	if len(out.Rows) != 1 || out.Rows[0].Hash != "b1" {
		t.Fatalf("user filter = %+v, want only b1", out.Rows)
	}
}

func TestRepoActivityDaysOpFoldsTheHistory(t *testing.T) {
	r := activityRegistry(t)
	got, err := r.Invoke(context.Background(), "repo.activity.days",
		repoActivityDaysIn{Repo: "test"})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(repoActivityDaysOut)
	if out.First != "2026-06-01" || out.Last != "2026-06-03" {
		t.Fatalf("span = %s..%s", out.First, out.Last)
	}
	if len(out.Days) != 3 {
		t.Fatalf("got %d days, want 3: %+v", len(out.Days), out.Days)
	}
	d, ok := out.Days["2026-06-03"]
	if !ok {
		t.Fatalf("merge day missing: %+v", out.Days)
	}
	if d.N != 1 || d.Merges != 1 {
		t.Fatalf("merge day = %+v, want N 1 Merges 1", d)
	}
	// Its only file touches were inherited, so no directory led it.
	if d.Path != -1 {
		t.Fatalf("merge day claimed directory %d (%v)", d.Path, out.Paths)
	}
	if len(out.Authors) != 2 {
		t.Fatalf("authors = %+v, want two", out.Authors)
	}
}

// Unknown repository, not a panic: the op is anonymous and the name
// comes off a URL.
func TestActivityOpsRejectAnUnknownRepo(t *testing.T) {
	r := activityRegistry(t)
	if _, err := r.Invoke(context.Background(), "repo.activity",
		repoActivityIn{Repo: "nope"}); err == nil {
		t.Fatal("want an error for an unknown repository")
	}
	if _, err := r.Invoke(context.Background(), "repo.activity.days",
		repoActivityDaysIn{Repo: "nope"}); err == nil {
		t.Fatal("want an error for an unknown repository")
	}
}