code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

internal/server/server_test.go raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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")
	}
}