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 "encoding/json"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "regexp"
10 "strings"
11 "testing"
12)
13
14// scriptTag and eventAttr strip a page down to what works with
15// JavaScript switched off.
16var (
17 scriptTag = regexp.MustCompile(`(?is)<script\b.*?</script>`)
18 eventAttr = regexp.MustCompile(`(?i)\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)`)
19)
20
21func withoutScript(html string) string {
22 return eventAttr.ReplaceAllString(scriptTag.ReplaceAllString(html, ""), "")
23}
24
25// The gate spec §6 asks for, made concrete: every write the grid can
26// do, the scriptless view can do too — proven by actually doing it,
27// with no JavaScript anywhere in the request.
28func TestACellCanBeEditedWithNoScriptAtAll(t *testing.T) {
29 app, cookies := signIn(t)
30 s, _ := newSheet(t, app, cookies, "Budget")
31 table := fmt.Sprintf("/sheets/%d/table", s.ID)
32
33 form := url.Values{"r": {"0"}, "c": {"0"}, "raw": {"=2*21"}, "page": {"1"}}
34 rec := do(t, app, as(postForm(table, form), cookies))
35 if rec.Code != http.StatusSeeOther {
36 t.Fatalf("scriptless cell edit = %d, want 303: %s", rec.Code, rec.Body.String())
37 }
38
39 page := do(t, app, as(httptest.NewRequest(http.MethodGet, table, nil), cookies)).Body.String()
40 if !strings.Contains(withoutScript(page), ">42<") {
41 t.Fatal("the scriptless table does not show the value it just computed")
42 }
43}
44
45// The two views must be one product. An edit made in either is visible
46// in the other, because both go through the same store call.
47func TestAScriptlessEditIsVisibleInTheGridAPI(t *testing.T) {
48 app, cookies := signIn(t)
49 s, cells := newSheet(t, app, cookies, "Budget")
50
51 form := url.Values{"r": {"3"}, "c": {"2"}, "raw": {"typed here"}, "page": {"1"}}
52 do(t, app, as(postForm(fmt.Sprintf("/sheets/%d/table", s.ID), form), cookies))
53
54 got := decodeGrid(t, do(t, app, as(httptest.NewRequest(http.MethodGet, cells, nil), cookies)))
55 var found bool
56 for _, c := range got.Cells {
57 if c.R == 3 && c.C == 2 && c.Display == "typed here" {
58 found = true
59 }
60 }
61 if !found {
62 t.Fatalf("the grid API does not see the scriptless edit: %+v", got.Cells)
63 }
64}
65
66// A scriptless edit must fan out like any other, or somebody using the
67// fallback is invisible to everybody watching the grid.
68func TestAScriptlessEditReachesOpenGrids(t *testing.T) {
69 app, cookies := signIn(t)
70 s, _ := newSheet(t, app, cookies, "Budget")
71
72 sub := app.hub.Subscribe(s.TabID)
73 defer sub.Close()
74
75 form := url.Values{"r": {"0"}, "c": {"0"}, "raw": {"=3*3"}, "page": {"1"}}
76 do(t, app, as(postForm(fmt.Sprintf("/sheets/%d/table", s.ID), form), cookies))
77
78 select {
79 case m := <-sub.C():
80 var g gridResponse
81 if err := json.Unmarshal(m.Data, &g); err != nil {
82 t.Fatalf("payload does not decode: %v", err)
83 }
84 if len(g.Cells) != 1 || g.Cells[0].Display != "9" {
85 t.Fatalf("fanned out %+v, want one cell displaying 9", g.Cells)
86 }
87 default:
88 t.Fatal("a scriptless edit reached no open grid")
89 }
90}
91
92// Spec §6: paginated, "because a 200,000-cell tab rendered as one HTML
93// table is a several-hundred-megabyte page".
94func TestTableViewIsPaginated(t *testing.T) {
95 app, cookies := signIn(t)
96 s, _ := newSheet(t, app, cookies, "Budget")
97 table := fmt.Sprintf("/sheets/%d/table", s.ID)
98
99 body := do(t, app, as(httptest.NewRequest(http.MethodGet, table, nil), cookies)).Body.String()
100 if n := strings.Count(body, "<tr>"); n > tableRowsPerPage+2 {
101 t.Fatalf("the page renders %d rows, want at most %d plus a header",
102 n, tableRowsPerPage)
103 }
104 if !strings.Contains(body, "Next rows") {
105 t.Error("no way to reach the rows below this page")
106 }
107
108 second := do(t, app, as(httptest.NewRequest(http.MethodGet, table+"?page=2", nil), cookies))
109 if second.Code != http.StatusOK {
110 t.Fatalf("page 2 = %d", second.Code)
111 }
112 // Row 26 is the first row of page two. If both pages showed row 1,
113 // the pagination would be decoration.
114 if !strings.Contains(second.Body.String(), ">26<") {
115 t.Error("page 2 does not start where page 1 ended")
116 }
117}
118
119// The affordance gate. Every page must still offer its actions with
120// every script and inline handler stripped out.
121func TestEveryPageWorksWithoutScript(t *testing.T) {
122 app, cookies := signIn(t)
123 s, _ := newSheet(t, app, cookies, "Budget")
124
125 pages := []struct {
126 path string
127 want []string
128 }{
129 // Sign-out is checked on /sheets rather than on "/": it is
130 // layout chrome, present on every signed-in page, and "/"
131 // stopped being one when it started redirecting onward.
132 //
133 // The list no longer carries name="title": Create is a button
134 // in the top bar that posts an empty form and lands the sheet
135 // as "Untitled sheet". The naming moved to the rename field on
136 // the sheet page, two rows down, which is where the title is
137 // now asserted.
138 {"/sheets", []string{`action="/sheets"`, `type="submit"`, `action="/signout"`}},
139 {"/sheets/import", []string{`action="/sheets/import"`, `type="file"`, `type="submit"`}},
140 {fmt.Sprintf("/sheets/%d", s.ID), []string{
141 fmt.Sprintf("/sheets/%d/table", s.ID),
142 // Rename: the only place a sheet is named now, so it is
143 // the one that has to survive with scripting off.
144 fmt.Sprintf(`action="/sheets/%d/rename"`, s.ID), `name="title"`}},
145 {fmt.Sprintf("/sheets/%d/table", s.ID), []string{"?page=1&amp;edit=0,0", "Next rows"}},
146 {fmt.Sprintf("/sheets/%d/table?page=1&edit=0,0", s.ID),
147 []string{`method="post"`, `name="raw"`, "Save"}},
148 }
149 for _, p := range pages {
150 body := withoutScript(do(t, app,
151 as(httptest.NewRequest(http.MethodGet, p.path, nil), cookies)).Body.String())
152 if strings.Contains(strings.ToLower(body), "<script") {
153 t.Errorf("%s: a script survived stripping", p.path)
154 }
155 for _, want := range p.want {
156 if !strings.Contains(body, want) {
157 t.Errorf("%s: with scripts stripped, %q is gone — that affordance is script-required",
158 p.path, want)
159 }
160 }
161 }
162}
163
164// The edit form must be pre-filled with the cell's INPUT, not its
165// result, or editing a formula silently replaces it with its own
166// output.
167func TestTheEditFormShowsTheFormulaNotTheResult(t *testing.T) {
168 app, cookies := signIn(t)
169 s, _ := newSheet(t, app, cookies, "Budget")
170 table := fmt.Sprintf("/sheets/%d/table", s.ID)
171
172 do(t, app, as(postForm(table, url.Values{"r": {"0"}, "c": {"0"}, "raw": {"=8*8"}, "page": {"1"}}), cookies))
173
174 body := do(t, app, as(httptest.NewRequest(http.MethodGet, table+"?page=1&edit=0,0", nil), cookies)).Body.String()
175 if !strings.Contains(body, `value="=8*8"`) {
176 t.Fatalf("the edit form is not pre-filled with the formula:\n%s",
177 body[max(0, strings.Index(body, "name=\"raw\"")-200):])
178 }
179}
180
181func TestTableViewOfAMissingSheetIs404(t *testing.T) {
182 app, cookies := signIn(t)
183 rec := do(t, app, as(httptest.NewRequest(http.MethodGet, "/sheets/999999/table", nil), cookies))
184 if rec.Code != http.StatusNotFound {
185 t.Fatalf("table view of a missing sheet = %d, want 404", rec.Code)
186 }
187}
188
189func TestTableWriteRequiresASession(t *testing.T) {
190 app, cookies := signIn(t)
191 s, _ := newSheet(t, app, cookies, "Budget")
192 form := url.Values{"r": {"0"}, "c": {"0"}, "raw": {"sneaky"}, "page": {"1"}}
193 rec := do(t, app, postForm(fmt.Sprintf("/sheets/%d/table", s.ID), form))
194 if rec.Code == http.StatusSeeOther {
195 t.Fatal("a scriptless write with no session was accepted")
196 }
197}
198
199func TestColLabel(t *testing.T) {
200 for _, tc := range []struct {
201 n int
202 want string
203 }{{0, "A"}, {1, "B"}, {25, "Z"}, {26, "AA"}, {27, "AB"}, {51, "AZ"}, {52, "BA"}} {
204 if got := colLabel(tc.n); got != tc.want {
205 t.Errorf("colLabel(%d) = %q, want %q", tc.n, got, tc.want)
206 }
207 }
208}
209
210func TestParseEditRejectsRubbish(t *testing.T) {
211 for _, bad := range []string{"", "1", "a,b", "1,", ",2", "1,2,3x"} {
212 if r, c := parseEdit(bad); r != -1 || c != -1 {
213 t.Errorf("parseEdit(%q) = %d,%d, want no cell selected", bad, r, c)
214 }
215 }
216 if r, c := parseEdit("3,4"); r != 3 || c != 4 {
217 t.Errorf("parseEdit(\"3,4\") = %d,%d", r, c)
218 }
219}
220