main pico / pkg / apps / prose / api_test.go
Antonio Mika  ·  2026-10-11
 1package prose
 2
 3import (
 4	"log/slog"
 5	"net/http"
 6	"net/http/httptest"
 7	"strings"
 8	"testing"
 9
10	"github.com/picosh/pico/pkg/db"
11	"github.com/picosh/pico/pkg/db/stub"
12	"github.com/picosh/pico/pkg/postfs"
13	"github.com/picosh/pico/pkg/shared/router"
14	"github.com/picosh/pico/pkg/storage"
15)
16
17func TestRoutes(t *testing.T) {
18	t.Chdir("../../..")
19	t.Setenv("PICO_PIPE_ENABLED", "false")
20	logger := slog.New(slog.DiscardHandler)
21	user := &db.User{ID: "user-1", Name: "tester"}
22	posts := stub.NewPostsDB(logger, user)
23	cfg := NewConfig("prose-test")
24	cfg.Logger = logger
25	for _, name := range []string{"hello", "raw"} {
26		post := postfs.NewPost(name+".md", []byte("# "+name))
27		post.UserID, post.Space, post.Title = user.ID, cfg.Space, name
28		if _, err := posts.InsertPost(post); err != nil {
29			t.Fatal(err)
30		}
31	}
32	st, err := storage.NewStorageMemory(map[string]map[string]string{})
33	if err != nil {
34		t.Fatal(err)
35	}
36	web := &webRouter{cfg: cfg, db: posts, storage: st}
37	handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
38
39	site := "https://tester." + cfg.Domain
40	for _, tt := range []struct {
41		url    string
42		status int
43		// body is what the response holds, "" for anything.
44		body string
45		// exact means the response is body and nothing else.
46		exact bool
47	}{
48		{url: site + "/hello", status: http.StatusOK, body: "<html"},
49		{url: site + "/hello.html", status: http.StatusOK, body: "<html"},
50		{url: site + "/hello.md", status: http.StatusOK, body: "# hello"},
51		{url: site + "/hello.lxt", status: http.StatusOK, body: "# hello"},
52		{url: site + "/raw/hello", status: http.StatusOK, body: "# hello"},
53		{url: site + "/raw", status: http.StatusOK, body: "<html"},
54		{url: site + "/raw.md", status: http.StatusOK, body: "# raw"},
55		{url: site + "/raw/raw", status: http.StatusOK, body: "# raw"},
56		{url: site + "/missing", status: http.StatusNotFound},
57		// Images are served from the user's bucket, which they don't have.
58		{url: site + "/cat.png", status: http.StatusUnprocessableEntity, body: "bucket does not exist"},
59		{url: site + "/cat.png/s:500", status: http.StatusUnprocessableEntity, body: "bucket does not exist"},
60		// A site serves its own robots.txt, which this user hasn't uploaded.
61		{url: site + "/robots.txt", status: http.StatusOK, exact: true},
62		{url: "https://" + cfg.Domain + "/robots.txt", status: http.StatusOK, body: "User-agent"},
63		{url: site + "/read.css", status: http.StatusOK},
64		{url: "https://" + cfg.Domain + "/read.css", status: http.StatusOK},
65		{url: "https://" + cfg.Domain + "/hello", status: http.StatusNotFound},
66	} {
67		res := httptest.NewRecorder()
68		handler.ServeHTTP(res, httptest.NewRequest(http.MethodGet, tt.url, nil))
69		got := res.Body.String()
70		if res.Code != tt.status || !strings.Contains(got, tt.body) || (tt.exact && got != tt.body) {
71			t.Errorf("%s = %d %.80q, want %d %q", tt.url, res.Code, got, tt.status, tt.body)
72		}
73	}
74}