Skip to content
quarry Artifact [d4af48415a]
Login

Artifact [d4af48415a]

Artifact d4af48415aebaa7f2b328de7114548818b5c245ba21cff293a93948cf5402507:


package api

import (
	"errors"
	"path/filepath"
	"strings"
	"sync"
	"testing"

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

type settingsEvents struct {
	mu      sync.Mutex
	summary []string
}

func (e *settingsEvents) Raise(kind evt.Kind, repo string, payload map[string]any) error {
	e.mu.Lock()
	defer e.mu.Unlock()
	if kind == evt.RepoSettings {
		e.summary = append(e.summary, payload["summary"].(string)+" by "+payload["actor"].(string))
	}
	return nil
}

func categoryRig(t *testing.T) (*ops.Registry, *settingsEvents) {
	t.Helper()
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	ev := &settingsEvents{}
	r := ops.NewRegistry()
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {}}), Categories: db, Events: ev}
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	return r, ev
}

func categoriesFrom(t *testing.T, out any) map[string]category {
	t.Helper()
	got := map[string]category{}
	for _, c := range out.(categoriesOut).Categories {
		got[c.Name] = c
	}
	return got
}

// #291: an admin token reads all four category strings with where each
// comes from, and sets one; the change is the repository's own from
// then on and reaches the settings trail with who made it.
func TestCategoriesReadAndSetByToken(t *testing.T) {
	r, ev := categoryRig(t)
	out, err := r.Invoke(as("adm", "a"), "repo.categories", categoriesIn{Repo: "test"})
	if err != nil {
		t.Fatal(err)
	}
	got := categoriesFrom(t, out)
	// A fresh forge has fleet defaults for nobody and anonymous; reader
	// and developer fall through to fossil's stock strings.
	if len(got) != 4 || got["nobody"].From != oltp.CategoryFleet || got["reader"].Caps != oltp.StockReader || got["developer"].From != oltp.CategoryStock {
		t.Fatalf("FAIL: categories = %+v", got)
	}
	out, err = r.Invoke(as("adm", "a"), "repo.category.set", categorySetIn{Repo: "test", Category: "anonymous", Caps: "hmnc34"})
	if err != nil {
		t.Fatal(err)
	}
	if c := categoriesFrom(t, out)["anonymous"]; c.Caps != "hmnc34" || c.From != oltp.CategoryOwn {
		t.Fatalf("FAIL: after the set, anonymous = %+v", c)
	}
	if len(ev.summary) != 1 || !strings.Contains(ev.summary[0], "anonymous") || !strings.HasSuffix(ev.summary[0], "by adm") {
		t.Fatalf("FAIL: trail = %q", ev.summary)
	}
	// The same string again changes nothing and says nothing.
	if _, err := r.Invoke(as("adm", "a"), "repo.category.set", categorySetIn{Repo: "test", Category: "anonymous", Caps: "hmnc34"}); err != nil || len(ev.summary) != 1 {
		t.Fatalf("FAIL: a no-op set: err %v, trail %q", err, ev.summary)
	}
	// Empty is a value: nobody blanked reads as the repository's own "".
	out, err = r.Invoke(as("adm", "a"), "repo.category.set", categorySetIn{Repo: "test", Category: "nobody", Caps: ""})
	if err != nil {
		t.Fatal(err)
	}
	if c := categoriesFrom(t, out)["nobody"]; c.Caps != "" || c.From != oltp.CategoryOwn {
		t.Fatalf("FAIL: a blanked nobody = %+v", c)
	}
}

// Only the four names are categories, and whitespace is refused rather
// than kept as a letter. (Both ops declare Caps "a", which the edge
// enforces for every op.)
func TestCategoriesRefusals(t *testing.T) {
	r, ev := categoryRig(t)
	if _, err := r.Invoke(as("adm", "a"), "repo.category.set", categorySetIn{Repo: "test", Category: "admin", Caps: "s"}); !errors.Is(err, ops.ErrInvalid) {
		t.Error("FAIL: an unknown category name was accepted")
	}
	if _, err := r.Invoke(as("adm", "a"), "repo.category.set", categorySetIn{Repo: "test", Category: "reader", Caps: "k p"}); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: whitespace in caps answered %v, want invalid", err)
	}
	if len(ev.summary) != 0 {
		t.Errorf("FAIL: refused sets reached the trail: %q", ev.summary)
	}
}