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 "strings"
9 "testing"
10
11 "amadan.net/oficina/sheets/internal/sheets/store"
12)
13
14func TestCellBatchPublishesOnlyFinalCommittedValues(t *testing.T) {
15 app, cookies := signIn(t)
16 s, path := newSheet(t, app, cookies, "Budget")
17 sub := app.hub.Subscribe(s.TabID)
18 defer sub.Close()
19 rec := postCell(t, app, cookies, path+"/batch", `{"cells":[{"r":1,"c":0,"raw":"=A1*2"},{"r":0,"c":0,"raw":"21"}],"client":"batch-client"}`)
20 if rec.Code != http.StatusOK {
21 t.Fatalf("batch %d: %s", rec.Code, rec.Body.String())
22 }
23 got := decodeGrid(t, rec)
24 if len(got.Cells) != 2 || got.Client != "batch-client" {
25 t.Fatalf("response %+v", got)
26 }
27 for _, c := range got.Cells {
28 if c.R == 1 && c.Display != "42" {
29 t.Fatalf("stale formula %+v", c)
30 }
31 }
32 select {
33 case m := <-sub.C():
34 if string(m.Data) != strings.TrimSpace(rec.Body.String()) {
35 t.Fatalf("publication differs: %s", m.Data)
36 }
37 default:
38 t.Fatal("no publication")
39 }
40 select {
41 case <-sub.C():
42 t.Fatal("intermediate publication")
43 default:
44 }
45 var events int64
46 app.db.Model(&store.Event{}).Where("tab_id = ? AND kind = ?", s.TabID, store.KindInput).Count(&events)
47 if events != 2 {
48 t.Fatalf("input events = %d", events)
49 }
50}
51
52func TestCellBatchRejectsInvalidRequestsWithoutWrites(t *testing.T) {
53 for _, body := range []string{
54 `{}`, `{"cells":[]}`, `{"cells":[{"r":0,"c":0,"raw":"7"}]} {}`,
55 `{"cells":[{"r":0,"c":0,"raw":"7"},{"r":0,"c":0,"raw":"8"}]}`,
56 `{"cells":[{"r":0,"c":0,"raw":"7"},{"r":-1,"c":0,"raw":"8"}]}`,
57 `{"cells":[{"r":0,"c":0,"raw":"7"},{"r":1,"c":0,"raw":"=SUM(("}]}`,
58 `{"cells":[{"r":0,"c":0,"raw":"7","fromR":0}]}`,
59 `{"cells":[{"r":0,"c":0,"raw":"7","fromR":-1,"fromC":0}]}`,
60 `{"cells":[{"r":0,"c":0,"raw":"` + strings.Repeat("x", maxRawBytes+1) + `"}]}`,
61 `{"cells":[{"r":0,"c":0,"raw":"` + strings.Repeat("x", 1<<20) + `"}]}`,
62 } {
63 app, cookies := signIn(t)
64 s, path := newSheet(t, app, cookies, "Budget")
65 sub := app.hub.Subscribe(s.TabID)
66 rec := postCell(t, app, cookies, path+"/batch", body)
67 if rec.Code != http.StatusBadRequest && rec.Code != http.StatusRequestEntityTooLarge {
68 t.Fatalf("invalid status %d", rec.Code)
69 }
70 if strings.Contains(rec.Body.String(), "SUM") || strings.Contains(rec.Body.String(), "xxx") {
71 t.Fatal("content in error")
72 }
73 snap := decodeGrid(t, do(t, app, as(httptest.NewRequest(http.MethodGet, path, nil), cookies)))
74 if len(snap.Cells) != 0 {
75 t.Fatalf("invalid batch wrote %+v", snap.Cells)
76 }
77 select {
78 case <-sub.C():
79 t.Fatal("failed batch published")
80 default:
81 }
82 sub.Close()
83 }
84 app, cookies := signIn(t)
85 _, path := newSheet(t, app, cookies, "Budget")
86 cells := make([]cellWrite, store.MaxCellBatch+1)
87 for i := range cells {
88 cells[i] = cellWrite{R: i, Raw: "1"}
89 }
90 body, _ := json.Marshal(map[string]any{"cells": cells})
91 if rec := postCell(t, app, cookies, path+"/batch", string(body)); rec.Code != http.StatusBadRequest {
92 t.Fatalf("oversized batch = %d", rec.Code)
93 }
94}
95
96func TestCellBatchUsesCopyTranslationAndRejectsWrongTab(t *testing.T) {
97 app, cookies := signIn(t)
98 s, path := newSheet(t, app, cookies, "Budget")
99 rec := postCell(t, app, cookies, path+"/batch", `{"cells":[{"r":1,"c":0,"raw":"=A1+$B$1","fromR":0,"fromC":0}]}`)
100 if rec.Code != 200 {
101 t.Fatalf("copy %d", rec.Code)
102 }
103 if got := decodeGrid(t, rec).Cells[0].Raw; got != "=A2+$B$1" {
104 t.Fatalf("translated = %q", got)
105 }
106 other, _ := newSheet(t, app, cookies, "Other")
107 wrong := fmt.Sprintf("/api/sheets/%d/tabs/%d/cells/batch", s.ID, other.TabID)
108 if rec := postCell(t, app, cookies, wrong, `{"cells":[{"r":0,"c":0,"raw":"1"}]}`); rec.Code != 404 {
109 t.Fatalf("cross sheet = %d", rec.Code)
110 }
111 if rec := postCell(t, app, nil, path+"/batch", `{"cells":[{"r":0,"c":0,"raw":"1"}]}`); rec.Code == 200 {
112 t.Fatal("unsigned write accepted")
113 }
114}
115
116func TestCellBatchEnforcesViewerCommenterAndStrangerIsolation(t *testing.T) {
117 for _, role := range []SheetRole{SheetViewer, SheetCommenter} {
118 app, cookies := signIn(t)
119 s, path := newSheet(t, app, cookies, "Budget")
120 guestOn(t, app, s.ID, "ada@elsewhere.example", role)
121 other, _ := newSheet(t, app, cookies, "Other")
122 guestOn(t, app, other.ID, "stranger@elsewhere.example", SheetEditor)
123 for _, email := range []string{"ada@elsewhere.example", "stranger@elsewhere.example"} {
124 guest := signInAs(t, app, email)
125 if rec := postCell(t, app, guest, path+"/batch", `{"cells":[{"r":0,"c":0,"raw":"1"}]}`); rec.Code != 404 {
126 t.Fatalf("%s %s batch = %d", role, email, rec.Code)
127 }
128 }
129 }
130}
131