test: unit tests for the SEO classes
Pure tests of the classes package (no database): the resolver's value chains with every layer present and absent, robots per directive, the staging lock and drafts; the redirect graph (chains, flattened and back-and-forth graphs, loops by exact, pattern and regex probes, the 10-hop cap); URL normalisation across every address rule; the robots grammar over every directive combination; Google's robots.txt matcher examples; templates and unknown placeholders; CSV export and import; owner resolution; and every kind of the issue report. go.mod: gorm.io/driver/postgres becomes a direct requirement (the tests' dry-run database).
This commit is contained in:
118
classes/csv_test.go
Normal file
118
classes/csv_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package classes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCSV: the export writes a BOM, the delimiter and escaped formula
|
||||
// cells; the import strips the BOM, detects or takes the delimiter, reads
|
||||
// quoting, unescapes formula cells and refuses what is not CSV.
|
||||
func TestCSV(t *testing.T) {
|
||||
header := []string{"source", "target", "note"}
|
||||
rows := [][]string{
|
||||
{"/a", "/b", "plain"},
|
||||
{"/ł", "/ż", "zażółć; gęślą, jaźń"},
|
||||
{"/q", "/r", `she said "hi"` + "\nnext line"},
|
||||
{"/f", "/g", "=HYPERLINK(\"x\")"},
|
||||
{"/p", "/s", "+1"},
|
||||
{"/m", "/n", "-1"},
|
||||
{"/at", "/x", "@SUM(A1)"},
|
||||
{"/tab", "/x", "\tlead"},
|
||||
{"/cr", "/x", "\rlead"},
|
||||
{"/ok", "/x", "a=b"},
|
||||
}
|
||||
|
||||
t.Run("round trip with each delimiter", func(t *testing.T) {
|
||||
for _, delim := range []string{",", ";", ""} {
|
||||
out, err := WriteCSV(header, rows, delim)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(out, BOM) {
|
||||
t.Fatalf("%q: no BOM", delim)
|
||||
}
|
||||
sep := ","
|
||||
if delim == ";" {
|
||||
sep = ";"
|
||||
}
|
||||
if !strings.HasPrefix(strings.TrimPrefix(out, BOM), strings.Join(header, sep)+"\n") {
|
||||
t.Fatalf("%q: header line %q", delim, strings.SplitN(out, "\n", 2)[0])
|
||||
}
|
||||
gotHeader, got, err := ReadCSV(out, delim)
|
||||
if err != nil || !reflect.DeepEqual(gotHeader, header) || len(got) != len(rows) {
|
||||
t.Fatalf("%q: ReadCSV = %v, %d rows, %v", delim, gotHeader, len(got), err)
|
||||
}
|
||||
for i, row := range rows {
|
||||
// Escaped cells keep their leading tab or CR: the apostrophe
|
||||
// protects them from the import's trimming.
|
||||
for j, col := range header {
|
||||
if want := row[j]; got[i].Fields[col] != want {
|
||||
t.Fatalf("%q row %d %s = %q, want %q", delim, i, col, got[i].Fields[col], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("formula cells are escaped on export", func(t *testing.T) {
|
||||
out, _ := WriteCSV([]string{"note"}, [][]string{{"=1+1"}, {"+x"}, {"-x"}, {"@x"}, {"a=b"}, {""}}, ",")
|
||||
lines := strings.Split(strings.TrimPrefix(out, BOM), "\n")
|
||||
if lines[1] != "'=1+1" || lines[2] != "'+x" || lines[3] != "'-x" || lines[4] != "'@x" || lines[5] != "a=b" {
|
||||
t.Fatalf("export = %q", lines)
|
||||
}
|
||||
for in, want := range map[string]string{"'=x": "=x", "'+x": "+x", "'abc": "'abc", "''=x": "''=x", "plain": "plain", "'": "'"} {
|
||||
if got := unescapeCell(in); got != want {
|
||||
t.Errorf("unescapeCell(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delimiter detection and the setting", func(t *testing.T) {
|
||||
if CSVDelimiter(";") != ';' || CSVDelimiter(" ; ") != ';' || CSVDelimiter(",") != ',' || CSVDelimiter("") != ',' || CSVDelimiter("|") != ',' {
|
||||
t.Fatal("CSVDelimiter")
|
||||
}
|
||||
h, rows, err := ReadCSV("Source;Target\n/a;/b\n", "")
|
||||
if err != nil || !reflect.DeepEqual(h, []string{"source", "target"}) || rows[0].Fields["target"] != "/b" {
|
||||
t.Fatalf("detected ; = %v %+v %v", h, rows, err)
|
||||
}
|
||||
h, rows, err = ReadCSV(BOM+" SOURCE , Target \n/a,/b\n", "")
|
||||
if err != nil || !reflect.DeepEqual(h, []string{"source", "target"}) || rows[0].Line != 2 {
|
||||
t.Fatalf("detected , = %v %+v %v", h, rows, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("quoting, blank rows and extra cells", func(t *testing.T) {
|
||||
data := "source,target,note\n\"/a,1\",\"/b\"\"x\",\"two\nlines\"\n,,\n\n/c,/d,n,extra\n"
|
||||
_, rows, err := ReadCSV(data, ",")
|
||||
if err != nil || len(rows) != 2 {
|
||||
t.Fatalf("rows = %+v, %v", rows, err)
|
||||
}
|
||||
if rows[0].Fields["source"] != "/a,1" || rows[0].Fields["target"] != `/b"x` || rows[0].Fields["note"] != "two\nlines" || rows[0].Line != 2 {
|
||||
t.Fatalf("quoted row = %+v", rows[0])
|
||||
}
|
||||
if rows[1].Fields["source"] != "/c" || len(rows[1].Fields) != 3 || rows[1].Line != 6 {
|
||||
t.Fatalf("row with an extra cell = %+v", rows[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unreadable files", func(t *testing.T) {
|
||||
for name, data := range map[string]string{
|
||||
"empty": "",
|
||||
"a BOM only": BOM,
|
||||
"blank": " \n\t\n",
|
||||
"a bare quote": "source,target\n\"/a,/b\n",
|
||||
"the wrong delimiter": "source;target\n/a;/b\n",
|
||||
} {
|
||||
delim := ""
|
||||
if name == "the wrong delimiter" {
|
||||
delim = ","
|
||||
}
|
||||
if _, _, err := ReadCSV(data, delim); !errors.Is(err, ErrUnreadable) {
|
||||
t.Errorf("%s: err = %v", name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user