Skip to content
quarry Artifact [3b2aab3c97]
Login

Artifact [3b2aab3c97]

Artifact 3b2aab3c978bab1c54d4b7b8ba89b6059a789ca7c196701261123231273eb992:


// chatref_test.go proves RFC 0011 §14's reference half end to end, on
// a REAL repository: a ticket filed through ticket.new, named from chat
// by the prefix a person would actually type, and the row that lands on
// the ticket's own page. Only the page proves it — a DB row filed under
// a spelling no page looks up is a row nobody sees.
package harness

import (
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"path/filepath"
	"strings"
	"testing"

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

// refForge is one ingested repository with chat wired, a maintainer who
// can file tickets, and an OLTP handle for the capability edits a
// refusal test needs.
type refForge struct {
	t      *testing.T
	url    string
	client *http.Client
	odb    *oltp.DB
}

func newRefForge(t *testing.T) *refForge {
	t.Helper()
	dataDir := t.TempDir()
	srv := ingestAndServeAuth(t, map[string]string{"alpha": buildStore(t, "refseed")}, dataDir,
		func(_, user, pass string, _ ops.Auth) api.Decision {
			if user != "" && pass == "hunter2" {
				return api.Allowed
			}
			return api.Denied
		})
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { odb.Close() })
	client := srv.Client()
	client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
	return &refForge{t: t, url: srv.URL, client: client, odb: odb}
}

func (f *refForge) do(method, path, user string, form url.Values) (int, string) {
	f.t.Helper()
	var body io.Reader
	if form != nil {
		body = strings.NewReader(form.Encode())
	}
	req, err := http.NewRequest(method, f.url+path, body)
	if err != nil {
		f.t.Fatal(err)
	}
	if form != nil {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	if user != "" {
		req.SetBasicAuth(user, "hunter2")
	}
	resp, err := f.client.Do(req)
	if err != nil {
		f.t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	if loc := resp.Header.Get("Location"); loc != "" {
		return resp.StatusCode, loc
	}
	return resp.StatusCode, string(b)
}

// newTicket files one through the ordinary form and answers its id.
func (f *refForge) newTicket(title string) string {
	f.t.Helper()
	code, loc := f.do("POST", "/r/alpha/ticket/new", "dan",
		url.Values{"title": {title}, "comment": {"filed for the reference test"}})
	if code != http.StatusSeeOther {
		f.t.Fatalf("ticket new: status %d (%s)", code, loc)
	}
	return strings.TrimPrefix(loc, "/r/alpha/ticket/")
}

// A chat message naming a ticket by a SHORT prefix puts the backlink
// row on that ticket's page. The row is keyed by the artifact's own id,
// never by the prefix the author typed — a row filed under `a91f3c2e`
// sits where no page ever looks.
func TestChatPrefixReferenceReachesTheTicketPage(t *testing.T) {
	f := newRefForge(t)
	id := f.newTicket("reference target")
	if len(id) < 12 {
		t.Fatalf("ticket id too short to prefix: %q", id)
	}
	prefix := id[:8]

	code, body := f.do("POST", "/api/v1/r/alpha/chat", "dan",
		url.Values{"text": {"fixed in [" + prefix + "] — please confirm"}})
	if code != http.StatusOK {
		t.Fatalf("chat post: status %d (%s)", code, body)
	}
	var posted struct {
		Seq uint64 `json:"seq"`
	}
	if err := json.Unmarshal([]byte(body), &posted); err != nil {
		t.Fatal(err)
	}

	page := ""
	if _, page = f.do("GET", "/r/alpha/ticket/"+id, "dan", nil); page == "" {
		t.Fatalf("ticket page empty")
	}
	if !strings.Contains(page, `data-chat-refs data-count="1"`) {
		t.Fatalf("no chat backlink row on the ticket page:\n%s", page)
	}
	if !strings.Contains(page, `href="/r/alpha/chat/`+itoa(posted.Seq)+`"`) {
		t.Fatalf("the backlink row does not permalink the message:\n%s", page)
	}
	// The row is armed like every other reference on the page: its own
	// token, and the page's one set of delegated handlers.
	if !strings.Contains(page, `data-chat-refs data-count="1" data-seq="`+itoa(posted.Seq)+`"`) {
		t.Fatalf("the backlink row carries no preview hook:\n%s", page)
	}
	if !strings.Contains(page, `data-ref="#`+itoa(posted.Seq)+`"`) {
		t.Fatalf("the backlink row carries no reference token:\n%s", page)
	}
	if n := strings.Count(page, "data-on:mouseover__debounce.300ms="); n != 1 {
		t.Fatalf("the page arms the preview %d times, want 1", n)
	}
}

// An artifact the index has never heard of is not a reference: the
// message renders it as the text its author typed, and nothing is
// recorded to point at a page that does not exist.
func TestChatReferenceToNothingIsPlainText(t *testing.T) {
	f := newRefForge(t)
	missing := strings.Repeat("9d", 20)
	code, body := f.do("POST", "/api/v1/r/alpha/chat", "dan",
		url.Values{"text": {"see [" + missing + "] about this"}})
	if code != http.StatusOK {
		t.Fatalf("chat post: status %d (%s)", code, body)
	}
	if n, _, _, err := f.odb.ChatRefs("alpha", missing); err != nil || n != 0 {
		t.Fatalf("a row was written for an artifact nobody has: (%d, %v)", n, err)
	}
	// And ref.resolve refuses it the way it refuses a typo: not found.
	for _, ref := range []string{
		"ticket/" + missing, "checkin/" + missing,
		"landing/" + missing, "forum/" + missing, "wiki/NoSuchPage",
	} {
		if code, _ := f.do("GET", "/api/v1/r/alpha/ref?ref="+url.QueryEscape(ref), "dan", nil); code != http.StatusNotFound {
			t.Fatalf("ref.resolve %q: status %d, want 404", ref, code)
		}
	}
}

// The capability is checked on EVERY branch, the addressed repository
// included. `alpha:ticket/x` was the bypass: it named the reader's own
// repository, so the cap test was skipped and the card came back to a
// reader who could not open the page it previewed.
func TestAddressedRepoIsNotACapBypass(t *testing.T) {
	f := newRefForge(t)
	id := f.newTicket("gated target")
	// Stock category caps hand `r` to everyone; blank them so the
	// account's own letters are the whole answer.
	for _, cat := range []string{"nobody", "anonymous"} {
		if err := f.odb.SetCategoryCaps("alpha", cat, ""); err != nil {
			t.Fatal(err)
		}
	}
	if err := f.odb.AddAccount("carl", "hunter2", false); err != nil {
		t.Fatal(err)
	}
	// Chat but not tickets: carl may read the channel and must NOT
	// learn anything about a ticket from hovering a reference in it.
	if err := f.odb.SetCaps("carl", "alpha", "C"); err != nil {
		t.Fatal(err)
	}
	for _, ref := range []string{"ticket/" + id, "alpha:ticket/" + id} {
		q := "/api/v1/r/alpha/ref?ref=" + url.QueryEscape(ref)
		// Refused, and refused as not-found: a 403 would confirm the
		// ticket exists to someone who may not know that.
		if code, body := f.do("GET", q, "carl", nil); code != http.StatusNotFound {
			t.Fatalf("%q without r: status %d, want 404\n%s", ref, code, body)
		}
		// With `r` the same token resolves — the gate is the letter,
		// not a wall.
		if err := f.odb.SetCaps("carl", "alpha", "Cr"); err != nil {
			t.Fatal(err)
		}
		code, body := f.do("GET", q, "carl", nil)
		if code != http.StatusOK {
			t.Fatalf("%q with r: status %d\n%s", ref, code, body)
		}
		var got struct {
			Kind string `json:"kind"`
			Href string `json:"href"`
		}
		if err := json.Unmarshal([]byte(body), &got); err != nil {
			t.Fatal(err)
		}
		if got.Kind != "ticket" || got.Href != "/r/alpha/ticket/"+id {
			t.Fatalf("%q resolved to %+v", ref, got)
		}
		if err := f.odb.SetCaps("carl", "alpha", "C"); err != nil {
			t.Fatal(err)
		}
	}
	// A person card names capabilities, which is `h` — never anonymous.
	pq := "/api/v1/r/alpha/ref?ref=" + url.QueryEscape("@dan")
	if code, body := f.do("GET", pq, "", nil); code != http.StatusNotFound {
		t.Fatalf("anonymous person card: status %d\n%s", code, body)
	}
	if code, body := f.do("GET", pq, "carl", nil); code != http.StatusNotFound {
		t.Fatalf("person card without h: status %d\n%s", code, body)
	}
	if err := f.odb.SetCaps("carl", "alpha", "Ch"); err != nil {
		t.Fatal(err)
	}
	if code, body := f.do("GET", pq, "carl", nil); code != http.StatusOK {
		t.Fatalf("person card with h: status %d\n%s", code, body)
	}
}

// A bracketed hash resolves to whatever the index says it is, so the
// letter it is gated on must be that kind's. `o` is the check-in
// letter: behind it alone a reader saw a check-in — and, through
// `[<ticket hash>]`, a ticket's title and status too.
func TestBracketedHashIsGatedByTheKindItResolvesTo(t *testing.T) {
	f := newRefForge(t)
	id := f.newTicket("bracketed target")
	for _, cat := range []string{"nobody", "anonymous"} {
		if err := f.odb.SetCategoryCaps("alpha", cat, ""); err != nil {
			t.Fatal(err)
		}
	}
	if err := f.odb.AddAccount("carl", "hunter2", false); err != nil {
		t.Fatal(err)
	}
	// Check-ins but not tickets. `[hash]` must follow the first and
	// refuse the second.
	if err := f.odb.SetCaps("carl", "alpha", "Co"); err != nil {
		t.Fatal(err)
	}
	var tl struct {
		Entries []struct {
			Kind string `json:"kind"`
			Hash string `json:"hash"`
		} `json:"entries"`
	}
	_, body := f.do("GET", "/api/v1/r/alpha/timeline?y=ci", "carl", nil)
	if err := json.Unmarshal([]byte(body), &tl); err != nil {
		t.Fatalf("timeline: %v\n%s", err, body)
	}
	ci := ""
	for _, e := range tl.Entries {
		if e.Kind == "ci" && len(e.Hash) >= 12 {
			ci = e.Hash
			break
		}
	}
	if ci == "" {
		t.Fatal("no check-in to reference")
	}
	q := func(ref string) string { return "/api/v1/r/alpha/ref?ref=" + url.QueryEscape(ref) }
	if code, body := f.do("GET", q("["+id[:12]+"]"), "carl", nil); code != http.StatusNotFound {
		t.Fatalf("bracketed ticket behind o alone: status %d, want 404\n%s", code, body)
	}
	code, body := f.do("GET", q("["+ci[:12]+"]"), "carl", nil)
	if code != http.StatusOK {
		t.Fatalf("bracketed check-in behind o: status %d\n%s", code, body)
	}
	var got struct {
		Kind string `json:"kind"`
		Href string `json:"href"`
	}
	if err := json.Unmarshal([]byte(body), &got); err != nil {
		t.Fatal(err)
	}
	if got.Kind != "checkin" || got.Href != "/r/alpha/checkin/"+ci {
		t.Fatalf("bracketed check-in resolved to %+v", got)
	}
	// With `r` the same bracketed ticket resolves, as a ticket.
	if err := f.odb.SetCaps("carl", "alpha", "Cor"); err != nil {
		t.Fatal(err)
	}
	code, body = f.do("GET", q("["+id[:12]+"]"), "carl", nil)
	if code != http.StatusOK {
		t.Fatalf("bracketed ticket with r: status %d\n%s", code, body)
	}
	got = struct {
		Kind string `json:"kind"`
		Href string `json:"href"`
	}{}
	if err := json.Unmarshal([]byte(body), &got); err != nil {
		t.Fatal(err)
	}
	if got.Kind != "ticket" || got.Href != "/r/alpha/ticket/"+id {
		t.Fatalf("bracketed ticket resolved to %+v", got)
	}
}

func itoa(n uint64) string {
	if n == 0 {
		return "0"
	}
	var b []byte
	for n > 0 {
		b = append([]byte{byte('0' + n%10)}, b...)
		n /= 10
	}
	return string(b)
}