Skip to content
quarry Artifact [e290d7b9e9]
Login

Artifact [e290d7b9e9]

Artifact e290d7b9e9bb06d2659db2eb06250756a7d5bb50a3707bfcde1c9104e561a2d5:


package web

import (
	"bytes"
	"context"
	"net/http"
	"strings"
	"testing"
	"time"
)

// The tier leads the row: a local ticket's first chip says so, because
// "not yet in the permanent history" changes what every other chip on
// the row means.
func TestLocalChipLeadsTheRow(t *testing.T) {
	rows := ticketRows("tk", []ticketSummary{
		{ID: "aaaa11112222", Title: "from a visitor", Status: "Open", Local: true},
	}, nil)
	if len(rows) != 1 || len(rows[0].Chips) == 0 || rows[0].Chips[0].Text != "local" {
		t.Fatalf("rows = %+v", rows)
	}
}

// is:local is where a reader will try to spell the tier, so the query
// box honours it — both directions.
func TestIsLocalToken(t *testing.T) {
	v := ticketParseQuery("is:local crash")
	if v.Get("origin") != "local" || v.Get("title") != "crash" || v.Get("state") != "" {
		t.Fatalf("parse = %v", v)
	}
	rep := ticketReportOf(v, append([]string{"origin"}, ticketAxes...))
	if got := ticketQueryString("", rep, "crash"); got != "is:local crash" {
		t.Fatalf("round trip = %q", got)
	}
}

// A LOCAL TICKET IS MARKED ON ITS TITLE, AND PROMOTED ONLY ON CONFIRMATION.
//
// The banner that led a local ticket's page pushed the ticket itself
// below the fold on a phone (dmestas, 2026-09-23). The title carries a
// small local icon instead; Promote and Discard sit in the side panel,
// each behind a disclosure that says what it will do, and only for an
// admin.
func TestALocalTicketIsMarkedAndPromotedOnConfirm(t *testing.T) {
	base := pageData{Repo: "tk", Ticket: ticketDetail{ID: strings.Repeat("ab", 20)}, TicketLocal: true}
	var buf bytes.Buffer
	admin := base
	admin.CanAdmin = true
	if err := ticketPage.ExecuteTemplate(&buf, "content", admin); err != nil {
		t.Fatal(err)
	}
	html := buf.String()
	if strings.Contains(html, `class="tkt-local"`) {
		t.Error("FAIL: the banner is back")
	}
	for _, want := range []string{`data-hook="ticket-local"`, `data-hook="ticket-kept"`} {
		if !strings.Contains(html, want) {
			t.Errorf("FAIL: admin view missing %s", want)
		}
	}
	for _, act := range []string{"promote", "discard"} {
		i := strings.Index(html, `data-hook="ticket-`+act+`"`)
		if i < 0 {
			t.Errorf("FAIL: no %s disclosure", act)
			continue
		}
		box := html[i:]
		box = box[:strings.Index(box, "</details>")]
		if !strings.Contains(box, "<summary>") || !strings.Contains(box, "/"+act+`"`) {
			t.Errorf("FAIL: the %s form is not behind its confirming disclosure", act)
		}
	}
	buf.Reset()
	if err := ticketPage.ExecuteTemplate(&buf, "content", base); err != nil {
		t.Fatal(err)
	}
	html = buf.String()
	if !strings.Contains(html, `data-hook="ticket-local"`) || strings.Contains(html, "/promote") || strings.Contains(html, "/discard") {
		t.Error("FAIL: a non-admin saw promote or discard, or no local mark")
	}
}

// The new-ticket form carries the honeypot and defaults to the local
// door; only a repository ADMIN is offered the fossil checkbox — a
// writer files locally and an admin promotes what belongs.
func TestNewTicketFormDoors(t *testing.T) {
	var buf bytes.Buffer
	if err := ticketNewPage.ExecuteTemplate(&buf, "content", pageData{Repo: "tk", CanWrite: true, CanAdmin: true}); err != nil {
		t.Fatal(err)
	}
	html := buf.String()
	for _, want := range []string{`name="website"`, `name="fossil"`, "start local"} {
		if !strings.Contains(html, want) {
			t.Fatalf("admin form missing %q", want)
		}
	}
	buf.Reset()
	if err := ticketNewPage.ExecuteTemplate(&buf, "content", pageData{Repo: "tk", CanWrite: true}); err != nil {
		t.Fatal(err)
	}
	if strings.Contains(buf.String(), `name="fossil"`) {
		t.Fatalf("a writer is offered the fossil checkbox")
	}
	buf.Reset()
	if err := ticketNewPage.ExecuteTemplate(&buf, "content", pageData{Repo: "tk"}); err != nil {
		t.Fatal(err)
	}
	if strings.Contains(buf.String(), `name="fossil"`) {
		t.Fatal("a visitor was offered the fossil door")
	}
}

// freshIntake empties the anonymous-filing limiter, which is package
// state: without it a repeated run (-count) spends the last run's budget.
func freshIntake(t *testing.T) {
	t.Helper()
	reset := func() {
		intakeMu.Lock()
		intakeSeen = map[string][]time.Time{}
		intakeMu.Unlock()
	}
	reset()
	t.Cleanup(reset)
}

func TestIntakeRateLimit(t *testing.T) {
	freshIntake(t)
	now := time.Now()
	addr := "test-10.9.9.9"
	for i := 0; i < 5; i++ {
		if !intakeAllowed(addr, now) {
			t.Fatalf("filing %d refused", i+1)
		}
	}
	if intakeAllowed(addr, now) {
		t.Fatal("the sixth filing inside an hour passed")
	}
	if !intakeAllowed(addr, now.Add(2*time.Hour)) {
		t.Fatal("the window never reopened")
	}
	if !intakeAllowed("test-other", now) {
		t.Fatal("one loud address silenced another")
	}
}

func TestRemoteAddrBelievesOnlyTheTrustedProxysLastHop(t *testing.T) {
	req, _ := http.NewRequest("GET", "/", nil)
	req.RemoteAddr = "127.0.0.1:9999"
	req.Header.Set("X-Forwarded-For", "10.9.9.9, 203.0.113.9")
	if remoteAddr(req) != "127.0.0.1" {
		t.Fatalf("FAIL: an untrusted X-Forwarded-For moved the address to %q", remoteAddr(req))
	}
	req = req.WithContext(context.WithValue(req.Context(), ctxProxy{}, TrustedProxy("X-Forwarded-For")))
	if remoteAddr(req) != "203.0.113.9" {
		t.Fatalf("FAIL: trusted chain gave %q, want the hop the proxy appended", remoteAddr(req))
	}
	t.Log("ok: the address is the socket, or the trusted proxy's own hop")
}