package server import ( "encoding/json" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "git.pablomurad.com/pablo/patchwork/internal/config" "git.pablomurad.com/pablo/patchwork/internal/render" ) func testTree() *render.Tree { cfg := config.Default() m := &render.Model{Cfg: cfg, Versions: map[string][]*render.Version{}} m.Pages = []render.PageDetail{{PageSummary: render.PageSummary{Slug: "notes/deep", Title: "Deep"}}} m.Versions["notes/deep"] = []*render.Version{{Number: 1, LatestNumber: 1, Baseline: true, Title: "Deep", BodyMarkdown: "Hello."}} t, err := m.Tree() if err != nil { panic(err) } return t } func newServer() *Server { s := New(slog.New(slog.NewTextHandler(io.Discard, nil))) s.Swap(testTree()) return s } func get(s *Server, path string, hdr ...string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, path, nil) for i := 0; i+1 < len(hdr); i += 2 { req.Header.Set(hdr[i], hdr[i+1]) } rec := httptest.NewRecorder() s.ServeHTTP(rec, req) return rec } func TestRoutesAndTypes(t *testing.T) { s := newServer() cases := map[string]string{ "/api/v1/pages": "application/json", "/api/v1/pages.json": "application/json", "/api/v1/revisions": "application/json", "/api/v1/pages/notes/deep": "application/json", "/api/v1/pages/notes/deep.json": "application/json", "/api/v1/pages/notes/deep/versions/1": "application/json", "/feeds/revisions.atom": "application/atom+xml", "/feeds/revisions.json": "application/feed+json", "/feeds/pages/notes/deep.atom": "application/atom+xml", "/text/revisions": "text/plain", "/text/pages/notes/deep/history": "text/plain", "/text/pages/notes/deep/versions/1": "text/plain", "/healthz": "text/plain", } for path, ctype := range cases { rec := get(s, path) if rec.Code != http.StatusOK || !strings.HasPrefix(rec.Header().Get("Content-Type"), ctype) { t.Errorf("%s: %d %s", path, rec.Code, rec.Header().Get("Content-Type")) } } } func TestNotFoundShape(t *testing.T) { s := newServer() for _, path := range []string{"/nope", "/api/v1/pages/missing", "/api/v1/pages/notes/deep/versions/9", "/text/pages/notes/deep/versions/x"} { rec := get(s, path) var e render.Error if rec.Code != http.StatusNotFound || json.Unmarshal(rec.Body.Bytes(), &e) != nil || e.Error.Code != "not_found" { t.Errorf("%s: %d %s", path, rec.Code, rec.Body) } } req := httptest.NewRequest(http.MethodPost, "/api/v1/pages", nil) rec := httptest.NewRecorder() s.ServeHTTP(rec, req) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("POST: %d", rec.Code) } } func TestETag(t *testing.T) { s := newServer() first := get(s, "/feeds/revisions.atom") etag := first.Header().Get("ETag") if etag == "" { t.Fatal("no ETag") } if rec := get(s, "/feeds/revisions.atom", "If-None-Match", etag); rec.Code != http.StatusNotModified || rec.Body.Len() != 0 { t.Fatalf("conditional GET: %d", rec.Code) } if rec := get(s, "/feeds/revisions.atom", "If-None-Match", `"other", W/`+etag); rec.Code != http.StatusNotModified { t.Fatalf("weak/list match: %d", rec.Code) } } func TestNotReady(t *testing.T) { s := New(slog.New(slog.NewTextHandler(io.Discard, nil))) if rec := get(s, "/healthz"); rec.Code != http.StatusServiceUnavailable { t.Fatalf("healthz before first build: %d", rec.Code) } } func TestTextOptions(t *testing.T) { s := newServer() colored := get(s, "/text/revisions").Body.String() plain := get(s, "/text/revisions?plain&w=60").Body.String() if !strings.Contains(colored, "\x1b[") || strings.Contains(plain, "\x1b[") { t.Fatal("?plain should drop ANSI") } }