Skip to content
quarry Artifact [16a2dbe883]
Login

Artifact [16a2dbe883]

Artifact 16a2dbe883a10594f39d7ef276c92ddab79d13e5bb692834fb9c458ff13bbf55:


package web

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

// The filing form reads as the mockup does: type pills from the
// tracker's own words, a title, the Markdown composer, the triage
// fields as chips whose unset state says what it would set, the
// context the bug button brought, and one primary submit.
func TestTheFilingFormIsQuickToRead(t *testing.T) {
	vocab := map[string][]string{
		"status":   {"Open", "Fixed"},
		"type":     {"Code_Defect", "Feature_Request"},
		"severity": {"Critical", "Minor"},
		"priority": {"High", "Low"},
	}
	var b strings.Builder
	err := ticketNewPage.ExecuteTemplate(&b, "content", pageData{Repo: "alpha", CanAdmin: true, Vocab: vocab,
		ZitVersion: "0.3.28", Origin: "https://forge.example",
		Prefill: ticketPrefill{Type: "Code_Defect", Severity: "Minor", Seen: "https://forge.example/r/alpha/timeline"}})
	if err != nil {
		t.Fatal(err)
	}
	got := b.String()
	for _, want := range []string{
		`data-hook="tn-types"`, `value="Code_Defect" checked>Code defect`, `value="Feature_Request">Feature request`,
		`class="tn-title" name="title" required`,
		`<textarea name="comment"`,
		`data-hook="tn-chips"`, `<select name="severity"`, `<option value="Minor" selected>Minor</option>`,
		`<select name="priority"`, `<option value="">+ Priority</option>`,
		`data-field="foundin"`,
		`data-hook="tn-context"`, `name="seen" value="https://forge.example/r/alpha/timeline"`, `name="seen_zit" value="0.3.28"`,
		`data-hook="tn-dest"`, `name="another"`, `data-hook="tn-submit"`, `data-hook="tn-curl"`,
	} {
		if !strings.Contains(got, want) {
			t.Errorf("FAIL: the form lacks %s", want)
		}
	}
	if strings.Contains(got, `<select name="status"><option value="">`) {
		t.Error("FAIL: status offers no status, which a ticket cannot hold")
	}
	var plain strings.Builder
	if err := ticketNewPage.ExecuteTemplate(&plain, "content", pageData{Repo: "alpha", Vocab: vocab}); err != nil {
		t.Fatal(err)
	}
	if strings.Contains(plain.String(), `data-hook="tn-context"`) || strings.Contains(plain.String(), `data-hook="tn-dest"`) {
		t.Error("FAIL: a plain visit shows a context row, or a non-admin is offered the permanent history")
	}
	t.Log("ok: type pills, title, composer, chips with + Priority, context, destination for an admin, one submit")
}

// "+name" is fossil's append syntax, never a column: a tracker that
// recorded one must not grow a "+comment" box on the form.
func TestAnAppendFieldIsNotOffered(t *testing.T) {
	for name, want := range map[string]bool{"assignee": true, "+comment": false, "comment": false, "severity": false} {
		if offeredField(name) != want {
			t.Errorf("FAIL: offeredField(%q) = %v", name, !want)
		}
	}
}

type ticketStubIn struct {
	Repo      string `json:"repo"`
	Title     string `json:"title"`
	Comment   string `json:"comment"`
	Type      string `json:"type"`
	Severity  string `json:"severity"`
	Status    string `json:"status"`
	Priority  string `json:"priority"`
	Subsystem string `json:"subsystem"`
	FoundIn   string `json:"foundin"`
}

// Filing: the context the form showed is a footer under what was
// written, only while ticked; "Create another" comes back to a fresh
// form that keeps the fields and says the last one landed.
func TestFilingKeepsContextAndCanComeBackForMore(t *testing.T) {
	r := viewRegistry(t, nil)
	var last ticketStubIn
	if err := ops.Register(r, ops.Op[ticketStubIn, composerStubOut]{
		Name: "ticket.intake", Caps: "n", Doc: "test stub", Kind: ops.Command, Auth: ops.Anonymous,
		Route: "POST /api/v1/r/{repo}/stub/intake",
		Run: func(_ context.Context, in ticketStubIn) (composerStubOut, error) {
			last = in
			return composerStubOut{ID: "abc123"}, nil
		},
	}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
	defer srv.Close()
	client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
	post := func(form url.Values) (int, string) {
		res, err := client.PostForm(srv.URL+"/r/tk/ticket/new", form)
		if err != nil {
			t.Fatal(err)
		}
		io.Copy(io.Discard, res.Body)
		res.Body.Close()
		return res.StatusCode, res.Header.Get("Location")
	}
	code, loc := post(url.Values{"title": {"broken"}, "comment": {"it broke"}, "type": {"Code_Defect"}, "severity": {"Minor"},
		"seen": {"https://forge.example/r/tk/timeline"}, "seen_zit": {"0.3.28"}, "ctx": {"1"}, "another": {"1"}})
	if code != http.StatusSeeOther || last.Comment != "it broke\n\n---\nSeen on https://forge.example/r/tk/timeline ยท zit 0.3.28" {
		t.Fatalf("FAIL: %d, comment %q", code, last.Comment)
	}
	u, _ := url.Parse(loc)
	if u.Path != "/r/tk/ticket/new" || u.Query().Get("opened") != "abc123" || u.Query().Get("type") != "Code_Defect" || u.Query().Get("severity") != "Minor" || u.Query().Get("title") != "" {
		t.Fatalf("FAIL: create another went to %q", loc)
	}
	_, loc = post(url.Values{"title": {"broken"}, "comment": {"it broke"}, "seen": {"https://forge.example/x"}})
	if last.Comment != "it broke" || loc != "/r/tk/ticket/abc123" {
		t.Fatalf("FAIL: unticked context was filed (%q), or a single ticket did not open (%q)", last.Comment, loc)
	}
	t.Log("ok: ticked context filed as a footer, unticked left out; create another keeps type and severity; a single ticket opens")
}