package api
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
func localDeps(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", Repos: testRepos(wikiIndex(t)), Local: db}); err != nil {
t.Fatal(err)
}
return r, db
}
func TestLocalTicketIntakeListGetSet(t *testing.T) {
r, _ := localDeps(t)
ctx := ops.WithUser(context.Background(), "visitor")
got, err := r.Invoke(ctx, "ticket.intake", ticketIntakeIn{
Repo: "test", Title: "crash on empty input", Type: "Code_Defect",
Comment: "steps: run with no args"})
if err != nil {
t.Fatal(err)
}
id := got.(ticketIntakeOut).ID
if len(id) != 40 {
t.Fatalf("id = %q", id)
}
// The listing carries the row marked local, above the archive.
lo, err := r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
tl := lo.(ticketListOut)
if tl.Count != 1 || !tl.Tickets[0].Local || tl.Tickets[0].Title != "crash on empty input" {
t.Fatalf("list = %+v", tl)
}
if lo, _ = r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "test", Origin: "fossil"}); lo.(ticketListOut).Count != 0 {
t.Fatal("origin=fossil still listed the local row")
}
if lo, _ = r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "test", Origin: "local"}); lo.(ticketListOut).Count != 1 {
t.Fatal("origin=local lost the local row")
}
// Cursor pages stay purely fossil.
if lo, _ = r.Invoke(ctx, "ticket.list", ticketListIn{Repo: "test", Offset: 10}); lo.(ticketListOut).Count != 0 {
t.Fatal("a deep page carried the intake queue")
}
// ticket.get serves the same shape either tier.
go1, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "test", ID: id[:12]})
if err != nil {
t.Fatal(err)
}
tg := go1.(ticketGetOut)
if !tg.Local || tg.Ticket.Description != "steps: run with no args" || tg.DescriptionHTML == "" {
t.Fatalf("get = %+v", tg)
}
if fieldValue(tg.Ticket.Fields, "status") != "Open" || fieldValue(tg.Ticket.Fields, "comment") != "" {
t.Fatalf("fields = %+v", tg.Ticket.Fields)
}
// ticket.set falls through to the local tier with fossil semantics.
if _, err := r.Invoke(ops.WithUser(ctx, "dan"), "ticket.set", ticketSetIn{
Repo: "test", ID: id[:12], Status: "Verified", Comment: "reproduced"}); err != nil {
t.Fatal(err)
}
go1, _ = r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "test", ID: id[:12]})
tg = go1.(ticketGetOut)
if fieldValue(tg.Ticket.Fields, "status") != "Verified" || len(tg.Comments) != 1 {
t.Fatalf("after set = %+v", tg)
}
if !strings.Contains(string(tg.Comments[0].HTML), "reproduced") {
t.Fatalf("comment html = %q", tg.Comments[0].HTML)
}
}
func TestLocalTicketDiscardOp(t *testing.T) {
r, _ := localDeps(t)
ctx := ops.WithUser(context.Background(), "dan")
got, err := r.Invoke(ctx, "ticket.intake", ticketIntakeIn{Repo: "test", Title: "buy things"})
if err != nil {
t.Fatal(err)
}
id := got.(ticketIntakeOut).ID
if _, err := r.Invoke(ctx, "ticket.discard", ticketDiscardIn{Repo: "test", ID: id[:12]}); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "test", ID: id[:12]}); !errors.Is(err, ops.ErrNotFound) {
t.Fatalf("discarded ticket: %v", err)
}
if _, err := r.Invoke(ctx, "ticket.discard", ticketDiscardIn{Repo: "test", ID: "deadbeef"}); !errors.Is(err, ops.ErrNotFound) {
t.Fatalf("discard of nothing: %v", err)
}
}
func TestLocalTicketPromoteRefusals(t *testing.T) {
r, db := localDeps(t)
ctx := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(ctx, "ticket.promote", ticketPromoteIn{Repo: "test", ID: "deadbeef"}); !errors.Is(err, ops.ErrNotFound) {
t.Fatalf("promote of nothing: %v", err)
}
got, err := r.Invoke(ctx, "ticket.intake", ticketIntakeIn{Repo: "test", Title: "worthy"})
if err != nil {
t.Fatal(err)
}
id := got.(ticketIntakeOut).ID
// The test repo has no zit client; promotion says so instead of
// panicking. (The happy path writes real artifacts and lives in the
// harness.)
if _, err := r.Invoke(ctx, "ticket.promote", ticketPromoteIn{Repo: "test", ID: id[:12]}); !errors.Is(err, ops.ErrUnavailable) {
t.Fatalf("promote without zit: %v", err)
}
// A promoted id answers ticket.get with the redirect and refuses a
// second promotion.
claim, _, err := db.LocalTicketClaim("test", id)
if err != nil {
t.Fatal(err)
}
if err := db.LocalTicketPromoted("test", id, "f0551171", claim); err != nil {
t.Fatal(err)
}
go1, err := r.Invoke(ctx, "ticket.get", ticketGetIn{Repo: "test", ID: id[:12]})
if err != nil {
t.Fatal(err)
}
if tg := go1.(ticketGetOut); tg.PromotedTo != "f0551171" || tg.Local {
t.Fatalf("redirect = %+v", tg)
}
if _, err := r.Invoke(ctx, "ticket.promote", ticketPromoteIn{Repo: "test", ID: id[:12]}); !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("second promote: %v", err)
}
}
func TestLocalTicketIntakeNeedsAStore(t *testing.T) {
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil {
t.Fatal(err)
}
_, err := r.Invoke(context.Background(), "ticket.intake", ticketIntakeIn{Repo: "test", Title: "x"})
if !errors.Is(err, ops.ErrUnavailable) {
t.Fatalf("intake without oltp: %v", err)
}
}