Skip to content
quarry Artifact [3a7054a33a]
Login

Artifact [3a7054a33a]

Artifact 3a7054a33a01cc1b261a778f371cc19814c51cd9a5cb77e085ff5359a54ff8c8:


package harness

import (
	"encoding/json"
	"io"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

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

// A store ticket carries its number in its own history (54c5175f): one
// filed through the forge has it from its first artifact; ones that
// predate numbering are backfilled by an admin, who first sees what
// would be written; a second run finds nothing to do.
func TestAStoreTicketCarriesItsNumber(t *testing.T) {
	store := buildStore(t, "num")
	zrun(t, store, "ticket", "new", "--user", "dan", "title=older one", "status=Open")
	zrun(t, store, "ticket", "new", "--user", "dan", "title=newer one", "status=Open")
	srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, t.TempDir(),
		func(repo, user, pass string, need ops.Auth) api.Decision { return api.Allowed })
	call := func(method, path, body string) map[string]any {
		t.Helper()
		req, _ := http.NewRequest(method, srv.URL+path, strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.SetBasicAuth("dan", "pw")
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("%s %s: %d %s", method, path, resp.StatusCode, b)
		}
		var out map[string]any
		json.Unmarshal(b, &out)
		return out
	}
	dry := call("POST", "/api/v1/r/alpha/ticket/numbers/record", `{}`)
	have := dry["missing"].(float64)
	if have < 2 || dry["written"].(float64) != 0 {
		t.Fatalf("FAIL: a dry run answered %v", dry)
	}
	wrote := call("POST", "/api/v1/r/alpha/ticket/numbers/record", `{"write":true}`)
	if wrote["written"].(float64) != have {
		t.Fatalf("FAIL: the backfill answered %v", wrote)
	}
	if again := call("POST", "/api/v1/r/alpha/ticket/numbers/record", `{}`); again["missing"].(float64) != 0 {
		t.Fatalf("FAIL: after the backfill %v are still missing", again["missing"])
	}
	filed := call("POST", "/api/v1/r/alpha/ticket", `{"title":"filed after numbering"}`)
	next := int(have) + 1
	if int(filed["number"].(float64)) != next {
		t.Fatalf("FAIL: the new ticket is %v, want #%d", filed["number"], next)
	}
	got := call("GET", "/api/v1/r/alpha/ticket/"+strconv.Itoa(next), "")
	fields := map[string]string{}
	for _, f := range got["ticket"].(map[string]any)["fields"].([]any) {
		m := f.(map[string]any)
		fields[m["name"].(string)] = m["value"].(string)
	}
	if fields["number"] != strconv.Itoa(next) || fields["title"] != "filed after numbering" {
		t.Fatalf("FAIL: #%d opened %v", next, fields)
	}
	t.Logf("ok: dry run %v missing, backfill wrote them, rerun 0; the new ticket is #%d from its first artifact and opens by number", have, next)
}

// A ticket that arrives by push after numbering began gets the next
// number, and the forge writes it into the ticket on its own, so the
// clone it came from learns the name on its next pull (54c5175f).
func TestAPushedTicketRecordsItsNumber(t *testing.T) {
	store := buildStore(t, "pushed")
	srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, t.TempDir(),
		func(repo, user, pass string, need ops.Auth) api.Decision { return api.Allowed })
	call := func(method, path, body string) map[string]any {
		t.Helper()
		req, _ := http.NewRequest(method, srv.URL+path, strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.SetBasicAuth("dan", "pw")
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("%s %s: %d %s", method, path, resp.StatusCode, b)
		}
		var out map[string]any
		json.Unmarshal(b, &out)
		return out
	}
	list := call("GET", "/api/v1/r/alpha/ticket?state=all", "") // the first pass
	before := len(list["tickets"].([]any))
	// As if pushed from a clone: written straight into the store.
	zrun(t, store, "ticket", "new", "--user", "eve", "title=filed on a clone", "status=Open")
	// Any write re-reads the store; filing one more through the API is
	// the ordinary way the pushed ticket is first seen.
	call("POST", "/api/v1/r/alpha/ticket", `{"title":"filed on the forge"}`)
	list = call("GET", "/api/v1/r/alpha/ticket?state=all", "")
	var id string
	var n int
	for _, row := range list["tickets"].([]any) {
		m := row.(map[string]any)
		if m["title"] == "filed on a clone" {
			id, n = m["id"].(string), int(m["number"].(float64))
		}
	}
	// The forge-filed ticket took #before+1 as it was written; the
	// pushed one is seen by the re-read that write made, so it is next.
	if id == "" || n != before+2 {
		t.Fatalf("FAIL: the pushed ticket is #%d (%q), want #%d", n, id, before+2)
	}
	deadline := time.Now().Add(10 * time.Second)
	for {
		got := call("GET", "/api/v1/r/alpha/ticket/"+id, "")
		for _, f := range got["ticket"].(map[string]any)["fields"].([]any) {
			m := f.(map[string]any)
			if m["name"] == "number" && m["value"] == strconv.Itoa(n) {
				t.Logf("ok: the pushed ticket became #%d and the forge wrote number=%d into it", n, n)
				return
			}
		}
		if time.Now().After(deadline) {
			t.Fatalf("FAIL: #%d was never written into the pushed ticket", n)
		}
		time.Sleep(200 * time.Millisecond)
	}
}