package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
func idemRegistry(t *testing.T) (*ops.Registry, *oltp.DB) {
t.Helper()
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Origin: "https://forge.example",
Repos: testRepos(wikiIndex(t)), Local: db, Idem: db}); err != nil {
t.Fatal(err)
}
return r, db
}
// A filing retried with the same key returns the ticket the first one
// filed, with its link — an agent whose connection dropped does not
// file twice. The same key for a different ticket is refused; no key,
// or another caller's key, files as before.
func TestAFilingRetriedWithItsKeyFilesOnce(t *testing.T) {
r, db := idemRegistry(t)
dan := ops.WithUser(context.Background(), "dan")
file := func(ctx context.Context, title, key string) (ticketIntakeOut, error) {
out, err := r.Invoke(ctx, "ticket.intake", ticketIntakeIn{Repo: "test", Title: title, IdempotencyKey: key})
if err != nil {
return ticketIntakeOut{}, err
}
return out.(ticketIntakeOut), nil
}
first, err := file(dan, "broken", "k1")
if err != nil {
t.Fatal(err)
}
again, err := file(dan, "broken", "k1")
if err != nil || again.ID != first.ID {
t.Fatalf("FAIL: a retry filed %q, first was %q (%v)", again.ID, first.ID, err)
}
if first.URL != "https://forge.example/r/test/ticket/"+first.ID {
t.Fatalf("FAIL: the answer's url is %q", first.URL)
}
if _, err := file(dan, "something else", "k1"); !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("FAIL: the same key for a different ticket answered %v", err)
}
eve := ops.WithUser(context.Background(), "eve")
if other, err := file(eve, "broken", "k1"); err != nil || other.ID == first.ID {
t.Fatalf("FAIL: another caller's key reached dan's ticket (%q, %v)", other.ID, err)
}
if _, err := file(dan, "broken", ""); err != nil {
t.Fatal(err)
}
all, _ := db.LocalTickets("test")
if len(all) != 3 {
t.Fatalf("FAIL: %d tickets on file, want 3 (dan once for k1, eve, dan without a key)", len(all))
}
t.Log("ok: one ticket for a retried key, with its url; a different request refused; another caller and no key file afresh")
}
// The key rides the conventional header, through the router.
func TestTheIdempotencyKeyHeaderReachesTheOp(t *testing.T) {
r, db := idemRegistry(t)
h, err := Router(r, "v")
if err != nil {
t.Fatal(err)
}
var ids []string
for i := 0; i < 2; i++ {
req := httptest.NewRequest("POST", "/api/v1/r/test/ticket/intake", strings.NewReader(`{"title":"from an agent"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "agent-run-7")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("FAIL: %d %s", rec.Code, rec.Body)
}
var out ticketIntakeOut
json.Unmarshal(rec.Body.Bytes(), &out)
ids = append(ids, out.ID)
}
all, _ := db.LocalTickets("test")
if ids[0] == "" || ids[0] != ids[1] || len(all) != 1 {
t.Fatalf("FAIL: two posts with one header filed %v (%d on file)", ids, len(all))
}
t.Log("ok: two posts with one Idempotency-Key header, one ticket")
}
// ticket.config describes filing field by field, so an agent fills the
// form without guessing: what is required, the words, the defaults.
func TestTicketConfigDescribesFiling(t *testing.T) {
r, _ := idemRegistry(t)
out, err := r.Invoke(context.Background(), "ticket.config", ticketConfigIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
by := map[string]ticketInput{}
for _, in := range out.(ticketConfigOut).Inputs {
by[in.Name] = in
}
if !by["title"].Required || by["title"].MaxLength != 200 {
t.Errorf("FAIL: title %+v", by["title"])
}
if by["type"].Kind != "choice" || len(by["type"].Words) == 0 || by["status"].Default != "Open" {
t.Errorf("FAIL: type %+v, status %+v", by["type"], by["status"])
}
if by["comment"].Kind != "markdown" || by["idempotencyKey"].Name == "" || by["fossil"].Kind != "boolean" {
t.Errorf("FAIL: comment %+v, key %+v, fossil %+v", by["comment"], by["idempotencyKey"], by["fossil"])
}
for name := range by {
if strings.HasPrefix(name, "fields.+") {
t.Errorf("FAIL: an append-syntax column is offered: %s", name)
}
}
t.Logf("ok: %d inputs; title required, type a choice, status defaults to Open, comment markdown, retry key described", len(by))
}