Add a new field to the alert schema to fix a mismatch between the prior schema and the upstream NWS API
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -93,12 +93,8 @@ func buildAlerts(parsed nwsAlertsResponse, fallbackAsOf time.Time) (model.Weathe
|
||||
sent := nwscommon.ParseTimePtr(p.Sent)
|
||||
effective := nwscommon.ParseTimePtr(p.Effective)
|
||||
onset := nwscommon.ParseTimePtr(p.Onset)
|
||||
|
||||
// Expires: prefer "expires"; fall back to "ends" if present.
|
||||
ends := nwscommon.ParseTimePtr(p.Ends)
|
||||
expires := nwscommon.ParseTimePtr(p.Expires)
|
||||
if expires == nil {
|
||||
expires = nwscommon.ParseTimePtr(p.Ends)
|
||||
}
|
||||
|
||||
refs := parseNWSAlertReferences(p.References)
|
||||
|
||||
@@ -123,6 +119,7 @@ func buildAlerts(parsed nwsAlertsResponse, fallbackAsOf time.Time) (model.Weathe
|
||||
Sent: sent,
|
||||
Effective: effective,
|
||||
Onset: onset,
|
||||
Ends: ends,
|
||||
Expires: expires,
|
||||
|
||||
AreaDescription: strings.TrimSpace(p.AreaDesc),
|
||||
|
||||
136
internal/normalizers/nws/alerts_test.go
Normal file
136
internal/normalizers/nws/alerts_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user