| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "net/http" |
| 6 | "net/http/httptest" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | func nativeGet(path string) *http.Request { |
| 14 | r := httptest.NewRequest("GET", path, nil) |
| 15 | r.Header.Set("Accept", "application/json") |
| 16 | return r |
| 17 | } |
| 18 | func TestNativeSheetPayloadUsesExistingPermissions(t *testing.T) { |
| 19 | a, cookies := signIn(t) |
| 20 | s, cells := newSheet(t, a, cookies, "Native secret title") |
| 21 | postCell(t, a, cookies, cells, `{"r":0,"c":0,"raw":"=2+3"}`) |
| 22 | for _, path := range []string{"/sheets", "/sheets/" + strconv.FormatUint(s.ID, 10), "/sheets/" + strconv.FormatUint(s.ID, 10) + "/history", "/sheets/" + strconv.FormatUint(s.ID, 10) + "/comments", "/sheets/" + strconv.FormatUint(s.ID, 10) + "/table"} { |
| 23 | rec := do(t, a, as(nativeGet(path), cookies)) |
| 24 | if rec.Code != 200 || !json.Valid(rec.Body.Bytes()) || rec.Header().Get("Cache-Control") != "no-store" { |
| 25 | t.Fatalf("native payload %s: %d", path, rec.Code) |
| 26 | } |
| 27 | if strings.Contains(rec.Body.String(), "<svg") { |
| 28 | t.Fatal("native payload includes HTML thumbnail") |
| 29 | } |
| 30 | } |
| 31 | rec := do(t, a, nativeGet("/sheets/"+strconv.FormatUint(s.ID, 10))) |
| 32 | if rec.Code == 200 { |
| 33 | t.Fatal("anonymous native sheet read succeeded") |
| 34 | } |
| 35 | } |
| 36 | func TestNativeListPreviewContainsShapeWithoutCellContents(t *testing.T) { |
| 37 | a, cookies := signIn(t) |
| 38 | _, cells := newSheet(t, a, cookies, "Workbook") |
| 39 | secret := "private-cell-preview-sentinel" |
| 40 | postCell(t, a, cookies, cells, `{"r":0,"c":0,"raw":"`+secret+`"}`) |
| 41 | rec := do(t, a, as(nativeGet("/sheets"), cookies)) |
| 42 | if rec.Code != 200 || strings.Contains(rec.Body.String(), secret) { |
| 43 | t.Fatal("native preview leaked a cell or failed") |
| 44 | } |
| 45 | var payload struct{ Sheets []struct{ Preview preview } } |
| 46 | if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | if len(payload.Sheets) != 1 || payload.Sheets[0].Preview.Empty || len(payload.Sheets[0].Preview.Cells) != 1 { |
| 50 | t.Fatal("native preview did not include the real cell shape") |
| 51 | } |
| 52 | if rec := do(t, a, nativeGet("/sheets")); rec.Code == 200 { |
| 53 | t.Fatal("unsigned list exposed previews") |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | func TestNativeCreateAndHistoryCommandsAnswerJSON(t *testing.T) { |
| 58 | a, cookies := signIn(t) |
| 59 | r := postForm("/sheets", map[string][]string{"title": {"Created natively"}}) |
| 60 | r.Header.Set("Accept", "application/json") |
| 61 | rec := do(t, a, as(r, cookies)) |
| 62 | if rec.Code != 201 { |
| 63 | t.Fatalf("create=%d", rec.Code) |
| 64 | } |
| 65 | var s SheetSummary |
| 66 | if err := json.Unmarshal(rec.Body.Bytes(), &s); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | path := "/sheets/" + strconv.FormatUint(s.ID, 10) |
| 70 | r = postForm(path+"/rename", map[string][]string{"title": {"Renamed natively"}}) |
| 71 | r.Header.Set("Accept", "application/json") |
| 72 | rec = do(t, a, as(r, cookies)) |
| 73 | if rec.Code != 200 || !json.Valid(rec.Body.Bytes()) { |
| 74 | t.Fatalf("rename=%d", rec.Code) |
| 75 | } |
| 76 | r = postForm(path+"/undo", nil) |
| 77 | r.Header.Set("Accept", "application/json") |
| 78 | rec = do(t, a, as(r, cookies)) |
| 79 | if !strings.Contains(rec.Body.String(), `"nothing"`) { |
| 80 | t.Fatalf("undo did not preserve notice: %s", rec.Body.String()) |
| 81 | } |
| 82 | } |
| 83 | func TestNativeNegotiationDoesNotExposeAuthPayloads(t *testing.T) { |
| 84 | a, _ := signIn(t) |
| 85 | r := nativeGet("/signin") |
| 86 | rec := do(t, a, r) |
| 87 | if strings.Contains(rec.Header().Get("Content-Type"), "application/json") { |
| 88 | t.Fatal("auth page was serialized") |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | func TestNativeStreamRechecksRevokedSession(t *testing.T) { |
| 93 | a, cookies := signIn(t) |
| 94 | s, _ := newSheet(t, a, cookies, "Private stream") |
| 95 | srv := liveServer(t, a) |
| 96 | response := openStream(t, srv, s, cookies) |
| 97 | if response.StatusCode != 200 { |
| 98 | t.Fatal(response.StatusCode) |
| 99 | } |
| 100 | if err := a.db.Exec("DELETE FROM sessions").Error; err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | a.publish(s.TabID, gridResponse{Version: 1, Cells: []gridCell{{R: 0, C: 0, Raw: "must-not-leak", Display: "must-not-leak"}}}) |
| 104 | event := readEventWithin(t, response.Body, 2*time.Second) |
| 105 | if !strings.Contains(event, "access-changed") || strings.Contains(event, "must-not-leak") { |
| 106 | t.Fatalf("unexpected event: %s", event) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | func TestNativeGridWindowKeepsOriginalCoordinates(t *testing.T) { |
| 111 | a, cookies := signIn(t) |
| 112 | _, cells := newSheet(t, a, cookies, "Window") |
| 113 | postCell(t, a, cookies, cells, `{"r":30,"c":27,"raw":"=7*8"}`) |
| 114 | rec := do(t, a, as(nativeGet(cells+"?firstRow=25&firstCol=26&rows=25&cols=25"), cookies)) |
| 115 | value := decodeGrid(t, rec) |
| 116 | if len(value.Cells) != 1 || value.Cells[0].R != 30 || value.Cells[0].C != 27 || value.Cells[0].Display != "56" { |
| 117 | t.Fatalf("window lost source coordinate: %+v", value.Cells) |
| 118 | } |
| 119 | rec = do(t, a, as(nativeGet(cells+"?firstRow=-1"), cookies)) |
| 120 | if rec.Code != 400 { |
| 121 | t.Fatal("negative offset accepted") |
| 122 | } |
| 123 | } |
| 124 | |