| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // A viewer is not offered an edit affordance the server would refuse. |
| 12 | // |
| 13 | // handleTable admits anyone who may READ — sheetFromURL takes any role — |
| 14 | // while handleTableWrite 404s a viewer or a commenter. So the page told |
| 15 | // every reader "every cell here is editable", gave them a link per cell |
| 16 | // that opened a form, and answered the submit with a 404. Offering an |
| 17 | // affordance and then refusing it is worse than not offering it. |
| 18 | // |
| 19 | // The claim, the anchors and the ?edit= form are all gated now, and the |
| 20 | // gate is a coordinate no cell can equal rather than a flag the template |
| 21 | // is trusted to honour — the same shape the anonymous link view uses, so |
| 22 | // the read-only view cannot grow an edit form by somebody adding a |
| 23 | // branch to the template. |
| 24 | // |
| 25 | // The last assertion is the one that keeps this honest: an owner must |
| 26 | // still get everything, or a gate that is simply stuck closed would pass |
| 27 | // every check above. |
| 28 | func TestTheTableViewOffersAViewerNothingToEdit(t *testing.T) { |
| 29 | app, cookies := signIn(t) |
| 30 | s, _ := newSheet(t, app, cookies, "Read only") |
| 31 | guestOn(t, app, s.ID, "ada@elsewhere.example", SheetViewer) |
| 32 | guestCookies := signInAs(t, app, "ada@elsewhere.example") |
| 33 | |
| 34 | path := fmt.Sprintf("/sheets/%d/table", s.ID) |
| 35 | rec := do(t, app, as(httptest.NewRequest(http.MethodGet, path, nil), guestCookies)) |
| 36 | if rec.Code != http.StatusOK { |
| 37 | t.Fatalf("a viewer cannot open the table view: %d", rec.Code) |
| 38 | } |
| 39 | body := rec.Body.String() |
| 40 | |
| 41 | if strings.Contains(body, "editable") { |
| 42 | t.Error("the table view tells a viewer the cells are editable, and " + |
| 43 | "the write route answers that viewer with a 404") |
| 44 | } |
| 45 | if strings.Contains(body, "edit%3D") || strings.Contains(body, "edit=") { |
| 46 | t.Error("the table view gives a viewer per-cell edit links") |
| 47 | } |
| 48 | |
| 49 | // Asking for the form by hand does not produce one either. |
| 50 | forced := path + "?edit=0,0" |
| 51 | rec = do(t, app, as(httptest.NewRequest(http.MethodGet, forced, nil), guestCookies)) |
| 52 | if rec.Code != http.StatusOK { |
| 53 | t.Fatalf("GET the table view with an edit coordinate as a viewer = %d", rec.Code) |
| 54 | } |
| 55 | if strings.Contains(rec.Body.String(), `name="raw"`) { |
| 56 | t.Error("an edit coordinate handed a viewer a cell form; read-only " + |
| 57 | "must be expressed in the data, not left to the template") |
| 58 | } |
| 59 | |
| 60 | // The owner still gets all of it, or the gate is too wide. |
| 61 | rec = do(t, app, as(httptest.NewRequest(http.MethodGet, path, nil), cookies)) |
| 62 | owner := rec.Body.String() |
| 63 | if !strings.Contains(owner, "editable") { |
| 64 | t.Error("the owner lost the editable claim too — the gate is too wide") |
| 65 | } |
| 66 | if !strings.Contains(owner, "edit=") && !strings.Contains(owner, "edit%3D") { |
| 67 | t.Error("the owner lost the edit links too — the gate is too wide") |
| 68 | } |
| 69 | } |
| 70 | |