package nws import ( "context" "encoding/json" "testing" "time" "gitea.maximumdirect.net/ejr/feedkit/event" "gitea.maximumdirect.net/ejr/weatherfeeder/model" "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestAlertsNormalizerMapsEndsSeparatelyFromExpires(t *testing.T) { raw := []byte(`{ "updated":"2026-06-16T10:00:00+00:00", "title":"Current watches, warnings, and advisories for St. Louis", "features":[{ "id":"https://api.weather.gov/alerts/alert-1", "properties":{ "event":"Flood Warning", "headline":"Flood Warning issued", "sent":"2026-06-16T09:55:00+00:00", "effective":"2026-06-16T10:00:00+00:00", "onset":"2026-06-16T10:15:00+00:00", "ends":"2026-06-16T14:00:00+00:00", "expires":"2026-06-16T11:00:00+00:00" } }] }`) out, err := AlertsNormalizer{}.Normalize(context.Background(), alertRawEvent(raw)) if err != nil { t.Fatalf("Normalize() error = %v", err) } run := decodeAlertRun(t, out) if len(run.Alerts) != 1 { t.Fatalf("expected 1 alert, got %d", len(run.Alerts)) } alert := run.Alerts[0] wantEnds := time.Date(2026, 6, 16, 14, 0, 0, 0, time.UTC) wantExpires := time.Date(2026, 6, 16, 11, 0, 0, 0, time.UTC) if alert.Ends == nil || !alert.Ends.Equal(wantEnds) { t.Fatalf("ends = %v, want %s", alert.Ends, wantEnds) } if alert.Expires == nil || !alert.Expires.Equal(wantExpires) { t.Fatalf("expires = %v, want %s", alert.Expires, wantExpires) } } func TestAlertsNormalizerDoesNotFallbackExpiresToEnds(t *testing.T) { raw := []byte(`{ "updated":"2026-06-16T10:00:00+00:00", "features":[{ "id":"alert-ends-only", "properties":{ "event":"Heat Advisory", "ends":"2026-06-16T22:00:00+00:00" } }] }`) out, err := AlertsNormalizer{}.Normalize(context.Background(), alertRawEvent(raw)) if err != nil { t.Fatalf("Normalize() error = %v", err) } run := decodeAlertRun(t, out) alert := run.Alerts[0] if alert.Ends == nil { t.Fatal("expected ends to be populated") } if alert.Expires != nil { t.Fatalf("expected expires nil when upstream expires is absent, got %v", alert.Expires) } } func TestAlertsNormalizerIgnoresInvalidEnds(t *testing.T) { raw := []byte(`{ "updated":"2026-06-16T10:00:00+00:00", "features":[{ "id":"alert-invalid-ends", "properties":{ "event":"Special Weather Statement", "ends":"not-a-time", "expires":"2026-06-16T11:00:00+00:00" } }] }`) out, err := AlertsNormalizer{}.Normalize(context.Background(), alertRawEvent(raw)) if err != nil { t.Fatalf("Normalize() error = %v", err) } run := decodeAlertRun(t, out) alert := run.Alerts[0] if alert.Ends != nil { t.Fatalf("expected invalid ends to map nil, got %v", alert.Ends) } if alert.Expires == nil { t.Fatal("expected expires to remain populated") } } func alertRawEvent(raw []byte) event.Event { emittedAt := time.Date(2026, 6, 16, 10, 5, 0, 0, time.UTC) return event.Event{ ID: "raw-alerts", Kind: event.Kind(standards.KindAlert), Source: "NWSAlerts", Schema: standards.SchemaRawNWSAlertsV1, EmittedAt: emittedAt, Payload: json.RawMessage(raw), } } func decodeAlertRun(t *testing.T, e *event.Event) model.WeatherAlertRun { t.Helper() if e == nil { t.Fatal("expected normalized event") } if e.Schema != standards.SchemaWeatherAlertV1 { t.Fatalf("schema = %q, want %q", e.Schema, standards.SchemaWeatherAlertV1) } var run model.WeatherAlertRun raw, err := json.Marshal(e.Payload) if err != nil { t.Fatalf("marshal alert payload: %v", err) } if err := json.Unmarshal(raw, &run); err != nil { t.Fatalf("decode alert payload: %v", err) } return run }