- lagoon.ValidateRequest supports Laravel 9 prohibited (!required, not
implicit); with no catalog line its message is validation.prohibited
- tide compares Content-Disposition with real calendar dates masked on both
sides; a different name, an invalid date or a one-sided date still diffs
- tide.NormalizePublications masks a Carbon +00:00 $.data.payload.created_at
and an uncaptured positive integer $.data.payload.id as {{id}}
- the album-date test's outside-album sibling moves off payload.created_at,
which now has its own mask
- READMEs and docs describe the rule and both masks
125 lines
5.8 KiB
Go
125 lines
5.8 KiB
Go
package tide
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestNormalizeContentDispositionDate: a download name built from the day
|
|
// of the request replays on a later day, while a different stem, an invalid
|
|
// date, a date on one side only or a missing header still diff.
|
|
func TestNormalizeContentDispositionDate(t *testing.T) {
|
|
const recorded = "attachment; filename=export-2026-09-17.csv"
|
|
cases := []struct {
|
|
name string
|
|
got map[string]string
|
|
diff bool
|
|
}{
|
|
{"same stem on a later day", map[string]string{"Content-Disposition": "attachment; filename=export-2026-10-03.csv"}, false},
|
|
{"same day", map[string]string{"Content-Disposition": recorded}, false},
|
|
{"header name case", map[string]string{"content-disposition": "attachment; filename=export-2027-01-01.csv"}, false},
|
|
{"different stem", map[string]string{"Content-Disposition": "attachment; filename=report-2026-10-03.csv"}, true},
|
|
{"invalid date", map[string]string{"Content-Disposition": "attachment; filename=export-2026-13-45.csv"}, true},
|
|
{"date on one side only", map[string]string{"Content-Disposition": "attachment; filename=export.csv"}, true},
|
|
{"inline instead of attachment", map[string]string{"Content-Disposition": "inline; filename=export-2026-10-03.csv"}, true},
|
|
{"header absent", map[string]string{}, true},
|
|
}
|
|
for _, c := range cases {
|
|
diffs := compareHeaders(map[string]string{"Content-Disposition": recorded}, c.got, nil)
|
|
if (len(diffs) > 0) != c.diff {
|
|
t.Errorf("%s: diffs = %+v, want diff=%v", c.name, diffs, c.diff)
|
|
}
|
|
}
|
|
// A recorded value that holds an invalid date keeps the byte comparison.
|
|
odd := "attachment; filename=export-2026-02-30.csv"
|
|
if diffs := compareHeaders(map[string]string{"Content-Disposition": odd}, map[string]string{"Content-Disposition": "attachment; filename=export-2026-02-28.csv"}, nil); len(diffs) != 1 {
|
|
t.Errorf("invalid recorded date: diffs = %+v, want one", diffs)
|
|
}
|
|
// Two dates must both be real and both present.
|
|
two := "attachment; filename=export-2026-09-01-2026-09-17.csv"
|
|
if diffs := compareHeaders(map[string]string{"Content-Disposition": two}, map[string]string{"Content-Disposition": "attachment; filename=export-2026-10-01-2026-10-03.csv"}, nil); len(diffs) != 0 {
|
|
t.Errorf("two dates: diffs = %+v, want none", diffs)
|
|
}
|
|
// The mask applies to Content-Disposition only.
|
|
if diffs := compareHeaders(map[string]string{"Location": "/files/2026-09-17"}, map[string]string{"Location": "/files/2026-10-03"}, nil); len(diffs) != 1 {
|
|
t.Errorf("Location: diffs = %+v, want one", diffs)
|
|
}
|
|
if _, n, ok := maskDispositionDates("x12026-09-170"); n != 0 || !ok {
|
|
t.Errorf("a date inside a longer digit run is not a date token")
|
|
}
|
|
}
|
|
|
|
// TestNormalizeNotificationPublication: a notification:new publication's id
|
|
// and created_at under $.data.payload normalize equal across two runs, while
|
|
// a Z date, a string id and every other path stay visible.
|
|
func TestNormalizeNotificationPublication(t *testing.T) {
|
|
store := mustMemoryStore()
|
|
pub := func(id, created string) []Publication {
|
|
return []Publication{{Method: "POST", Path: "/api/publish", Body: json.RawMessage(
|
|
`{"channel":"acme#5","data":{"event":"notification:new","payload":{"id":` + id +
|
|
`,"type":"item_added","payload":{"album_id":3,"created_at":"2026-01-01T00:00:00+00:00"},"read_at":null,"created_at":"` + created + `"}}}`)}}
|
|
}
|
|
a, err := NormalizePublications(pub("10000001", "2026-09-30T11:21:55+00:00"), store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := NormalizePublications(pub("42", "2026-10-03T08:00:00+00:00"), store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diffs := DiffPublications(a, b); len(diffs) != 0 {
|
|
t.Fatalf("diffs = %+v", diffs)
|
|
}
|
|
body := string(a[0].Body)
|
|
if !strings.Contains(body, `"payload":{"id":{{id}},`) || !strings.HasSuffix(body, `"read_at":null,"created_at":"{{datetime}}"}}}`) {
|
|
t.Fatalf("notification not masked: %s", body)
|
|
}
|
|
// Only $.data.payload.created_at is masked, not a nested payload date.
|
|
if !strings.Contains(body, `"album_id":3,"created_at":"2026-01-01T00:00:00+00:00"`) {
|
|
t.Fatalf("over-normalised: %s", body)
|
|
}
|
|
|
|
// A Z date, a zero or negative id and a string id stay visible.
|
|
for _, c := range []struct{ id, created, want string }{
|
|
{"42", "2026-10-03T08:00:00Z", `"created_at":"2026-10-03T08:00:00Z"`},
|
|
{`"42"`, "2026-10-03T08:00:00+00:00", `"payload":{"id":"42",`},
|
|
{"0", "2026-10-03T08:00:00+00:00", `"payload":{"id":0,`},
|
|
{"-3", "2026-10-03T08:00:00+00:00", `"payload":{"id":-3,`},
|
|
{"4.5", "2026-10-03T08:00:00+00:00", `"payload":{"id":4.5,`},
|
|
} {
|
|
got, err := NormalizePublications(pub(c.id, c.created), store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(got[0].Body), c.want) {
|
|
t.Errorf("id %s created %s: %s lacks %s", c.id, c.created, got[0].Body, c.want)
|
|
}
|
|
if diffs := DiffPublications(a, got); len(diffs) == 0 {
|
|
t.Errorf("id %s created %s must diff against the masked publication", c.id, c.created)
|
|
}
|
|
}
|
|
|
|
// A captured id keeps its own placeholder.
|
|
store.Set("id:note", "77")
|
|
got, err := NormalizePublications(pub("77", "2026-10-03T08:00:00+00:00"), store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(got[0].Body), `"payload":{"id":{{id:note}},`) {
|
|
t.Fatalf("captured id: %s", got[0].Body)
|
|
}
|
|
|
|
// An album publication's existing masks are unchanged.
|
|
album := []Publication{{Method: "POST", Path: "/api/publish", Body: json.RawMessage(
|
|
`{"channel":"c","data":{"payload":{"album":{"created_at":"2026-09-30T11:21:55+00:00"},"actor":{"user_id":1,"name":null},"timestamp":"2026-09-30T11:21:55+00:00"}}}`)}}
|
|
got, err = NormalizePublications(album, store)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := `{"channel":"c","data":{"payload":{"album":{"created_at":"{{datetime}}"},"actor":"{{actor}}","timestamp":"{{timestamp}}"}}}`
|
|
if string(got[0].Body) != want {
|
|
t.Fatalf("album publication\n got %s\nwant %s", got[0].Body, want)
|
|
}
|
|
}
|