| 1 | package xlsx_test |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "amadan.net/oficina/sheets/internal/sheets/store" |
| 9 | "amadan.net/oficina/sheets/internal/sheets/xlsx" |
| 10 | ) |
| 11 | |
| 12 | // import -> export -> import must be stable. A file we wrote must open in |
| 13 | // Excel and LibreOffice; that half is checked by hand against the |
| 14 | // fixtures and recorded, because CI cannot open Excel — see the task |
| 15 | // report for what that check did and did not confirm in this |
| 16 | // environment. |
| 17 | func TestRoundTripIsStable(t *testing.T) { |
| 18 | for _, name := range fixtureNames(t) { |
| 19 | t.Run(name, func(t *testing.T) { |
| 20 | first := importFixture(t, name) |
| 21 | var buf bytes.Buffer |
| 22 | if err := xlsx.Export(first, &buf); err != nil { |
| 23 | t.Fatalf("export: %v", err) |
| 24 | } |
| 25 | second := importReader(t, bytes.NewReader(buf.Bytes())) |
| 26 | if diff := diffTabs(t, first, second); diff != "" { |
| 27 | t.Errorf("round trip changed the sheet:\n%s", diff) |
| 28 | } |
| 29 | }) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // Fills imported from XLSX keep their original hex so a file we did not |
| 34 | // author re-exports byte-identically in colour terms. The intent drives |
| 35 | // display; the retained hex drives export until a user re-picks — see |
| 36 | // models.go's Format.FillHex and colors.go's package doc. |
| 37 | func TestImportedFillHexIsRetained(t *testing.T) { |
| 38 | h := importFixture(t, "fills.xlsx") |
| 39 | f := formatOf(t, h, 0, 0) // A1, gencorpus_test.go's buildFillsFixture |
| 40 | if f.FillHex != "FF00B050" { |
| 41 | t.Fatalf("FillHex = %q, want FF00B050", f.FillHex) |
| 42 | } |
| 43 | // Re-exporting must emit that exact hex back out, not the resolved |
| 44 | // OKLCH intent's approximation of it. |
| 45 | var buf bytes.Buffer |
| 46 | if err := xlsx.Export(h, &buf); err != nil { |
| 47 | t.Fatalf("export: %v", err) |
| 48 | } |
| 49 | reimported := importReader(t, bytes.NewReader(buf.Bytes())) |
| 50 | f2 := formatOf(t, reimported, 0, 0) |
| 51 | if f2.FillHex != "FF00B050" { |
| 52 | t.Fatalf("FillHex after re-export = %q, want FF00B050 (not byte-identical re-export)", f2.FillHex) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // formatOf resolves (rowPos, colPos) to the real ids h.GridRows/GridCols |
| 57 | // hold for them and fetches the Format there, failing the test if either |
| 58 | // the coordinate or the format is missing. |
| 59 | func formatOf(t *testing.T, h *store.Handle, rowPos, colPos int) *store.Format { |
| 60 | t.Helper() |
| 61 | rowID, colID, ok := cellAt(h, rowPos, colPos) |
| 62 | if !ok { |
| 63 | t.Fatalf("no cell at position (%d,%d)", rowPos, colPos) |
| 64 | } |
| 65 | f := h.Format(rowID, colID) |
| 66 | if f == nil { |
| 67 | t.Fatalf("Format(%d,%d) at position (%d,%d) is nil", rowID, colID, rowPos, colPos) |
| 68 | } |
| 69 | return f |
| 70 | } |
| 71 | |
| 72 | func cellAt(h *store.Handle, rowPos, colPos int) (rowID, colID uint64, ok bool) { |
| 73 | foundRow, foundCol := false, false |
| 74 | for _, r := range h.GridRows() { |
| 75 | if r.Pos == rowPos { |
| 76 | rowID, foundRow = r.ID, true |
| 77 | } |
| 78 | } |
| 79 | for _, c := range h.GridCols() { |
| 80 | if c.Pos == colPos { |
| 81 | colID, foundCol = c.ID, true |
| 82 | } |
| 83 | } |
| 84 | return rowID, colID, foundRow && foundCol |
| 85 | } |
| 86 | |
| 87 | // Import runs as a background job (spec §11); its error text reaches the |
| 88 | // owner and platform logs this app cannot grep (spec §2.4). A formula |
| 89 | // that fails to parse must never leak into that error string — this pins |
| 90 | // it against a formula built specifically to fail, carrying a value no |
| 91 | // error message should ever contain. |
| 92 | func TestImportErrorsContainNoCellContent(t *testing.T) { |
| 93 | const canary = "SECRET-PAYROLL-FIGURE-93f7" |
| 94 | |
| 95 | buf := badFormulaFixture(t, canary) |
| 96 | |
| 97 | db := openTestDB(t) |
| 98 | sealer := testSealer(t) |
| 99 | h := newTab(t, db, sealer) |
| 100 | |
| 101 | err := xlsx.Import(buf, h) |
| 102 | if err == nil { |
| 103 | t.Fatal("want an error importing a deliberately unparseable formula, got nil") |
| 104 | } |
| 105 | if strings.Contains(err.Error(), canary) { |
| 106 | t.Fatalf("import error contains cell content: %q", err.Error()) |
| 107 | } |
| 108 | } |
| 109 | |