package api
import (
"context"
"errors"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
type fakeCompanions struct {
bound map[string]string // parent -> store
}
func (f *fakeCompanions) Companion(repo, feature string) (string, bool, error) {
s, ok := f.bound[repo]
return s, ok, nil
}
func (f *fakeCompanions) CompanionParent(store string) (string, string, bool, error) {
for p, s := range f.bound {
if s == store {
return p, "forum", true, nil
}
}
return "", "", false, nil
}
func (f *fakeCompanions) BindCompanion(repo, feature, store string) error {
if f.bound == nil {
f.bound = map[string]string{}
}
f.bound[repo] = store
return nil
}
// The forum ops resolve through the parent: the reader is checked
// against the parent's name, and the store handed back is the one the
// binding names. An unbound repository is its own forum, as before.
func TestForumRepoForFollowsTheBinding(t *testing.T) {
fossil, forum := &Repo{}, &Repo{}
d := Deps{
Repos: NewRepos(map[string]*Repo{"fossil": fossil, "fossil-forum": forum, "sqlite": &Repo{}}),
Companions: &fakeCompanions{bound: map[string]string{"fossil": "fossil-forum"}},
CanRead: func(_ context.Context, repo string) bool {
// Only the PARENT's name is ever asked about.
if repo == "fossil-forum" {
t.Errorf("FAIL: the gate was asked about the companion %q", repo)
}
return repo != "secret"
},
}
ctx := context.Background()
if r, err := d.forumRepoFor(ctx, "fossil"); err != nil || r != forum {
t.Errorf("FAIL: fossil's forum resolved to %p (%v), want the companion %p", r, err, forum)
}
if r, err := d.forumRepoFor(ctx, "sqlite"); err != nil || r == forum || r == nil {
t.Errorf("FAIL: an unbound repository did not resolve to itself: %p %v", r, err)
}
if _, err := d.forumRepoFor(ctx, "secret"); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: an unreadable parent answered %v, want not found", err)
}
// A binding to a store this node does not serve is an operator's
// problem and says so, rather than "no forum".
d.Companions = &fakeCompanions{bound: map[string]string{"fossil": "elsewhere"}}
if _, err := d.forumRepoFor(ctx, "fossil"); err == nil || !strings.Contains(err.Error(), "elsewhere") {
t.Errorf("FAIL: a binding to an unserved store answered %v", err)
}
// A companion is not a project: the listing skips it.
if d.companionParent("elsewhere") != "fossil" || d.companionParent("fossil") != "" {
t.Error("FAIL: companionParent does not answer both ways")
}
t.Log("ok: forum ops read the parent's gate and the companion's store")
}
// forum.bind refuses every shape RFC 0015 rules out, each with a
// reason, and accepts the same binding twice as the same answer.
func TestForumBindRefusesTheWrongShapes(t *testing.T) {
comp := &fakeCompanions{bound: map[string]string{"fossil": "fossil-forum"}}
d := Deps{
Repos: NewRepos(map[string]*Repo{"fossil": {}, "fossil-forum": {}, "sqlite": {}, "unindexed": {}}),
Companions: comp,
}
r := ops.NewRegistry()
if err := registerCompanions(r, d); err != nil {
t.Fatal(err)
}
ctx := context.Background()
bind := func(repo, store string) error {
_, err := r.Invoke(ctx, "forum.bind", forumBindIn{Repo: repo, Store: store})
return err
}
cases := []struct{ name, repo, store, want string }{
{"self", "sqlite", "sqlite", "its own forum"},
{"unknown parent", "nope", "sqlite", "no repository"},
{"a companion as parent", "fossil-forum", "sqlite", "itself a companion"},
{"parent already bound elsewhere", "fossil", "sqlite", "already bound to fossil-forum"},
{"store already somebody's", "sqlite", "fossil-forum", "already fossil's forum"},
{"store that has a forum of its own", "sqlite", "fossil", "forum of its own"},
{"store not indexed yet", "sqlite", "unindexed", "not indexed yet"},
}
for _, c := range cases {
err := bind(c.repo, c.store)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("FAIL: %s: got %v, want a reason containing %q", c.name, err, c.want)
}
}
if err := bind("fossil", "fossil-forum"); err != nil {
t.Errorf("FAIL: re-binding the same pair is not idempotent: %v", err)
}
if len(comp.bound) != 1 {
t.Errorf("FAIL: a refused bind was recorded: %v", comp.bound)
}
t.Log("ok: every refused shape names its reason; the same binding twice is one binding")
}