oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8 "time"
9)
10
11// listBody fetches the sheet list as the signed-in viewer.
12func listBody(t *testing.T, app *App, cookies []*http.Cookie) string {
13 t.Helper()
14 rec := do(t, app, as(httptest.NewRequest(http.MethodGet, "/sheets", nil), cookies))
15 if rec.Code != http.StatusOK {
16 t.Fatalf("GET /sheets = %d", rec.Code)
17 }
18 return rec.Body.String()
19}
20
21// firstPreview cuts the first thumbnail out of a rendered list.
22//
23// It anchors on the SLOT and not on "<svg", which is the mistake this
24// helper exists to stop anybody making twice: the layout's account menu
25// renders a chevron icon further up the page, so a search for the first
26// <svg> finds that one, and every assertion about the thumbnail then
27// passes or fails on the wrong element.
28func firstPreview(t *testing.T, page string) string {
29 t.Helper()
30 i := strings.Index(page, "data-preview-slot")
31 if i < 0 {
32 t.Fatal("the list rendered no thumbnail slot at all, so this test proves nothing")
33 }
34 rest := page[i:]
35 start := strings.Index(rest, "<svg")
36 end := strings.Index(rest, "</svg>")
37 if start < 0 || end < 0 {
38 t.Fatalf("the thumbnail slot holds no SVG: %.200s", rest)
39 }
40 return rest[start : end+6]
41}
42
43// TestAPreviewNeverCarriesACell is the gate on the property the whole
44// design rests on.
45//
46// The thumbnail is built from which cells EXIST and what formats they
47// carry — never from a cell's value. That is what makes it safe to
48// cache, safe to render on every list load, and safe from the one rule
49// without needing an exemption or an argument. A refactor that reached
50// for the value to make the bars prettier would break it silently: the
51// picture would look better and nobody would notice what it now
52// contained.
53//
54// The fixture words are deliberately unusual. "3228.33" would be a
55// weak canary — a coordinate or a width could produce those digits by
56// accident — so the values here are strings nothing else in an SVG of
57// rectangles could plausibly emit.
58func TestAPreviewNeverCarriesACell(t *testing.T) {
59 app, cookies := signIn(t)
60 _, cells := newSheet(t, app, cookies, "Ledger")
61
62 secrets := []string{"Zanzibar", "Quixotry", "Vellichor"}
63 for i, word := range secrets {
64 body := `{"r":` + itoa(i) + `,"c":0,"raw":"` + word + `"}`
65 if rec := postCell(t, app, cookies, cells, body); rec.Code != http.StatusOK {
66 t.Fatalf("write %q = %d", word, rec.Code)
67 }
68 }
69
70 svg := firstPreview(t, listBody(t, app, cookies))
71
72 for _, word := range secrets {
73 if strings.Contains(svg, word) {
74 t.Errorf("the thumbnail contains the cell value %q — a preview is built "+
75 "from the sheet's shape, never its contents, and that is the whole "+
76 "reason it needs no seal reasoning", word)
77 }
78 }
79 // It must still be a picture OF something: a test that passes
80 // because nothing is drawn would pass forever.
81 if strings.Count(svg, "<rect") < 4 {
82 t.Errorf("the thumbnail has %d rects for a sheet with three filled cells — "+
83 "it is not drawing the shape it is supposed to", strings.Count(svg, "<rect"))
84 }
85}
86
87// itoa is strconv.Itoa for a single digit, which is all this file
88// needs; a full import for three row numbers is noise.
89func itoa(i int) string { return string(rune('0' + i)) }
90
91// TestAnEmptySheetPreviewsAsAnEmptyBox pins the honest answer for a
92// sheet with nothing in it: the box, not a fabricated picture.
93func TestAnEmptySheetPreviewsAsAnEmptyBox(t *testing.T) {
94 app, cookies := signIn(t)
95 newSheet(t, app, cookies, "Nothing yet")
96
97 svg := firstPreview(t, listBody(t, app, cookies))
98 if n := strings.Count(svg, "<rect"); n != 1 {
99 t.Errorf("an empty sheet drew %d rects, want 1 (the border box) — a preview "+
100 "that invents content is worse than one that admits there is none", n)
101 }
102}
103
104// TestThePreviewCacheHitsAndThenMisses is the test that would have
105// caught the trap Docs named: comparing time.Time with == instead of
106// comparing UnixNano int64s.
107//
108// A cache that never hits is invisible — every load simply re-renders
109// and looks correct — so the only way to know is to ask it.
110func TestThePreviewCacheHitsAndThenMisses(t *testing.T) {
111 c := newPreviewCache()
112 at := time.Now().UTC()
113
114 c.put(7, at.UnixNano(), "<svg/>")
115
116 if _, ok := c.get(7, at.UnixNano()); !ok {
117 t.Error("a cache entry did not hit for the timestamp it was stored under — " +
118 "which is what happens when times are compared with == rather than by " +
119 "UnixNano, and it fails silently by re-rendering forever")
120 }
121 if _, ok := c.get(7, at.Add(time.Nanosecond).UnixNano()); ok {
122 t.Error("an entry hit for a NEWER timestamp — an edit would not invalidate it, " +
123 "and the list would show a preview of the sheet as it used to be")
124 }
125 if _, ok := c.get(8, at.UnixNano()); ok {
126 t.Error("an entry hit for a different sheet")
127 }
128}
129
130// TestThePreviewCacheIsBounded proves the eviction is real. An unbounded
131// map keyed on sheet id is a slow leak, and the only symptom is memory.
132func TestThePreviewCacheIsBounded(t *testing.T) {
133 c := newPreviewCache()
134 for i := 0; i < maxPreviewEntries+10; i++ {
135 c.put(uint64(i), int64(i), "<svg/>")
136 }
137 c.mu.RLock()
138 n := len(c.at)
139 c.mu.RUnlock()
140 if n > maxPreviewEntries {
141 t.Errorf("the cache holds %d entries, past its %d bound", n, maxPreviewEntries)
142 }
143}
144
145// TestAFillReachesThePreview checks the picture actually carries the
146// formatting it claims to — including that a colour this app applied is
147// emitted as an intent, so the thumbnail follows the reader's theme the
148// way the grid does.
149func TestAFillReachesThePreview(t *testing.T) {
150 app, cookies := signIn(t)
151 s, _ := newSheet(t, app, cookies, "Coloured")
152
153 if rec := postFormat(t, app, cookies, s, `{"r1":0,"c1":0,"r2":0,"c2":2,"fill":"green"}`); rec.Code != http.StatusOK {
154 t.Fatalf("fill = %d: %s", rec.Code, rec.Body.String())
155 }
156
157 svg := firstPreview(t, listBody(t, app, cookies))
158 if !strings.Contains(svg, "oklch(var(--sheet-fill-l)") {
159 t.Error("the thumbnail has no theme-resolved fill — a colour this app applied " +
160 "is hue and chroma with no lightness, so a baked hex here would be a " +
161 "light-mode swatch sitting on a dark list")
162 }
163}
164
165// TestSafeHexRefusesWhatIsNotAColour guards the one place a value from
166// an uploaded document reaches an SVG attribute.
167func TestSafeHexRefusesWhatIsNotAColour(t *testing.T) {
168 for _, bad := range []string{
169 "", "red", "#FFEB9C", "FFEB9", "FFEB9CC", "url(x)", `"/><script>`,
170 "FFEB9G", "oklch(1 0 0)",
171 } {
172 if got := safeHex(bad); got != "" {
173 t.Errorf("safeHex(%q) = %q, want empty", bad, got)
174 }
175 }
176 if got := safeHex("FFEB9C"); got != "#FFEB9C" {
177 t.Errorf("safeHex(6 digits) = %q", got)
178 }
179 // XLSX writes ARGB; the alpha is dropped, as it is everywhere else
180 // this app handles a fill hex.
181 if got := safeHex("FFFFEB9C"); got != "#FFEB9C" {
182 t.Errorf("safeHex(8 digits) = %q", got)
183 }
184}
185