main pico / pkg / apps / pastes / api_test.go
Antonio Mika  ·  2026-10-10
  1package pastes
  2
  3import (
  4	"html/template"
  5	"log/slog"
  6	"net/http"
  7	"net/http/httptest"
  8	"strings"
  9	"testing"
 10	"time"
 11
 12	"github.com/picosh/pico/pkg/db"
 13	"github.com/picosh/pico/pkg/db/stub"
 14	"github.com/picosh/pico/pkg/shared"
 15	"github.com/picosh/pico/pkg/shared/router"
 16)
 17
 18type pasteDB struct {
 19	*stub.StubDB
 20	post *db.Post
 21	// user and slug are the last ones asked for.
 22	user, slug string
 23}
 24
 25func (m *pasteDB) FindUserByName(name string) (*db.User, error) {
 26	m.user = name
 27	return &db.User{ID: "user-1", Name: name}, nil
 28}
 29
 30func (m *pasteDB) FindPostWithSlug(slug string, _ string, _ string) (*db.Post, error) {
 31	m.slug = slug
 32	return m.post, nil
 33}
 34
 35func TestLargePasteEscapesHTML(t *testing.T) {
 36	t.Chdir("../../..")
 37	logger := slog.New(slog.DiscardHandler)
 38	cfg := &Config{
 39		Domain: "pastes.test", Protocol: "https", Space: "pastes", Logger: logger,
 40	}
 41	now := time.Now()
 42	text := strings.Repeat("x", shared.MB) + "\n<script>alert(1)</script>\n<img src=x onerror=alert(1)>\n& \" '"
 43	dbpool := &pasteDB{
 44		StubDB: stub.NewStubDB(logger),
 45		post: &db.Post{
 46			Filename: "test.html", Slug: "test.html", Username: "tester",
 47			Text: text, FileSize: len(text), PublishAt: &now, UpdatedAt: &now,
 48		},
 49	}
 50	web := &webRouter{cfg: cfg, db: dbpool}
 51	handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
 52
 53	page := httptest.NewRecorder()
 54	handler.ServeHTTP(page, httptest.NewRequest(http.MethodGet, "https://tester.pastes.test/test.html", nil))
 55	if page.Code != http.StatusOK {
 56		t.Fatalf("page status = %d", page.Code)
 57	}
 58	want := "<pre>" + template.HTMLEscapeString(text) + "</pre>"
 59	if !strings.Contains(page.Body.String(), want) {
 60		t.Fatal("page does not contain the escaped paste in a pre element")
 61	}
 62	if strings.Contains(page.Body.String(), "<script>alert(1)</script>") {
 63		t.Fatal("page contains the uploaded script as executable HTML")
 64	}
 65
 66	raw := httptest.NewRecorder()
 67	handler.ServeHTTP(raw, httptest.NewRequest(http.MethodGet, "https://tester.pastes.test/test.html/raw", nil))
 68	if raw.Code != http.StatusOK || raw.Body.String() != text {
 69		t.Fatal("raw response does not preserve the uploaded text")
 70	}
 71}
 72
 73func TestRoutes(t *testing.T) {
 74	t.Chdir("../../..")
 75	logger := slog.New(slog.DiscardHandler)
 76	cfg := &Config{
 77		Domain: "pastes.test", Protocol: "https", Space: "pastes", Logger: logger,
 78	}
 79	now := time.Now()
 80	dbpool := &pasteDB{
 81		StubDB: stub.NewStubDB(logger),
 82		post: &db.Post{
 83			Filename: "a.txt", Slug: "a.txt", Username: "tester",
 84			Text: "paste", FileSize: 5, PublishAt: &now, UpdatedAt: &now,
 85		},
 86	}
 87	web := &webRouter{cfg: cfg, db: dbpool}
 88	handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
 89
 90	for _, tt := range []struct {
 91		url    string
 92		status int
 93		raw    bool
 94		user   string
 95		slug   string
 96	}{
 97		{url: "https://pastes.test/tester/a.txt", status: http.StatusOK, user: "tester", slug: "a.txt"},
 98		{url: "https://pastes.test/tester/a.txt/raw", status: http.StatusOK, raw: true, user: "tester", slug: "a.txt"},
 99		{url: "https://pastes.test/raw/tester/a.txt", status: http.StatusOK, raw: true, user: "tester", slug: "a.txt"},
100		// Both raw forms fit, and the trailing one wins.
101		{url: "https://pastes.test/raw/tester/raw", status: http.StatusOK, raw: true, user: "raw", slug: "tester"},
102		{url: "https://pastes.test/tester/a.txt/other", status: http.StatusNotFound},
103		{url: "https://tester.pastes.test/a.txt", status: http.StatusOK, user: "tester", slug: "a.txt"},
104		{url: "https://tester.pastes.test/a.txt/raw", status: http.StatusOK, raw: true, user: "tester", slug: "a.txt"},
105		{url: "https://tester.pastes.test/raw/a.txt", status: http.StatusOK, raw: true, user: "tester", slug: "a.txt"},
106		{url: "https://tester.pastes.test/a.txt/other", status: http.StatusNotFound},
107	} {
108		dbpool.user, dbpool.slug = "", ""
109		res := httptest.NewRecorder()
110		handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, tt.url, nil))
111		raw := res.Body.String() == "paste"
112		if res.Code != tt.status || dbpool.user != tt.user || dbpool.slug != tt.slug || (res.Code == http.StatusOK && raw != tt.raw) {
113			t.Errorf("%s = %d raw %v for %q/%q, want %d raw %v for %q/%q",
114				tt.url, res.Code, raw, dbpool.user, dbpool.slug, tt.status, tt.raw, tt.user, tt.slug)
115		}
116	}
117}