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 "strconv"
7 "testing"
8)
9
10// The tests for spec §4.2's translation pass — "Copy / drag-fill →
11// Translation pass at paste time. Relative refs are re-resolved by
12// offset from the paste anchor; absolute ($A$1) refs resolve to the
13// same id."
14//
15// calc.Translate has existed, with tests, since the engine landed. What
16// did not exist was any caller. Measured in a browser on 2026-09-03:
17// filling `=A1*2` down B1:B3 wrote `=A1*2` into all three cells and the
18// column read 20, 20, 20 where it should read 20, 40, 60. Every Go test
19// passed, because every Go test wrote a cell rather than copying one.
20//
21// So these tests are written at the HTTP boundary, which is where the
22// offset now arrives, and not at calc — calc was never wrong.
23
24// pasteInto POSTs a copied cell to (r, c), naming the cell it came from.
25func pasteInto(t *testing.T, app *App, cookies []*http.Cookie, cells string, r, c, fromR, fromC int, raw string) gridResponse {
26 t.Helper()
27 body := `{"r":` + strconv.Itoa(r) + `,"c":` + strconv.Itoa(c) +
28 `,"fromR":` + strconv.Itoa(fromR) + `,"fromC":` + strconv.Itoa(fromC) +
29 `,"raw":` + strconv.Quote(raw) + `}`
30 rec := postCell(t, app, cookies, cells, body)
31 if rec.Code != http.StatusOK {
32 t.Fatalf("paste to %d,%d = %d: %s", r, c, rec.Code, rec.Body.String())
33 }
34 return decodeGrid(t, rec)
35}
36
37func TestCopyingAFormulaDownOffsetsItsRelativeReferences(t *testing.T) {
38 app, cookies := signIn(t)
39 _, cells := newSheet(t, app, cookies, "Fill")
40
41 for r, v := range []string{"10", "20", "30"} {
42 postCell(t, app, cookies, cells, `{"r":`+strconv.Itoa(r)+`,"c":0,"raw":"`+v+`"}`)
43 }
44 postCell(t, app, cookies, cells, `{"r":0,"c":1,"raw":"=A1*2"}`)
45
46 // B1 filled down into B2 and B3 — one row and two rows below its
47 // origin, so A1 becomes A2 and A3.
48 for _, tc := range []struct {
49 row int
50 wantRaw, wantDisplay string
51 }{
52 {1, "=A2*2", "40"},
53 {2, "=A3*2", "60"},
54 } {
55 got := pasteInto(t, app, cookies, cells, tc.row, 1, 0, 1, "=A1*2")
56 if len(got.Cells) == 0 {
57 t.Fatalf("row %d: the paste returned no cells", tc.row)
58 }
59 cell := got.Cells[0]
60 if cell.Raw != tc.wantRaw {
61 t.Errorf("filled into row %d: raw = %q, want %q — the formula bar "+
62 "must show the reference the cell actually uses",
63 tc.row, cell.Raw, tc.wantRaw)
64 }
65 if cell.Display != tc.wantDisplay {
66 t.Errorf("filled into row %d: display = %q, want %q",
67 tc.row, cell.Display, tc.wantDisplay)
68 }
69 }
70}
71
72func TestCopyingAFormulaAcrossOffsetsTheColumn(t *testing.T) {
73 app, cookies := signIn(t)
74 _, cells := newSheet(t, app, cookies, "Across")
75
76 postCell(t, app, cookies, cells, `{"r":0,"c":0,"raw":"7"}`)
77 postCell(t, app, cookies, cells, `{"r":0,"c":1,"raw":"9"}`)
78 postCell(t, app, cookies, cells, `{"r":1,"c":0,"raw":"=A1+1"}`)
79
80 got := pasteInto(t, app, cookies, cells, 1, 1, 1, 0, "=A1+1")
81 if got.Cells[0].Raw != "=B1+1" {
82 t.Errorf("copied one column right: raw = %q, want %q", got.Cells[0].Raw, "=B1+1")
83 }
84 if got.Cells[0].Display != "10" {
85 t.Errorf("display = %q, want %q", got.Cells[0].Display, "10")
86 }
87}
88
89func TestAnAbsoluteReferenceSurvivesTheCopy(t *testing.T) {
90 app, cookies := signIn(t)
91 _, cells := newSheet(t, app, cookies, "Absolute")
92
93 postCell(t, app, cookies, cells, `{"r":0,"c":0,"raw":"100"}`)
94 postCell(t, app, cookies, cells, `{"r":0,"c":1,"raw":"3"}`)
95 postCell(t, app, cookies, cells, `{"r":1,"c":1,"raw":"5"}`)
96
97 // =$A$1*B1 copied down: the absolute half must not move and the
98 // relative half must. That is the whole meaning of the dollar sign,
99 // and a Translate that offset both would still pass a test written
100 // with only relative references in it.
101 got := pasteInto(t, app, cookies, cells, 1, 2, 0, 2, "=$A$1*B1")
102 if got.Cells[0].Raw != "=$A$1*B2" {
103 t.Errorf("raw = %q, want %q — $A$1 must stay put and B1 must move",
104 got.Cells[0].Raw, "=$A$1*B2")
105 }
106 if got.Cells[0].Display != "500" {
107 t.Errorf("display = %q, want %q", got.Cells[0].Display, "500")
108 }
109}
110
111func TestPastingInPlaceChangesNothing(t *testing.T) {
112 app, cookies := signIn(t)
113 _, cells := newSheet(t, app, cookies, "InPlace")
114
115 postCell(t, app, cookies, cells, `{"r":0,"c":0,"raw":"4"}`)
116 got := pasteInto(t, app, cookies, cells, 1, 1, 1, 1, "=A1*2")
117 if got.Cells[0].Raw != "=A1*2" {
118 t.Errorf("a zero offset rewrote the formula: %q", got.Cells[0].Raw)
119 }
120}
121
122func TestALiteralIsCopiedVerbatim(t *testing.T) {
123 app, cookies := signIn(t)
124 _, cells := newSheet(t, app, cookies, "Literal")
125
126 // Text that looks like it might contain a reference. Only a cell
127 // whose raw input starts with "=" is a formula, and translating
128 // anything else would corrupt the one kind of cell that cannot
129 // possibly need it.
130 got := pasteInto(t, app, cookies, cells, 5, 5, 0, 0, "A1 and B2")
131 if got.Cells[0].Raw != "A1 and B2" {
132 t.Errorf("a text cell was translated: %q", got.Cells[0].Raw)
133 }
134}
135
136// TestCopyingAFormulaOffTheGridGivesREF pins the edge Excel names.
137//
138// =A1 copied one column left has nowhere to point. Rendering it anyway
139// is the dangerous outcome and was the one this code had before the
140// check: calc.ColLabel(-1) returns the empty string, so `=A1` rendered
141// as `=1` — a formula silently replaced by the number one, which looks
142// like a value somebody typed.
143func TestCopyingAFormulaOffTheGridGivesREF(t *testing.T) {
144 app, cookies := signIn(t)
145 _, cells := newSheet(t, app, cookies, "OffGrid")
146
147 for _, tc := range []struct {
148 name string
149 r, c, fromR, fromC int
150 raw string
151 }{
152 {"one column left of column A", 0, 0, 0, 1, "=A1"},
153 {"one row above row 1", 0, 0, 1, 0, "=A1"},
154 {"inside a call", 0, 0, 2, 0, "=SUM(A1:A3)"},
155 } {
156 got := pasteInto(t, app, cookies, cells, tc.r, tc.c, tc.fromR, tc.fromC, tc.raw)
157 if len(got.Cells) == 0 {
158 t.Fatalf("%s: the paste returned no cells", tc.name)
159 }
160 if got.Cells[0].Display != "#REF!" {
161 t.Errorf("%s: display = %q, want %q", tc.name, got.Cells[0].Display, "#REF!")
162 }
163 }
164}
165
166// TestAnUntranslatableFormulaIsStillWritten is the other half of the
167// case above, and the reason it is a separate test: the failure mode
168// worth guarding is not "the cell says #REF!" but "the write was
169// refused and the cell kept whatever was in it", which would leave a
170// paste half-applied.
171func TestAnUntranslatableFormulaIsStillWritten(t *testing.T) {
172 app, cookies := signIn(t)
173 _, cells := newSheet(t, app, cookies, "Refused")
174
175 postCell(t, app, cookies, cells, `{"r":0,"c":0,"raw":"keep me"}`)
176 pasteInto(t, app, cookies, cells, 0, 0, 1, 0, "=A1")
177
178 read := decodeGrid(t, do(t, app, as(httptest.NewRequest(http.MethodGet, cells, nil), cookies)))
179 for _, c := range read.Cells {
180 if c.R == 0 && c.C == 0 {
181 if c.Display == "keep me" {
182 t.Fatal("the paste was refused and the old value survived — a paste " +
183 "that silently skips a cell is worse than one that writes #REF!")
184 }
185 return
186 }
187 }
188 t.Fatal("A1 is absent after the paste")
189}
190
191// TestAnOutOfRangeOriginIsRefused: fromR/fromC are attacker-controlled
192// like every other field on this route, and a wild offset would ask
193// calc to translate by millions and render a reference nothing can
194// parse back.
195func TestAnOutOfRangeOriginIsRefused(t *testing.T) {
196 app, cookies := signIn(t)
197 _, cells := newSheet(t, app, cookies, "Hostile")
198
199 for _, body := range []string{
200 `{"r":0,"c":0,"fromR":-1,"fromC":0,"raw":"=A1"}`,
201 `{"r":0,"c":0,"fromR":0,"fromC":-5,"raw":"=A1"}`,
202 `{"r":0,"c":0,"fromR":99999999,"fromC":0,"raw":"=A1"}`,
203 } {
204 if rec := postCell(t, app, cookies, cells, body); rec.Code != http.StatusBadRequest {
205 t.Errorf("%s = %d, want 400", body, rec.Code)
206 }
207 }
208}
209