package nws import ( "encoding/json" "strings" "testing" "time" "gitea.maximumdirect.net/ejr/feedkit/event" "gitea.maximumdirect.net/ejr/weatherfeeder/model" "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestWeatherStoriesNormalizerProducesCanonicalSchemaAndMapsSample(t *testing.T) { out, err := (WeatherStoriesNormalizer{}).Normalize(nil, weatherStoriesRawEvent(weatherStoriesSamplePayload())) if err != nil { t.Fatalf("Normalize() error = %v", err) } if out == nil { t.Fatalf("Normalize() returned nil output") } if out.Schema != standards.SchemaWeatherStoryV1 { t.Fatalf("Schema = %q, want %q", out.Schema, standards.SchemaWeatherStoryV1) } if out.Kind != event.Kind("weather_story") { t.Fatalf("Kind = %q, want weather_story", out.Kind) } payload, ok := out.Payload.(model.WeatherStoryRun) if !ok { t.Fatalf("Payload type = %T, want model.WeatherStoryRun", out.Payload) } if payload.OfficeID != "LSX" { t.Fatalf("OfficeID = %q, want LSX", payload.OfficeID) } wantAsOf := time.Date(2026, 5, 30, 9, 0, 34, 0, time.UTC) if !payload.AsOf.Equal(wantAsOf) { t.Fatalf("AsOf = %s, want %s", payload.AsOf, wantAsOf) } if out.EffectiveAt == nil || !out.EffectiveAt.Equal(wantAsOf) { t.Fatalf("EffectiveAt = %v, want %s", out.EffectiveAt, wantAsOf) } if len(payload.Stories) != 1 { t.Fatalf("Stories len = %d, want 1", len(payload.Stories)) } story := payload.Stories[0] if story.Title != "Several Chances for Rain Through Monday" { t.Fatalf("Title = %q", story.Title) } if story.Description != "A stagnant weather pattern." { t.Fatalf("Description = %q", story.Description) } if story.AltText != "This slide shows the forecast." { t.Fatalf("AltText = %q", story.AltText) } if story.Priority { t.Fatalf("Priority = true, want false") } if story.Order != 1 { t.Fatalf("Order = %d, want 1", story.Order) } if story.DownloadURL != "https://api.weather.gov/offices/LSX/weatherstories/download/3228e499-2aae-45a8-9ff9-1c060311026f" { t.Fatalf("DownloadURL = %q", story.DownloadURL) } } func TestWeatherStoriesNormalizerEmptyStoriesUsesFallbackAsOf(t *testing.T) { effectiveAt := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC) in := weatherStoriesRawEvent(`{"stories":[]}`) in.EffectiveAt = &effectiveAt out, err := (WeatherStoriesNormalizer{}).Normalize(nil, in) if err != nil { t.Fatalf("Normalize() error = %v", err) } payload, ok := out.Payload.(model.WeatherStoryRun) if !ok { t.Fatalf("Payload type = %T, want model.WeatherStoryRun", out.Payload) } if !payload.AsOf.Equal(effectiveAt) { t.Fatalf("AsOf = %s, want fallback %s", payload.AsOf, effectiveAt) } if payload.Stories == nil { t.Fatalf("Stories = nil, want empty slice") } if len(payload.Stories) != 0 { t.Fatalf("Stories len = %d, want 0", len(payload.Stories)) } } func TestWeatherStoriesNormalizerRejectsInvalidRequiredStoryTime(t *testing.T) { _, err := (WeatherStoriesNormalizer{}).Normalize(nil, weatherStoriesRawEvent(`{ "stories": [{ "officeId": "LSX", "startTime": "bad", "endTime": "2026-05-31T11:00:00+00:00", "updateTime": "2026-05-30T09:00:34+00:00" }] }`)) if err == nil { t.Fatalf("Normalize() error = nil, want error") } if !strings.Contains(err.Error(), "stories[0].startTime") { t.Fatalf("error = %q, want field context", err) } } func TestWeatherStoriesNormalizerMatch(t *testing.T) { n := WeatherStoriesNormalizer{} if !n.Match(event.Event{Schema: standards.SchemaRawNWSWeatherStoriesV1}) { t.Fatalf("Match(raw weatherstories) = false, want true") } if n.Match(event.Event{Schema: standards.SchemaRawNWSAlertsV1}) { t.Fatalf("Match(raw alerts) = true, want false") } } func weatherStoriesRawEvent(payload string) event.Event { return event.Event{ ID: "evt-weatherstories-1", Kind: event.Kind("weather_story"), Source: "nws-weatherstories-test", EmittedAt: time.Date(2026, 5, 30, 9, 5, 0, 0, time.UTC), Schema: standards.SchemaRawNWSWeatherStoriesV1, Payload: json.RawMessage(payload), } } func weatherStoriesSamplePayload() string { return `{ "stories": [ { "officeId": " LSX ", "startTime": "2026-05-30T08:46:00+00:00", "endTime": "2026-05-31T11:00:00+00:00", "updateTime": "2026-05-30T09:00:34+00:00", "title": " Several Chances for Rain Through Monday ", "description": " A stagnant weather pattern. ", "altText": " This slide shows the forecast. ", "priority": false, "order": 1, "download": " https://api.weather.gov/offices/LSX/weatherstories/download/3228e499-2aae-45a8-9ff9-1c060311026f " } ] }` }