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)
}
}