Files
summercms/modules/tide/normalize_upload_test.go
Jakub Zych 5101ecb49c feat(14-03): tide keeps binary upstream bodies and compares every *_url upload by shape
- A response body that is not valid UTF-8 (a cover image) is written as a
  YAML !!binary scalar, never masked and replayed byte for byte
- The upload URL normalizer covers every key ending in _url (cover_url),
  not only url and thumb_url
- README and parity-testing docs updated
2026-10-03 21:34:52 +02:00

100 lines
5.3 KiB
Go

package tide
import (
"strings"
"testing"
)
func normalizeOne(t *testing.T, body string, step Step, prefix string) (string, []Diff) {
t.Helper()
out, diffs := normalizeJSON([]byte(body), step, maskOptions{uploadPrefix: prefix})
return strings.NewReplacer(`\u003c`, "<", `\u003e`, ">").Replace(string(out)), diffs
}
// TestNormalizeMaskEdges covers the response normalizer's masks: upload
// URLs under the default and a custom prefix, look-alikes under another
// prefix, keys holding other types, ids and dates of the wrong shape,
// disabled paths and pass-through of bodies that are not JSON.
func TestNormalizeMaskEdges(t *testing.T) {
orig := DefaultUploadPrefix + "/6ab/f82/1c3/6abf821c3d4e5f6a7b8c9d.png"
thumb := DefaultUploadPrefix + "/6ab/f82/1c3/thumb_57_200_200_0_0_crop.png"
cases := []struct {
name, body, prefix string
step Step
want string
diffs int
}{
{"original and thumb", `{"url":"` + orig + `","thumb_url":"` + thumb + `"}`, "", Step{},
`{"thumb_url":"` + DefaultUploadPrefix + `/<partition>/thumb_<id>_200_200_0_0_crop.png","url":"` + DefaultUploadPrefix + `/<partition>/<disk_name>.png"}`, 0},
{"custom prefix with a slash", `{"url":"/files/6ab/f82/1c3/6abf821c3d4e5f6a7b8c9d.jpg"}`, "/files/", Step{},
`{"url":"/files/<partition>/<disk_name>.jpg"}`, 0},
{"upload under another prefix", `{"url":"/elsewhere/6ab/f82/1c3/6abf821c3d4e5f6a7b8c9d.png"}`, "", Step{}, "", 1},
{"plain url kept", `{"url":"https://example.com/a.png"}`, "", Step{}, `{"url":"https://example.com/a.png"}`, 0},
{"any _url key", `{"cover_url":"` + orig + `","master_url":"https://api.example.test/masters/1"}`, "", Step{},
`{"cover_url":"` + DefaultUploadPrefix + `/<partition>/<disk_name>.png","master_url":"https://api.example.test/masters/1"}`, 0},
{"_url key under another prefix", `{"cover_url":"/elsewhere/6ab/f82/1c3/6abf821c3d4e5f6a7b8c9d.png"}`, "", Step{}, "", 1},
{"_urls list not masked", `{"cover_urls":["` + orig + `"]}`, "", Step{}, `{"cover_urls":["` + orig + `"]}`, 0},
{"non-string url kept", `{"url":5,"thumb_url":null}`, "", Step{}, `{"thumb_url":null,"url":5}`, 0},
{"slug never masked", `{"slug":"abc_id","id":7}`, "", Step{}, `{"id":"<id>","slug":"abc_id"}`, 0},
{"collection key", `{"collection_key":"e53f1bd22f01bbe8268079f016301ad5","client_id":null}`, "", Step{}, `{"client_id":null,"collection_key":"<id>"}`, 0},
{"collection key of the wrong type", `{"collection_key":5}`, "", Step{}, "", 1},
{"issued at", `{"token_issued_at":1700000000}`, "", Step{}, `{"token_issued_at":"<id>"}`, 0},
{"wrong date shape", `{"created_at":"2020-01-01 10:00:00"}`, "", Step{}, "", 1},
{"date of the wrong type", `{"updated_at":5}`, "", Step{}, "", 1},
{"null date and id", `{"deleted_at":null,"genre_id":null}`, "", Step{}, `{"deleted_at":null,"genre_id":null}`, 0},
{"decimal id", `{"id":1.5}`, "", Step{}, "", 1},
{"string id", `{"album_id":"7"}`, "", Step{}, "", 1},
{"id list", `{"collection_ids":[3,4]}`, "", Step{}, `{"collection_ids":["<id>","<id>"]}`, 0},
{"checkpoint", `{"checkpoint":"2020-01-01T10:00:00+00:00"}`, "", Step{}, `{"checkpoint":"<datetime>"}`, 0},
{"disabled by key", `{"created_at":"not a date"}`, "", Step{Normalize: []NormalizeRule{{Path: "created_at", Disable: true}}}, `{"created_at":"not a date"}`, 0},
{"disabled by path", `{"errors":{"album_id":["The album id field is required."]}}`, "", Step{Normalize: []NormalizeRule{{Path: "$.errors.album_id[0]", Disable: true}}}, `{"errors":{"album_id":["The album id field is required."]}}`, 0},
{"disabled path without $", `{"a":{"user_id":"x"}}`, "", Step{Normalize: []NormalizeRule{{Path: "a.user_id", Disable: true}, {Path: "a.user_id"}}}, `{"a":{"user_id":"x"}}`, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, diffs := normalizeOne(t, tc.body, tc.step, tc.prefix)
if len(diffs) != tc.diffs {
t.Fatalf("diffs %+v, want %d", diffs, tc.diffs)
}
if tc.want != "" && got != tc.want {
t.Fatalf("got %s\nwant %s", got, tc.want)
}
})
}
for _, raw := range []string{"", " ", "not json", "<html>"} {
if got, diffs := normalizeOne(t, raw, Step{}, ""); got != raw || diffs != nil {
t.Errorf("non-JSON %q became %q %v", raw, got, diffs)
}
}
if lastPathKey("$.a.b[3]") != "b" || lastPathKey("$.x") != "x" || !strings.HasPrefix(maskOptions{}.prefix(), "/storage") {
t.Error("path key helpers")
}
}
// TestCoverageReportHelpers covers the coverage table and batch helpers the
// parity CLI prints.
func TestCoverageReportHelpers(t *testing.T) {
c := Coverage{Rows: []CoverageRow{{ID: "GET_x", Status: "ported", Outcome: "pass"}}}
if h := CoverageHeaders(); strings.Join(h, ",") != "route,status,outcome" {
t.Fatalf("headers %v", h)
}
if rows := c.CoverageRows(); len(rows) != 1 || strings.Join(rows[0], ",") != "GET_x,ported,pass" {
t.Fatalf("rows %v", rows)
}
if c.ResumeLine() != "manifest recording complete" {
t.Fatal(c.ResumeLine())
}
c.ResumeRemaining = 3
if c.ResumeLine() != "resume remaining 3" {
t.Fatal(c.ResumeLine())
}
for in, want := range map[string]int{"": 0, " ": 0, "7": 7} {
if n, err := ParseNextBatch(in); err != nil || n != want {
t.Errorf("ParseNextBatch(%q) = %d %v", in, n, err)
}
}
if _, err := ParseNextBatch("x"); err == nil {
t.Error("a non-number batch")
}
}