Artifact [3e0be12f5c]
Not logged in

Artifact 3e0be12f5c633df50aa3df2178e747ae9be3962954c4f22eccc8dac6c54a651c:


package web

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

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

// The paste-and-drop control under a composer follows the same answer
// the section's form does — the family's letter and a bound bucket —
// and is absent for anyone else. It names the repository and the
// family the script begins deferred tickets with at submit, the
// pending list the script fills, and the error line it reports to.
func TestComposerControlFollowsTheLetterAndTheBinding(t *testing.T) {
	if attachComposerFor(nil, "") != nil {
		t.Errorf("FAIL: a viewer with nothing to offer got a composer control")
	}
	if attachComposerFor(&attachWrite{Repo: "tk", Family: "forum", CanMod: true}, "") != nil {
		t.Errorf("FAIL: a moderator without the upload letter got a composer control")
	}
	if attachComposerFor(&attachWrite{Repo: "tk", Family: "forum", Unbound: true}, "") != nil {
		t.Errorf("FAIL: an administrator with no bucket got a composer control")
	}
	c := attachComposerFor(&attachWrite{Repo: "tk", Family: "wiki", CanAttach: true, Limit: "2.0 MB"}, "Design")
	if c == nil || c.Repo != "tk" || c.Family != "wiki" || c.Target != "Design" || c.Limit != "2.0 MB" {
		t.Fatalf("FAIL: control = %+v", c)
	}
	render := func(page interface {
		ExecuteTemplate(io.Writer, string, any) error
	}, data pageData) string {
		var buf bytes.Buffer
		if err := page.ExecuteTemplate(&buf, "content", data); err != nil {
			t.Fatal(err)
		}
		return buf.String()
	}
	forum := &attachComposer{Repo: "tk", Family: "forum", Limit: "2.0 MB"}
	hooks := []string{`data-hook="attach-composer"`, `data-repo="tk"`, `data-family="forum"`,
		`data-hook="attach-pending"`, `data-hook="attach-error"`, `data-drop="ready"`}
	newThread := render(forumNewPage, pageData{Repo: "tk", AttachComposer: forum})
	for _, want := range hooks {
		if !strings.Contains(newThread, want) {
			t.Errorf("FAIL: the new-thread composer lacks %s", want)
		}
	}
	// Drawn hidden: with no script there is nothing a person can do
	// with it, and the script reveals it once it can hash and PUT.
	if !strings.Contains(newThread, `data-drop="ready" hidden>`) {
		t.Errorf("FAIL: the control is not drawn hidden")
	}
	if without := render(forumNewPage, pageData{Repo: "tk"}); strings.Contains(without, `data-hook="attach-composer"`) {
		t.Errorf("FAIL: without the letter, the new-thread page carries the control")
	}
	thread := render(forumThreadPage, pageData{Repo: "tk", ForumRoot: "abcd", AttachComposer: forum})
	if !strings.Contains(thread, `data-hook="attach-composer"`) || !strings.Contains(thread, `data-family="forum"`) {
		t.Errorf("FAIL: the reply composer lacks the control")
	}
	wiki := render(wikiEditPage, pageData{Repo: "tk", Wiki: wikiDetail{Title: "Design"}, AttachComposer: c})
	if !strings.Contains(wiki, `data-hook="attach-composer"`) || !strings.Contains(wiki, `data-family="wiki"`) || !strings.Contains(wiki, `data-target="Design"`) {
		t.Errorf("FAIL: the wiki editor's control does not name its page")
	}
	if strings.Contains(newThread, `data-target=`) {
		t.Errorf("FAIL: a composer for an object not yet written names a target")
	}
	// The ticket page's description and comment boxes both take a paste.
	page := render(ticketPage, pageData{Repo: "tk", Ticket: ticketDetail{ID: tkID}, CanWrite: true, AttachTarget: tkID,
		AttachComposer: &attachComposer{Repo: "tk", Family: "ticket", Target: tkID, Limit: "2.0 MB"}})
	if n := strings.Count(page, `data-hook="attach-composer" data-repo="tk" data-family="ticket" data-target="`+tkID+`"`); n != 2 {
		t.Errorf("FAIL: %d controls on the ticket page, wanted the description and the comment", n)
	}
	ticket := render(ticketNewPage, pageData{Repo: "tk", CanAdmin: true, Vocab: map[string][]string{},
		AttachComposer: &attachComposer{Repo: "tk", Family: "ticket", Limit: "2.0 MB"}})
	if !strings.Contains(ticket, `data-hook="attach-composer"`) || !strings.Contains(ticket, `data-family="ticket"`) {
		t.Errorf("FAIL: the new-ticket form lacks the control")
	}
	if plain := render(ticketNewPage, pageData{Repo: "tk", Vocab: map[string][]string{}}); strings.Contains(plain, `data-hook="attach-composer"`) {
		t.Errorf("FAIL: a filer without the control's answer sees it")
	}
	t.Log("ok: the composer control follows the letter and the binding on every composer")
}

// A section a viewer may upload to says so for the script's drop
// handling; one they may not does not.
func TestSectionIsADropTarget(t *testing.T) {
	r := attachStub(t, mixedFiles(), nil)
	cards := attachCards(context.Background(), r, "tk", mixedRows())
	with := renderTicketAttachments(t, cards, &attachWrite{Repo: "tk", Family: "ticket", CanAttach: true, Limit: "1 MB", Back: "/r/tk/ticket/abab#attachments"})
	if !strings.Contains(with, `data-target="`+tkID+`" data-family="ticket" data-drop="ready">`) {
		t.Errorf("FAIL: the section a holder of the letter sees is not a drop target")
	}
	without := renderTicketAttachments(t, cards, nil)
	if strings.Contains(without, `data-drop=`) {
		t.Errorf("FAIL: a reader's section is a drop target")
	}
	t.Log("ok: the section carries the script's drop hook for a holder of the letter only")
}

type composerStubIn struct {
	Repo   string   `json:"repo"`
	Root   string   `json:"root"`
	Name   string   `json:"name"`
	ID     string   `json:"id"`
	Title  string   `json:"title"`
	Body   string   `json:"body"`
	Attach []string `json:"attach"`
}

type composerStubOut struct {
	ID   string `json:"id"`
	Hash string `json:"hash"`
}

// The composer POSTs hand every `attach` value to their op; a new
// ticket filed into the local tier is refused with files rather than
// filed without them, since only a fossil ticket carries an A card.
func TestComposerPostsForwardAttachTickets(t *testing.T) {
	// The refusal page reads the version through the registry like
	// every page; the view registry carries that stub.
	r := viewRegistry(t, nil)
	got := map[string][]string{}
	for _, name := range []string{"forum.post", "forum.reply", "ticket.new", "ticket.intake", "ticket.set", "wiki.save"} {
		name := name
		if err := ops.Register(r, ops.Op[composerStubIn, composerStubOut]{
			Name: name, Caps: "3n", Doc: "test stub", Kind: ops.Command, Auth: ops.Token,
			Route: "POST /api/v1/r/{repo}/stub/" + name,
			Run: func(_ context.Context, in composerStubIn) (composerStubOut, error) {
				got[name] = in.Attach
				return composerStubOut{ID: "t1", Hash: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}, 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(path string, form url.Values) int {
		res, err := client.PostForm(srv.URL+path, form)
		if err != nil {
			t.Fatal(err)
		}
		io.Copy(io.Discard, res.Body)
		res.Body.Close()
		return res.StatusCode
	}
	tickets := url.Values{"attach": {"tk1", "tk2"}}
	thread := url.Values{"title": {"hello"}, "body": {"x"}, "attach": tickets["attach"]}
	if code := post("/r/tk/forum-new", thread); code != http.StatusSeeOther || strings.Join(got["forum.post"], ",") != "tk1,tk2" {
		t.Errorf("FAIL: forum-new = %d, attach %v", code, got["forum.post"])
	}
	reply := url.Values{"body": {"x"}, "attach": tickets["attach"]}
	if code := post("/r/tk/forum/abcd/reply", reply); code != http.StatusSeeOther || strings.Join(got["forum.reply"], ",") != "tk1,tk2" {
		t.Errorf("FAIL: reply = %d, attach %v", code, got["forum.reply"])
	}
	fossil := url.Values{"title": {"broken"}, "fossil": {"1"}, "attach": tickets["attach"]}
	if code := post("/r/tk/ticket/new", fossil); code != http.StatusSeeOther || strings.Join(got["ticket.new"], ",") != "tk1,tk2" {
		t.Errorf("FAIL: ticket/new into the fossil = %d, attach %v", code, got["ticket.new"])
	}
	local := url.Values{"title": {"broken"}, "attach": tickets["attach"]}
	if code := post("/r/tk/ticket/new", local); code != http.StatusBadRequest || got["ticket.intake"] != nil {
		t.Errorf("FAIL: a local ticket with files = %d, intake saw %v", code, got["ticket.intake"])
	}
	if code := post("/r/tk/ticket/new", url.Values{"title": {"broken"}}); code != http.StatusSeeOther || len(got["ticket.intake"]) != 0 {
		t.Errorf("FAIL: a local ticket without files = %d, attach %v", code, got["ticket.intake"])
	}
	if code := post("/r/tk/wiki/Design/edit", url.Values{"body": {"x"}, "attach": tickets["attach"]}); code != http.StatusSeeOther || strings.Join(got["wiki.save"], ",") != "tk1,tk2" {
		t.Errorf("FAIL: wiki edit = %d, attach %v", code, got["wiki.save"])
	}
	t.Log("ok: the composer POSTs forward the tickets; a local ticket refuses them")
}