// landingping_test.go proves RFC 0011's landing family end to end: a
// settled landing reaches the person who opened it and the owners of
// the branch it settled onto, on BOTH halves โ quarry's own attention
// stream and the notify companion โ and reaches nobody else. The last
// case is the one that matters most: a reindex replays every artifact
// through the fold, and the fold must stay silent (RFC 0001 ยง5).
package harness
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/events"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ingest"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
const pingPW = "pw12345678"
type pingForge struct {
srv *httptest.Server
ev *events.Server
notify *notifyCapture
store string
sync func()
reindex func() (int, error)
}
// buildPingForge stands up a forge with BOTH planes wired: the local
// attention stream and a recording notify. bob opens; alice and carol
// own trunk; carol settles.
func buildPingForge(t *testing.T) *pingForge {
t.Helper()
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
store := buildStore(t, "pingseed")
dataDir := t.TempDir()
ev, err := events.Start(filepath.Join(dataDir, "nats"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(ev.Close)
odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { odb.Close() })
for _, name := range []string{"alice", "bob", "carol"} {
if err := odb.AddAccount(name, pingPW, false); err != nil {
t.Fatal(err)
}
if err := odb.SetCaps(name, "alpha", "kfw3C5aiz"); err != nil {
t.Fatal(err)
}
}
// alice owns trunk and never touches this landing; carol owns it
// too, which is what lets her be the hand that settles.
if err := odb.SetProtection("alpha", "trunk", []string{"alice", "carol"}); err != nil {
t.Fatal(err)
}
ix, err := index.Open(filepath.Join(dataDir, "alpha.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
cli := zitcli.New(store)
pipe := ingest.NewPipeline(cli, ev, ix, "alpha", "harness")
nc := ¬ifyCapture{}
deps := api.Deps{Version: "0.0.0-harness", Repos: api.NewRepos(nil),
Protect: odb, Forum: odb, Votes: odb, Notify: nc,
Attention: harnessAttention{ev: ev}, AttnRead: odb, Accounts: harnessAccounts{db: odb}}
deps.CapsFor = func(account, repo string) api.CapSet { return odb.EffectiveCaps(account, repo) }
deps.Repos.Put("alpha", &api.Repo{Index: ix, Zit: cli, Sync: pipe.Sync, Code: "alpha",
Reindex: pipe.Reindex,
Leaves: func() (map[string]bool, error) { return cli.Leaves(context.Background()) }})
f := &pingForge{ev: ev, notify: nc, store: store, reindex: pipe.Reindex}
f.sync = func() {
t.Helper()
if _, err := pipe.Sync(); err != nil {
t.Fatal(err)
}
}
f.sync()
r := ops.NewRegistry()
if err := api.RegisterAll(r, deps); err != nil {
t.Fatal(err)
}
verify := func(repo, user, pass string, _ ops.Auth) api.Decision {
if pass == pingPW && (user == "alice" || user == "bob" || user == "carol") {
return api.Allowed
}
return api.Denied
}
mux, err := api.RouterWithCaps(r, "0.0.0-harness", deps.CapsFor, verify)
if err != nil {
t.Fatal(err)
}
f.srv = httptest.NewServer(mux)
t.Cleanup(f.srv.Close)
return f
}
func (f *pingForge) call(t *testing.T, user, method, path, body string) (int, string) {
t.Helper()
req, _ := http.NewRequest(method, f.srv.URL+path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(user, pingPW)
resp, err := f.srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(b)
}
// total is every trigger the fake notify has seen, read under its lock.
func (n *notifyCapture) total() int {
n.mu.Lock()
defer n.mu.Unlock()
return len(n.events)
}
// attention returns the account's attention rows, waiting for want of
// them. want 0 waits the whole window, so "nothing arrived" is proved.
func (f *pingForge) attention(t *testing.T, who string, want int) []events.AttentionEvent {
t.Helper()
var out []events.AttentionEvent
for i := 0; i < 60; i++ {
got, err := f.ev.RecentAttention(who, 50)
if err != nil {
t.Fatal(err)
}
out = got
if want > 0 && len(out) >= want {
return out
}
time.Sleep(25 * time.Millisecond)
}
return out
}
// A settled landing reaches the opener and the branch owner on both
// planes, reaches the hand that settled it on neither, and a reindex
// afterwards reaches nobody at all.
func TestSettledLandingPingsOpenerAndOwnerNotTheActor(t *testing.T) {
f := buildPingForge(t)
zrun(t, f.store, "update", "trunk")
if err := os.WriteFile(filepath.Join(f.store, "feature.txt"), []byte("work\n"), 0o644); err != nil {
t.Fatal(err)
}
zrun(t, f.store, "add", "feature.txt")
zrun(t, f.store, "commit", "-m", "work on feature-x", "--branch", "feature-x")
f.sync()
code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing",
`{"branch":"feature-x","target":"trunk","message":"land feature-x"}`)
if code != http.StatusOK {
t.Fatalf("landing.open: %d %s", code, body)
}
var opened struct {
ID string `json:"id"`
}
if err := json.Unmarshal([]byte(body), &opened); err != nil || opened.ID == "" {
t.Fatalf("no landing id: %v %s", err, body)
}
f.sync()
if code, body := f.call(t, "carol", "POST",
"/api/v1/r/alpha/landing/"+opened.ID[:12]+"/accept", `{"message":"landed"}`); code != http.StatusOK {
t.Fatalf("landing.accept: %d %s", code, body)
}
// The notify half: one trigger each to the opener and the owner.
f.notify.waitFor(t, "quarry-landing", "bob", 1)
f.notify.waitFor(t, "quarry-landing", "alice", 1)
if n := f.notify.count("quarry-landing", "carol"); n != 0 {
t.Fatalf("the actor got %d landing triggers, want none", n)
}
for _, who := range []string{"bob", "alice"} {
if n := f.notify.count("quarry-landing", who); n != 1 {
t.Fatalf("%s got %d landing triggers, want exactly one", who, n)
}
}
// The attention half: the same two people, keyed to the same event.
for _, who := range []string{"bob", "alice"} {
evs := f.attention(t, who, 1)
if len(evs) != 1 {
t.Fatalf("%s has %d attention rows, want 1: %+v", who, len(evs), evs)
}
e := evs[0]
if !strings.HasPrefix(e.Kind, "landing.") {
t.Fatalf("%s attention kind = %q, want the landing family", who, e.Kind)
}
if e.Repo != "alpha" || e.From != "carol" {
t.Fatalf("%s attention repo/from = %q/%q", who, e.Repo, e.From)
}
if e.TxnID != "landing:accept:"+opened.ID+":"+who {
t.Fatalf("%s attention TxnID = %q, want the address-based key", who, e.TxnID)
}
if !strings.HasSuffix(e.Link, "/landing/"+opened.ID) {
t.Fatalf("%s attention link = %q", who, e.Link)
}
}
if evs := f.attention(t, "carol", 0); len(evs) != 0 {
t.Fatalf("the actor got attention rows: %+v", evs)
}
// The fold is not a notifier. A reindex replays every artifact,
// including the accept โ and tells nobody.
before := f.notify.total()
if _, err := f.reindex(); err != nil {
t.Fatal(err)
}
f.sync()
time.Sleep(300 * time.Millisecond)
if got := f.notify.total(); got != before {
t.Fatalf("a reindex fired %d new notifications, want none", got-before)
}
for _, who := range []string{"bob", "alice", "carol"} {
want := 1
if who == "carol" {
want = 0
}
if evs := f.attention(t, who, 0); len(evs) != want {
t.Fatalf("after reindex %s has %d attention rows, want %d", who, len(evs), want)
}
}
}