package harness
import (
"encoding/json"
"io"
"net/http"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// TestCapsGateTheAPI is RFC 0004 §4 enforced end to end: each account
// holds exactly one fossil capability letter and may reach exactly the
// surfaces that letter opens. The Auth floor lets every authenticated
// caller through; the capability check is what decides.
func TestCapsGateTheAPI(t *testing.T) {
stores := map[string]string{"alpha": buildStore(t, "capseed")}
dataDir := t.TempDir()
srv := ingestAndServeAuth(t, stores, dataDir,
func(repo, user, pass string, need ops.Auth) api.Decision {
if user == "" {
return api.Denied
}
return api.Allowed
})
// The harness opened this same file; a second handle sees its rows.
odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
defer odb.Close()
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(odb.AddAccount("reader", "pw12345678", false))
must(odb.SetCaps("reader", "alpha", "o")) // Read
must(odb.AddAccount("forumer", "pw12345678", false))
must(odb.SetCaps("forumer", "alpha", "3")) // WrForum
// This test is about letters, not the fleet default: RFC 0008's
// Public project lets any signed-in account post in the forum, so
// pin fossil's stock anonymous string to see the gate alone.
must(odb.SetCategoryCaps("alpha", "anonymous", oltp.StockAnonymous))
call := func(user, method, path, body string) int {
var r io.Reader
if body != "" {
r = strings.NewReader(body)
}
req, _ := http.NewRequest(method, srv.URL+path, r)
req.SetBasicAuth(user, "pw12345678")
if body != "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp.StatusCode
}
// o reads.
if got := call("reader", "GET", "/api/v1/r/alpha/timeline", ""); got != http.StatusOK {
t.Fatalf("o cannot read the timeline: %d", got)
}
// o is refused the forum write surface; 3 is not.
if got := call("reader", "POST", "/api/v1/r/alpha/forum",
"title=x&body=y"); got != http.StatusForbidden {
t.Fatalf("o opened the forum write surface: %d", got)
}
if got := call("forumer", "POST", "/api/v1/r/alpha/forum",
"title=from+the+forum+cap&body=hello"); got != http.StatusOK {
t.Fatalf("3 cannot write the forum: %d", got)
}
// 3 does not carry repository administration (a).
if got := call("forumer", "POST", "/api/v1/repos",
"name=nope&create=1"); got == http.StatusOK {
t.Fatal("3 reached repository creation")
}
// The WEB forms answer to the same letters as the API — a page that
// gates more loosely than its op is a hole with a nice layout.
if got := call("reader", "POST", "/r/alpha/forum-new",
"title=x&body=y"); got != http.StatusForbidden {
t.Fatalf("o posted a thread through the web form: %d", got)
}
// 3 posts successfully: a redirect to the new thread, or the page
// itself — either way, NOT a refusal.
if got := call("forumer", "POST", "/r/alpha/forum-new",
"title=web+thread&body=hello"); got == http.StatusForbidden || got == http.StatusUnauthorized {
t.Fatalf("3 cannot post through the web form: %d", got)
}
// Ticket filing needs n — which the ANONYMOUS category grants to
// every signed-in account (fossil's hmnc default). So this passes,
// and that is the rule, not a leak: a signed-in human may file a
// ticket unless the repo says otherwise.
if got := call("forumer", "POST", "/r/alpha/ticket/new",
"title=t&comment=c"); got == http.StatusForbidden {
t.Fatalf("the anonymous category did not grant n: %d", got)
}
// Take n away from the category and the same request is refused —
// which is what proves the gate reads caps rather than waving
// signed-in callers through.
if err := odb.SetCategoryCaps("alpha", "anonymous", "h"); err != nil {
t.Fatal(err)
}
if got := call("forumer", "POST", "/r/alpha/ticket/new",
"title=t2&comment=c"); got != http.StatusForbidden {
t.Fatalf("ticket filing survived the loss of n: %d", got)
}
}
// TestCategoryCapsReachTheSurfaces: an account with NO explicit caps
// still reaches what the anonymous category grants, and a repo taken
// private (blank nobody+anonymous) closes even the read surfaces —
// fossil's Security-Audit behavior, which is quarry's invisibility.
func TestCategoryCapsReachTheSurfaces(t *testing.T) {
stores := map[string]string{"alpha": buildStore(t, "catseed")}
dataDir := t.TempDir()
srv := ingestAndServeAuth(t, stores, dataDir,
func(repo, user, pass string, need ops.Auth) api.Decision {
if user == "" {
return api.Denied
}
return api.Allowed
})
odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
defer odb.Close()
if err := odb.AddAccount("plain", "pw12345678", false); err != nil {
t.Fatal(err)
}
get := func(user, path string) int {
req, _ := http.NewRequest("GET", srv.URL+path, nil)
if user != "" {
req.SetBasicAuth(user, "pw12345678")
}
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp.StatusCode
}
// Stock nobody carries o: the timeline reads for everyone.
if got := get("plain", "/api/v1/r/alpha/timeline"); got != http.StatusOK {
t.Fatalf("category caps do not reach the timeline: %d", got)
}
// Take it private: blank the two visitor categories.
if err := odb.SetCategoryCaps("alpha", "nobody", ""); err != nil {
t.Fatal(err)
}
if err := odb.SetCategoryCaps("alpha", "anonymous", ""); err != nil {
t.Fatal(err)
}
if got := get("plain", "/api/v1/r/alpha/timeline"); got == http.StatusOK {
t.Fatal("a private repo still served its timeline to a capless account")
}
}
// Capability e end to end: fossil withholds private_contact from the
// public and shows it to holders of e. quarry dropped it outright
// because it had no capability to gate on; it has one now, so the
// field must reach a holder and nobody else.
//
// The unit test covers the render rule. This covers the thing that
// actually decides it — a real caps plane answering for a real account.
func TestPrivateContactReachesOnlyHoldersOfE(t *testing.T) {
// The realistic source of this field is an IMPORT: a fossil repo
// hosted here brings its own private_contact values with it. quarry
// does not author the field, so the test writes it the way a mirror
// would — into the store, before the index ever sees it.
store := buildStore(t, "eseed")
zrun(t, store, "ticket", "new", "--user", "eseed",
"title=a bug with a contact", "status=Open",
"[email protected]")
dataDir := t.TempDir()
srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, dataDir,
func(repo, user, pass string, need ops.Auth) api.Decision {
if user == "" {
return api.Denied
}
return api.Allowed
})
odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
defer odb.Close()
for _, u := range []string{"plain", "holder"} {
if err := odb.AddAccount(u, "pw12345678", false); err != nil {
t.Fatal(err)
}
}
// Only one of them may see private contact details.
if err := odb.SetCaps("holder", "alpha", "re"); err != nil {
t.Fatal(err)
}
if err := odb.SetCaps("plain", "alpha", "r"); err != nil {
t.Fatal(err)
}
get := func(user, path string) string {
req, _ := http.NewRequest("GET", srv.URL+path, nil)
req.SetBasicAuth(user, "pw12345678")
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return string(b)
}
var list struct {
Tickets []struct {
ID string `json:"id"`
Title string `json:"title"`
} `json:"tickets"`
}
if err := json.Unmarshal([]byte(get("holder", "/api/v1/r/alpha/ticket?state=all&limit=50")), &list); err != nil {
t.Fatal(err)
}
var id string
for _, tk := range list.Tickets {
if strings.Contains(tk.Title, "a contact") {
id = tk.ID
}
}
if id == "" {
t.Fatalf("the imported ticket never reached the index: %+v", list.Tickets)
}
page := "/r/alpha/ticket/" + id
if body := get("plain", page); strings.Contains(body, "[email protected]") {
t.Error("a reader without e was shown a private contact address")
}
if body := get("holder", page); !strings.Contains(body, "[email protected]") {
t.Error("a holder of e was refused the field the capability names")
}
}
// A ticket its own tracker marked private. quarry hid it from
// everyone, including the people it was for, because there was no
// capability to grant an exception to. Fossil shows it to holders of e.
//
// The failure to avoid is not just "shown to the wrong reader" but
// "confirmed to exist": without e the ticket must not resolve at all,
// must not be counted, and must not appear under any facet — a listing
// that says 4 while showing 3 has told a stranger a private ticket is
// there.
func TestPrivateTicketsExistOnlyForHoldersOfE(t *testing.T) {
store := buildStore(t, "privseed")
zrun(t, store, "ticket", "new", "--user", "privseed",
"title=a private matter", "status=Open", "is_private=1")
dataDir := t.TempDir()
srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, dataDir,
func(repo, user, pass string, need ops.Auth) api.Decision {
if user == "" {
return api.Denied
}
return api.Allowed
})
odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
defer odb.Close()
for _, u := range []string{"plain", "holder"} {
if err := odb.AddAccount(u, "pw12345678", false); err != nil {
t.Fatal(err)
}
}
if err := odb.SetCaps("holder", "alpha", "re"); err != nil {
t.Fatal(err)
}
if err := odb.SetCaps("plain", "alpha", "r"); err != nil {
t.Fatal(err)
}
list := func(user string) (titles []string, open int) {
req, _ := http.NewRequest("GET", srv.URL+"/api/v1/r/alpha/ticket?state=all&limit=50", nil)
req.SetBasicAuth(user, "pw12345678")
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var out struct {
Tickets []struct{ Title string } `json:"tickets"`
Open int `json:"open"`
}
json.NewDecoder(resp.Body).Decode(&out)
for _, tk := range out.Tickets {
titles = append(titles, tk.Title)
}
return titles, out.Open
}
plainTitles, plainOpen := list("plain")
for _, title := range plainTitles {
if strings.Contains(title, "private matter") {
t.Error("a private ticket was listed to a reader without e")
}
}
heldTitles, heldOpen := list("holder")
var found bool
for _, title := range heldTitles {
if strings.Contains(title, "private matter") {
found = true
}
}
if !found {
t.Errorf("a holder of e cannot see the private ticket: %v", heldTitles)
}
// The count must agree with the list each reader is shown. A number
// that includes what the listing hides is a confirmation leak.
if plainOpen != len(plainTitles) || heldOpen != len(heldTitles) {
t.Errorf("counts disagree with listings: plain %d/%d, holder %d/%d",
plainOpen, len(plainTitles), heldOpen, len(heldTitles))
}
if heldOpen <= plainOpen {
t.Errorf("the private ticket is in nobody's count: plain %d, holder %d", plainOpen, heldOpen)
}
}
// Who may land a branch. zit#127 scoped approvals out, correctly, for a CLI
// where the operator IS the authority; a multi-account forge has to answer it.
//
// The rule: accept rides `i` — fossil's Write/check-in letter — because accept
// MERGES, and everything else about a landing is a forum artifact. Speaking in
// the discussion is `3`; settling it with a check-in is `i`. RFC 0004 stored
// `i` and left it inert; this is the surface that wakes it.
//
// And there is deliberately NO rule against accepting your own landing. Inside
// a cathedral the author already holds the commit bit, and a required second
// pair of eyes is a policy fossil has no letter for — inventing one here would
// be quarry policy wearing fossil's clothes. The test pins that absence so
// nobody adds the rule back as an obvious-looking improvement.
func TestLandingAcceptRidesTheCheckinCapability(t *testing.T) {
stores := map[string]string{"alpha": buildStore(t, "landseed")}
dataDir := t.TempDir()
srv := ingestAndServeAuth(t, stores, dataDir,
func(repo, user, pass string, need ops.Auth) api.Decision {
if user == "" {
return api.Denied
}
return api.Allowed
})
odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
defer odb.Close()
for _, u := range []string{"talker", "lander"} {
if err := odb.AddAccount(u, "pw12345678", false); err != nil {
t.Fatal(err)
}
}
// talker may read and speak in the thread but not write check-ins;
// lander may do both.
if err := odb.SetCaps("talker", "alpha", "23"); err != nil {
t.Fatal(err)
}
if err := odb.SetCaps("lander", "alpha", "23i"); err != nil {
t.Fatal(err)
}
code := func(user, path string) int {
req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(`{"message":"x"}`))
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(user, "pw12345678")
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp.StatusCode
}
// Forbidden is the capability speaking. Anything else — 404 for a
// missing record, 500 from the store — means the gate never ran, so
// this asserts the gate and not the outcome behind it.
if got := code("talker", "/api/v1/r/alpha/landing/deadbeef/accept"); got != http.StatusForbidden {
t.Errorf("accept without i: %d, want 403 — the check-in capability is what settles a landing", got)
}
if got := code("lander", "/api/v1/r/alpha/landing/deadbeef/accept"); got == http.StatusForbidden {
t.Error("a holder of i was refused the accept surface")
}
// Rejecting settles the record, a decision like accepting: the
// forum letter alone does not carry it, the check-in letter does.
if got := code("talker", "/api/v1/r/alpha/landing/deadbeef/reject"); got != http.StatusForbidden {
t.Errorf("reject without i: %d, want 403 — a decision takes the check-in capability", got)
}
if got := code("lander", "/api/v1/r/alpha/landing/deadbeef/reject"); got == http.StatusForbidden {
t.Error("a holder of i was refused the reject surface")
}
if got := code("talker", "/api/v1/r/alpha/landing/deadbeef/comment"); got == http.StatusForbidden {
t.Error("commenting on a landing demanded more than the forum capability")
}
}