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}