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) } }