Skip to content
quarry Artifact [805794ff9c]
Login

Artifact [805794ff9c]

Artifact 805794ff9c163637325527b75eb784fe6fe6b33276bc0155358a0efd306d7ebf:


package harness

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

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

// The whole landing lifecycle through quarry's own surfaces, against a real
// store and a real zit: open a branch for landing, discuss it, and settle it.
// Every step mints ordinary fossil artifacts, so the proof that this works is
// the same proof that a stock fossil clone would still read the repository.
func TestLandingLifecycleThroughTheForge(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	store := buildStore(t, "flowseed")
	// A branch worth landing: a real edit, on its own branch.
	if err := os.WriteFile(filepath.Join(store, "feature.txt"),
		[]byte("the work\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, store, "add", "feature.txt")
	zrun(t, store, "commit", "-m", "work on the branch", "--branch", "feat")

	srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, t.TempDir(),
		func(repo, user, pass string, _ ops.Auth) api.Decision {
			if user == "dan" && pass == "hunter2" {
				return api.Allowed
			}
			return api.Denied
		})
	post := func(path, body string) (int, string) {
		t.Helper()
		req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.SetBasicAuth("dan", "hunter2")
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		return resp.StatusCode, string(b)
	}

	code, body := post("/api/v1/r/alpha/landing",
		`{"branch":"feat","target":"trunk","message":"land the feat work"}`)
	if code != http.StatusOK {
		t.Fatalf("opening a landing: %d %s", code, body)
	}
	var opened struct {
		ID string `json:"id"`
	}
	json.Unmarshal([]byte(body), &opened)
	if opened.ID == "" {
		t.Fatalf("no record id came back: %s", body)
	}

	// It is a forum thread, so it reads as one — the compatibility
	// promise, checked rather than asserted.
	req, _ := http.NewRequest("GET", srv.URL+"/api/v1/r/alpha/forum/"+opened.ID, nil)
	req.SetBasicAuth("dan", "hunter2")
	resp, _ := srv.Client().Do(req)
	if resp != nil {
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			t.Errorf("the record is not readable as an ordinary forum thread: %d", resp.StatusCode)
		}
	}

	if code, body := post("/api/v1/r/alpha/landing/"+opened.ID[:12]+"/comment",
		`{"message":"looks right to me"}`); code != http.StatusOK {
		t.Fatalf("commenting: %d %s", code, body)
	}

	// Settling it. reject writes no check-in, so it needs nothing more
	// than the capability to speak.
	if code, body := post("/api/v1/r/alpha/landing/"+opened.ID[:12]+"/reject",
		`{"message":"not this week"}`); code != http.StatusOK {
		t.Fatalf("rejecting: %d %s", code, body)
	}

	// A settled record does not reopen, and settling it again is a
	// NO-OP rather than an error: already-rejected is the end state the
	// caller asked for, so reporting failure would make a double press
	// look broken when the landing is exactly as they wanted.
	//
	// What must not happen is a second state reply. That is the bug this
	// pins — the guard used to read index state that lagged the store,
	// so a second press inside one ingest round wrote another reply, and
	// zit does not refuse those either.
	reCode, reBody := post("/api/v1/r/alpha/landing/"+opened.ID[:12]+"/reject",
		`{"message":"again"}`)
	if reCode != http.StatusOK {
		t.Errorf("re-settling a closed record: %d %s, want 200", reCode, reBody)
	}
	if !strings.Contains(reBody, `"state":"rejected"`) {
		t.Errorf("re-settle should report the settled state, got %s", reBody)
	}
	if !strings.Contains(reBody, `"hash":""`) {
		t.Errorf("re-settle wrote a reply rather than doing nothing: %s", reBody)
	}

	// Accept: the one op that MERGES. It writes a check-in to the
	// target, which is why it takes `i` and why nothing else here does.
	// Against a bare store this is zit#131's path — the forge has no
	// checkout, and until that shipped this could not run at all.
	if err := os.WriteFile(filepath.Join(store, "second.txt"),
		[]byte("more work\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, store, "update", "trunk")
	zrun(t, store, "add", "second.txt")
	zrun(t, store, "commit", "-m", "second branch work", "--branch", "feat2")
	code, body = post("/api/v1/r/alpha/landing",
		`{"branch":"feat2","target":"trunk","message":"land the second lot"}`)
	if code != http.StatusOK {
		t.Fatalf("opening the second landing: %d %s", code, body)
	}
	var two struct {
		ID string `json:"id"`
	}
	json.Unmarshal([]byte(body), &two)
	if code, body := post("/api/v1/r/alpha/landing/"+two.ID[:12]+"/accept",
		`{"message":"landing it"}`); code != http.StatusOK {
		t.Fatalf("accepting: %d %s", code, body)
	}
	// The branch is on trunk now — the whole point of the ceremony.
	merged := zout(t, store, "timeline", "-n", "5")
	if !strings.Contains(merged, "second branch work") && !strings.Contains(merged, "feat2") {
		t.Errorf("the accepted branch does not appear on trunk:\n%s", merged)
	}

	// And the listing agrees about where it ended up.
	req, _ = http.NewRequest("GET", srv.URL+"/api/v1/r/alpha/landing?state=rejected", nil)
	req.SetBasicAuth("dan", "hunter2")
	resp, err := srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	if !strings.Contains(string(b), opened.ID) {
		t.Errorf("the rejected record is not listed as rejected: %s", b)
	}
}

// zout runs a zit verb and returns its output, for the assertions that have
// to ask the STORE what happened rather than trusting the forge's own report.
func zout(t *testing.T, dir string, args ...string) string {
	t.Helper()
	cmd := exec.Command("zit", args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("zit %v: %v\n%s", args, err, out)
	}
	return string(out)
}