package main import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/kami/maven/internal/ipc" ) // eventsCore serves a canned intake journal. Embedding // ipc.UnimplementedCoreAPI means any other call fails loudly. type eventsCore struct { ipc.UnimplementedCoreAPI events []ipc.IntakeEvent err error gotN int } func (c *eventsCore) RecentEvents(_ context.Context, n int) ([]ipc.IntakeEvent, error) { c.gotN = n return c.events, c.err } func getEvents(t *testing.T, core ipc.CoreAPI) *httptest.ResponseRecorder { t.Helper() w := httptest.NewRecorder() handleEvents(w, httptest.NewRequest(http.MethodGet, "/events", nil), core) return w } func TestEventsPageRendersTheJournal(t *testing.T) { core := &eventsCore{events: []ipc.IntakeEvent{ {Source: "rss:tech", Kind: "note", Title: "Вышло ядро 6.19", Priority: "low", OccurredAt: time.Date(2026, 8, 1, 7, 15, 0, 0, time.UTC)}, {Source: "ambient:notif", Kind: "fact", Title: "calendar_event_20260801_планёрка", Body: "10:00-11:00 планёрка", Priority: "low", OccurredAt: time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC)}, }} w := getEvents(t, core) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } body := w.Body.String() occurred := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC).Local().Format("02.01 15:04:05") for _, want := range []string{"rss:tech", "Вышло ядро 6.19", "ambient:notif", "10:00-11:00 планёрка", occurred} { if !strings.Contains(body, want) { t.Errorf("page does not mention %q", want) } } if core.gotN != eventsPageLimit { t.Errorf("asked core for %d events, want %d", core.gotN, eventsPageLimit) } } func TestEventsPageSaysNothingArrived(t *testing.T) { w := getEvents(t, &eventsCore{}) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } if !strings.Contains(w.Body.String(), "nothing has arrived yet") { t.Error("empty journal did not render the empty-state line") } } func TestEventsPageReportsAReadFailure(t *testing.T) { // An unreachable journal must say so rather than render an empty table, // which would imply nothing arrived. w := getEvents(t, &eventsCore{err: errors.New("core is down")}) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200 with the error rendered", w.Code) } body := w.Body.String() if !strings.Contains(body, "journal unavailable") || !strings.Contains(body, "core is down") { t.Errorf("page did not report the read failure: %s", body) } if strings.Contains(body, "nothing has arrived yet") { t.Error("a failed read rendered as an empty journal") } } func TestEventsPageWithoutCore(t *testing.T) { w := getEvents(t, nil) if w.Code != http.StatusServiceUnavailable { t.Errorf("status = %d, want 503", w.Code) } } // awayFromLocal returns a zone three hours off whatever this machine runs in, // so a test can tell "rendered in his clock" apart from "rendered in whatever // zone the value arrived in" without depending on TZ. func awayFromLocal() *time.Location { _, off := time.Now().Zone() return time.FixedZone("away", off+3*60*60) } func TestEventsPageRendersBothTimesInLocalZone(t *testing.T) { // OccurredAt carries the source's zone — the store hands back UTC and // internal/rss parses a pubDate to UTC — while NoticedAt is the bus's local // instant. Rendered raw, the two columns of one row were in two zones and a // feed item read hours older than it was. away := awayFromLocal() occurred := time.Date(2026, 8, 1, 7, 15, 0, 0, time.UTC).In(away) noticed := occurred.Add(2 * time.Minute) core := &eventsCore{events: []ipc.IntakeEvent{{ Source: "rss:tech", Kind: "note", Title: "Вышло ядро 6.19", Priority: "low", OccurredAt: occurred, NoticedAt: noticed, }}} body := getEvents(t, core).Body.String() const layout = "02.01 15:04:05" for _, ts := range []time.Time{occurred, noticed} { if !strings.Contains(body, ts.Local().Format(layout)) { t.Errorf("page does not render %s in his clock (%s)", ts, ts.Local().Format(layout)) } if strings.Contains(body, ts.In(away).Format(layout)) { t.Errorf("page rendered %s in the source's zone", ts) } } } func TestEventsPageEscapesIntakeText(t *testing.T) { // Titles come from outside — a feed headline, a notification. They are shown // on a page and must never be able to inject markup into it. core := &eventsCore{events: []ipc.IntakeEvent{{ Source: "rss:x", Kind: "note", Priority: "low", Title: ``, OccurredAt: time.Date(2026, 8, 1, 7, 0, 0, 0, time.UTC), }}} body := getEvents(t, core).Body.String() if strings.Contains(body, "") { t.Error("intake title was not escaped") } if !strings.Contains(body, "<script>") { t.Error("intake title is missing from the page entirely") } }