1 Commits

Author SHA1 Message Date
2a33fe01cf Update to support upstream weatherfeeder v0.12.1 and add ends field to the alerts schema
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
2026-06-16 20:08:03 -05:00
15 changed files with 117 additions and 36 deletions

View File

@@ -212,14 +212,17 @@ Run `data` fields:
| `alerts` | array | active alerts, possibly empty | | `alerts` | array | active alerts, possibly empty |
Alerts are active when `messageType` is not `Cancel`, `effective` is absent or Alerts are active when `messageType` is not `Cancel`, `effective` is absent or
at or before request time, and `expires` is absent or after request time. at or before request time, and the alert end boundary is absent or after request
time. The end boundary prefers `ends`; if `ends` is absent, `expires` is used as
a fallback for older rows or providers that do not supply an alert-period end.
`onset` is presented when available but is not used as the active boundary. `onset` is presented when available but is not used as the active boundary.
Alert fields include `id`, `event`, `headline`, `severity`, `urgency`, Alert fields include `id`, `event`, `headline`, `severity`, `urgency`,
`certainty`, `status`, `messageType`, `category`, `response`, `description`, `certainty`, `status`, `messageType`, `category`, `response`, `description`,
`instruction`, `sent`, `effective`, `onset`, `expires`, `areaDescription`, `instruction`, `sent`, `effective`, `onset`, `ends`, `expires`,
`senderName`, and `references`. Most alert fields are optional except `id` when `areaDescription`, `senderName`, and `references`. Most alert fields are
an alert item is present. optional except `id` when an alert item is present. `ends` is the alert-period
end; `expires` is provider expiration metadata.
Reference fields are `id`, `identifier`, `sender`, and `sent`. Reference fields are `id`, `identifier`, `sender`, and `sent`.

View File

@@ -105,8 +105,8 @@ and `event_emitted_at`.
`alert_index`, `alert_id`, `event`, `headline`, `severity`, `urgency`, `alert_index`, `alert_id`, `event`, `headline`, `severity`, `urgency`,
`certainty`, `status`, `message_type`, `category`, `response`, `description`, `certainty`, `status`, `message_type`, `category`, `response`, `description`,
`instruction`, `sent`, `effective`, `onset`, `expires`, `area_description`, `instruction`, `sent`, `effective`, `onset`, `ends`, `expires`,
`sender_name`, and `run_event_id`. `area_description`, `sender_name`, and `run_event_id`.
### `alert_references` ### `alert_references`

View File

@@ -134,7 +134,9 @@ behavior deterministic.
`/alerts/active` uses the shared `format` and `units` binder. The handler calls `/alerts/active` uses the shared `format` and `units` binder. The handler calls
the application service with `alertNow().UTC()` so active alert filtering uses the application service with `alertNow().UTC()` so active alert filtering uses
the request-time instant while remaining deterministic in endpoint tests. the request-time instant while remaining deterministic in endpoint tests. The
application service prefers alert `ends` over `expires` when deciding whether an
alert has ended.
## Outlook Filters ## Outlook Filters

View File

@@ -79,8 +79,9 @@ successful responses with `data: null`.
- `CurrentConditions`: aggregates recent rows from `observations` using the - `CurrentConditions`: aggregates recent rows from `observations` using the
application-provided observation window. application-provided observation window.
- `LatestAlertRun`: latest row from `alert_runs`, then child `alerts` and - `LatestAlertRun`: latest row from `alert_runs`, then child `alerts` and
`alert_references`. This is the latest stored alert snapshot; active-time `alert_references`. This is the latest stored alert snapshot. The repository
filtering is performed by the application service. maps both `ends` and `expires`; active-time filtering is performed by the
application service.
- `LatestHourlyForecast`: latest `forecasts` row where `product = 'hourly'`, - `LatestHourlyForecast`: latest `forecasts` row where `product = 'hourly'`,
then child `forecast_periods`. then child `forecast_periods`.
- `LatestNarrativeForecast`: latest `forecasts` row where - `LatestNarrativeForecast`: latest `forecasts` row where

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25.5
require ( require (
gitea.maximumdirect.net/ejr/feedapi v0.1.0 gitea.maximumdirect.net/ejr/feedapi v0.1.0
gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0 gitea.maximumdirect.net/ejr/weatherfeeder v0.12.1
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
) )

4
go.sum
View File

@@ -1,7 +1,7 @@
gitea.maximumdirect.net/ejr/feedapi v0.1.0 h1:ZB5QWKD5DPFV3P7vyeJqXPMcSWN9qHkDUHw1LgN9hwY= gitea.maximumdirect.net/ejr/feedapi v0.1.0 h1:ZB5QWKD5DPFV3P7vyeJqXPMcSWN9qHkDUHw1LgN9hwY=
gitea.maximumdirect.net/ejr/feedapi v0.1.0/go.mod h1:3fIaFFx4ywt0TWbN8DIIBAHJn7ZQUm6PNcceqRgy3bw= gitea.maximumdirect.net/ejr/feedapi v0.1.0/go.mod h1:3fIaFFx4ywt0TWbN8DIIBAHJn7ZQUm6PNcceqRgy3bw=
gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0 h1:U3yln3o2rGqfMvWVRwOGgQeqYuqMm+/p0XIRhK8TDUQ= gitea.maximumdirect.net/ejr/weatherfeeder v0.12.1 h1:dZYDpOd0vEIk6QljsrhFyrOY0Lt4WyEoazhqDFIZKNQ=
gitea.maximumdirect.net/ejr/weatherfeeder v0.12.0/go.mod h1:VVtuwrbddWdUu21ovCSSojhH5J9P6kk0/dfnFqC4/Lw= gitea.maximumdirect.net/ejr/weatherfeeder v0.12.1/go.mod h1:VVtuwrbddWdUu21ovCSSojhH5J9P6kk0/dfnFqC4/Lw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -324,12 +324,14 @@ func TestObservationUSUnitsChangesFieldNames(t *testing.T) {
} }
func TestAlertsUSUnitsKeepSchema(t *testing.T) { func TestAlertsUSUnitsKeepSchema(t *testing.T) {
ends := time.Date(2026, 6, 11, 14, 0, 0, 0, time.UTC)
h := newHandler(t, &fakeService{ h := newHandler(t, &fakeService{
alerts: &model.WeatherAlertRun{ alerts: &model.WeatherAlertRun{
AsOf: time.Now().UTC(), AsOf: time.Now().UTC(),
Alerts: []model.WeatherAlert{{ Alerts: []model.WeatherAlert{{
ID: "abc", ID: "abc",
Headline: "A headline", Headline: "A headline",
Ends: &ends,
}}, }},
}, },
}, "/alerts/active") }, "/alerts/active")
@@ -359,6 +361,9 @@ func TestAlertsUSUnitsKeepSchema(t *testing.T) {
if first["id"] != "abc" { if first["id"] != "abc" {
t.Fatalf("expected alert id abc, got %#v", first["id"]) t.Fatalf("expected alert id abc, got %#v", first["id"])
} }
if _, ok := first["ends"].(string); !ok {
t.Fatalf("expected alert ends string, got %#v", first["ends"])
}
} }
func TestAlertsRouteRegistered(t *testing.T) { func TestAlertsRouteRegistered(t *testing.T) {
@@ -456,8 +461,10 @@ func TestAlertsTextOmitsInactiveAlertsAfterServiceFiltering(t *testing.T) {
activeAt := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) activeAt := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC)
setAlertNowForTest(t, activeAt) setAlertNowForTest(t, activeAt)
effective := activeAt.Add(-1 * time.Hour) effective := activeAt.Add(-1 * time.Hour)
activeExpires := activeAt.Add(1 * time.Hour) activeEnds := activeAt.Add(1 * time.Hour)
expiredAtBoundary := activeAt expiredEndsAtBoundary := activeAt
expiredProviderMetadata := activeAt.Add(-30 * time.Minute)
activeProviderMetadata := activeAt.Add(30 * time.Minute)
repo := &alertRepository{ repo := &alertRepository{
alerts: &model.WeatherAlertRun{ alerts: &model.WeatherAlertRun{
AsOf: activeAt, AsOf: activeAt,
@@ -467,21 +474,23 @@ func TestAlertsTextOmitsInactiveAlertsAfterServiceFiltering(t *testing.T) {
Headline: "Active warning", Headline: "Active warning",
MessageType: "Alert", MessageType: "Alert",
Effective: &effective, Effective: &effective,
Expires: &activeExpires, Ends: &activeEnds,
Expires: &expiredProviderMetadata,
}, },
{ {
ID: "expired-alert", ID: "expired-alert",
Headline: "Expired warning", Headline: "Expired warning",
MessageType: "Alert", MessageType: "Alert",
Effective: &effective, Effective: &effective,
Expires: &expiredAtBoundary, Ends: &expiredEndsAtBoundary,
Expires: &activeProviderMetadata,
}, },
{ {
ID: "canceled-alert", ID: "canceled-alert",
Headline: "Canceled warning", Headline: "Canceled warning",
MessageType: " cancel ", MessageType: " cancel ",
Effective: &effective, Effective: &effective,
Expires: &activeExpires, Ends: &activeEnds,
}, },
}, },
}, },
@@ -496,7 +505,7 @@ func TestAlertsTextOmitsInactiveAlertsAfterServiceFiltering(t *testing.T) {
t.Fatalf("expected 200, got %d", w.Code) t.Fatalf("expected 200, got %d", w.Code)
} }
body := w.Body.String() body := w.Body.String()
for _, want := range []string{"Alerts: 1", "active-alert", "Active warning"} { for _, want := range []string{"Alerts: 1", "active-alert", "Active warning", "Ends:"} {
if !strings.Contains(body, want) { if !strings.Contains(body, want) {
t.Fatalf("expected %q in text body, got %q", want, body) t.Fatalf("expected %q in text body, got %q", want, body)
} }
@@ -2448,7 +2457,7 @@ func testRenderers(t *testing.T) *render.Registry {
"outlooks_convective.txt.tmpl": "Convective Outlook\n{{if .Data}}Outlooks: {{len .Data.Outlooks}}\nDiscussions: {{len .Data.Discussions}}{{range .Data.Discussions}}\nDiscussion: {{.Discussion}}{{end}}{{else}}No convective outlook data available.{{end}}", "outlooks_convective.txt.tmpl": "Convective Outlook\n{{if .Data}}Outlooks: {{len .Data.Outlooks}}\nDiscussions: {{len .Data.Discussions}}{{range .Data.Discussions}}\nDiscussion: {{.Discussion}}{{end}}{{else}}No convective outlook data available.{{end}}",
"weatherstories.txt.tmpl": "Weather Stories", "weatherstories.txt.tmpl": "Weather Stories",
"weatherstories_latest.txt.tmpl": "Latest Weather Story", "weatherstories_latest.txt.tmpl": "Latest Weather Story",
"alerts_active.txt.tmpl": "{{if .Data}}Active Alerts\nAlerts: {{len .Data.Alerts}}{{range .Data.Alerts}}\n{{.ID}}{{if .Headline}}\nHeadline: {{.Headline}}{{end}}{{end}}{{else}}No active alerts data available.{{end}}", "alerts_active.txt.tmpl": "{{if .Data}}Active Alerts\nAlerts: {{len .Data.Alerts}}{{range .Data.Alerts}}\n{{.ID}}{{if .Headline}}\nHeadline: {{.Headline}}{{end}}{{if .Ends}}\nEnds: {{.Ends}}{{end}}{{end}}{{else}}No active alerts data available.{{end}}",
"conditions_current.txt.tmpl": "Conditions text", "conditions_current.txt.tmpl": "Conditions text",
} { } {
tmpl, err := template.New(name).Parse(body) tmpl, err := template.New(name).Parse(body)

View File

@@ -33,6 +33,7 @@ func mapAlertRow(row alertRow) indexedAlert {
Sent: timePtr(row.Sent), Sent: timePtr(row.Sent),
Effective: timePtr(row.Effective), Effective: timePtr(row.Effective),
Onset: timePtr(row.Onset), Onset: timePtr(row.Onset),
Ends: timePtr(row.Ends),
Expires: timePtr(row.Expires), Expires: timePtr(row.Expires),
AreaDescription: stringValue(row.AreaDescription), AreaDescription: stringValue(row.AreaDescription),
SenderName: stringValue(row.SenderName), SenderName: stringValue(row.SenderName),

View File

@@ -33,6 +33,7 @@ SELECT
sent, sent,
effective, effective,
onset, onset,
ends,
expires, expires,
area_description, area_description,
sender_name sender_name

View File

@@ -70,6 +70,7 @@ func (r *Repository) loadAlerts(ctx context.Context, eventID string) ([]model.We
&row.Sent, &row.Sent,
&row.Effective, &row.Effective,
&row.Onset, &row.Onset,
&row.Ends,
&row.Expires, &row.Expires,
&row.AreaDescription, &row.AreaDescription,
&row.SenderName, &row.SenderName,

View File

@@ -35,6 +35,7 @@ type alertRow struct {
Sent sql.NullTime Sent sql.NullTime
Effective sql.NullTime Effective sql.NullTime
Onset sql.NullTime Onset sql.NullTime
Ends sql.NullTime
Expires sql.NullTime Expires sql.NullTime
AreaDescription sql.NullString AreaDescription sql.NullString
SenderName sql.NullString SenderName sql.NullString

View File

@@ -246,6 +246,36 @@ func TestAttachAlertReferencesPreservesOrder(t *testing.T) {
} }
} }
func TestMapAlertRowMapsEndsAndExpires(t *testing.T) {
ends := time.Date(2026, 6, 16, 14, 0, 0, 0, time.FixedZone("CDT", -5*3600))
expires := time.Date(2026, 6, 16, 11, 0, 0, 0, time.FixedZone("CDT", -5*3600))
alert := mapAlertRow(alertRow{
AlertIndex: 1,
AlertID: "alert-1",
Ends: sql.NullTime{Time: ends, Valid: true},
Expires: sql.NullTime{Time: expires, Valid: true},
}).Alert
if alert.Ends == nil || alert.Ends.Location().String() != "UTC" || !alert.Ends.Equal(ends.UTC()) {
t.Fatalf("expected ends UTC %s, got %v", ends.UTC(), alert.Ends)
}
if alert.Expires == nil || alert.Expires.Location().String() != "UTC" || !alert.Expires.Equal(expires.UTC()) {
t.Fatalf("expected expires UTC %s, got %v", expires.UTC(), alert.Expires)
}
}
func TestMapAlertRowNullableEnds(t *testing.T) {
alert := mapAlertRow(alertRow{
AlertIndex: 1,
AlertID: "alert-1",
}).Alert
if alert.Ends != nil {
t.Fatalf("expected nil ends, got %v", alert.Ends)
}
}
func TestMapCurrentConditionsRowNoSamplesReturnsNil(t *testing.T) { func TestMapCurrentConditionsRowNoSamplesReturnsNil(t *testing.T) {
got := mapCurrentConditionsRow(currentConditionsRow{ got := mapCurrentConditionsRow(currentConditionsRow{
SampleCount: 0, SampleCount: 0,

View File

@@ -147,6 +147,7 @@ func cloneAlert(alert model.WeatherAlert) model.WeatherAlert {
out.Sent = copyTime(alert.Sent) out.Sent = copyTime(alert.Sent)
out.Effective = copyTime(alert.Effective) out.Effective = copyTime(alert.Effective)
out.Onset = copyTime(alert.Onset) out.Onset = copyTime(alert.Onset)
out.Ends = copyTime(alert.Ends)
out.Expires = copyTime(alert.Expires) out.Expires = copyTime(alert.Expires)
if alert.References != nil { if alert.References != nil {
out.References = make([]model.AlertReference, len(alert.References)) out.References = make([]model.AlertReference, len(alert.References))
@@ -170,7 +171,11 @@ func isActiveAlert(alert model.WeatherAlert, activeAt time.Time) bool {
if alert.Effective != nil && activeAt.Before(*alert.Effective) { if alert.Effective != nil && activeAt.Before(*alert.Effective) {
return false return false
} }
if alert.Expires != nil && !activeAt.Before(*alert.Expires) { endBoundary := alert.Ends
if endBoundary == nil {
endBoundary = alert.Expires
}
if endBoundary != nil && !activeAt.Before(*endBoundary) {
return false return false
} }
return true return true

View File

@@ -132,7 +132,7 @@ func TestServiceLatestActiveAlertRunDelegatesAndFilters(t *testing.T) {
if repo.alertRunCalls != 1 { if repo.alertRunCalls != 1 {
t.Fatalf("expected one repository call, got %d", repo.alertRunCalls) t.Fatalf("expected one repository call, got %d", repo.alertRunCalls)
} }
assertAlertIDs(t, run, []string{"current", "effective-at-boundary", "missing-effective", "missing-expires", "later-onset"}) assertAlertIDs(t, run, []string{"current", "effective-at-boundary", "missing-effective", "missing-expires", "later-onset", "ends-preferred"})
} }
func TestServiceLatestActiveAlertRunNoData(t *testing.T) { func TestServiceLatestActiveAlertRunNoData(t *testing.T) {
@@ -165,9 +165,9 @@ func TestServiceLatestActiveAlertRunPropagatesErrors(t *testing.T) {
func TestServiceLatestActiveAlertRunKeepsMetadataWithEmptyAlerts(t *testing.T) { func TestServiceLatestActiveAlertRunKeepsMetadataWithEmptyAlerts(t *testing.T) {
activeAt := testTime(12) activeAt := testTime(12)
repo := &fakeRepository{alerts: testAlertRunWithAlerts([]model.WeatherAlert{ repo := &fakeRepository{alerts: testAlertRunWithAlerts([]model.WeatherAlert{
testAlert("expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12)), testAlert("expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12), testTimePtr(13)),
testAlert("cancel", "Cancel", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)), testAlert("cancel", "Cancel", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(13)),
testAlert("future", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(14)), testAlert("future", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(14), testTimePtr(14)),
})} })}
svc := NewService(repo) svc := NewService(repo)
@@ -213,7 +213,8 @@ func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) {
*run.Alerts[0].Sent = testTime(1) *run.Alerts[0].Sent = testTime(1)
*run.Alerts[0].Effective = testTime(2) *run.Alerts[0].Effective = testTime(2)
*run.Alerts[0].Onset = testTime(3) *run.Alerts[0].Onset = testTime(3)
*run.Alerts[0].Expires = testTime(4) *run.Alerts[0].Ends = testTime(4)
*run.Alerts[0].Expires = testTime(5)
*run.Alerts[0].References[0].Sent = testTime(5) *run.Alerts[0].References[0].Sent = testTime(5)
run.Alerts[0].ID = "changed" run.Alerts[0].ID = "changed"
run.Alerts[0].References[0].ID = "changed" run.Alerts[0].References[0].ID = "changed"
@@ -237,7 +238,10 @@ func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) {
if original.Alerts[0].Onset == nil || !original.Alerts[0].Onset.Equal(testTime(11)) { if original.Alerts[0].Onset == nil || !original.Alerts[0].Onset.Equal(testTime(11)) {
t.Fatalf("expected original onset unchanged, got %v", original.Alerts[0].Onset) t.Fatalf("expected original onset unchanged, got %v", original.Alerts[0].Onset)
} }
if original.Alerts[0].Expires == nil || !original.Alerts[0].Expires.Equal(testTime(13)) { if original.Alerts[0].Ends == nil || !original.Alerts[0].Ends.Equal(testTime(13)) {
t.Fatalf("expected original ends unchanged, got %v", original.Alerts[0].Ends)
}
if original.Alerts[0].Expires == nil || !original.Alerts[0].Expires.Equal(testTime(12)) {
t.Fatalf("expected original expires unchanged, got %v", original.Alerts[0].Expires) t.Fatalf("expected original expires unchanged, got %v", original.Alerts[0].Expires)
} }
if original.Alerts[0].References[0].ID != "ref-current" { if original.Alerts[0].References[0].ID != "ref-current" {
@@ -246,11 +250,28 @@ func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) {
if original.Alerts[0].References[0].Sent == nil || !original.Alerts[0].References[0].Sent.Equal(testTime(8)) { if original.Alerts[0].References[0].Sent == nil || !original.Alerts[0].References[0].Sent.Equal(testTime(8)) {
t.Fatalf("expected original reference sent unchanged, got %v", original.Alerts[0].References[0].Sent) t.Fatalf("expected original reference sent unchanged, got %v", original.Alerts[0].References[0].Sent)
} }
if len(original.Alerts) != 8 { if len(original.Alerts) != 10 {
t.Fatalf("expected original alert slice unchanged, got %d entries", len(original.Alerts)) t.Fatalf("expected original alert slice unchanged, got %d entries", len(original.Alerts))
} }
} }
func TestServiceLatestActiveAlertRunUsesEndsBeforeExpires(t *testing.T) {
activeAt := testTime(12)
repo := &fakeRepository{alerts: testAlertRunWithAlerts([]model.WeatherAlert{
testAlert("ends-at-boundary", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12), testTimePtr(13)),
testAlert("ends-after-active-expires-before", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(11)),
testAlert("expires-fallback", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), nil, testTimePtr(13)),
testAlert("expires-fallback-expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), nil, testTimePtr(12)),
})}
svc := NewService(repo)
run, err := svc.LatestActiveAlertRun(context.Background(), activeAt)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertAlertIDs(t, run, []string{"ends-after-active-expires-before", "expires-fallback"})
}
func TestServiceDelegatesLatestConvectiveOutlookRun(t *testing.T) { func TestServiceDelegatesLatestConvectiveOutlookRun(t *testing.T) {
repo := &fakeRepository{outlookRun: testOutlookRun()} repo := &fakeRepository{outlookRun: testOutlookRun()}
svc := NewService(repo) svc := NewService(repo)
@@ -509,14 +530,16 @@ func TestServicePropagatesErrors(t *testing.T) {
func testAlertRun() *model.WeatherAlertRun { func testAlertRun() *model.WeatherAlertRun {
return testAlertRunWithAlerts([]model.WeatherAlert{ return testAlertRunWithAlerts([]model.WeatherAlert{
testAlert("current", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)), testAlert("current", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(12)),
testAlert("expired", "Update", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12)), testAlert("expired", "Update", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12), testTimePtr(13)),
testAlert("future-effective", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(15)), testAlert("future-effective", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(15), testTimePtr(15)),
testAlert("canceled", " cancel ", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)), testAlert("canceled", " cancel ", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13), testTimePtr(13)),
testAlert("effective-at-boundary", "Alert", testTimePtr(9), testTimePtr(12), testTimePtr(12), testTimePtr(14)), testAlert("effective-at-boundary", "Alert", testTimePtr(9), testTimePtr(12), testTimePtr(12), testTimePtr(14), testTimePtr(14)),
testAlert("missing-effective", "Alert", testTimePtr(9), nil, nil, testTimePtr(14)), testAlert("missing-effective", "Alert", testTimePtr(9), nil, nil, testTimePtr(14), testTimePtr(14)),
testAlert("missing-expires", "Alert", testTimePtr(9), testTimePtr(10), nil, nil), testAlert("missing-expires", "Alert", testTimePtr(9), testTimePtr(10), nil, nil, nil),
testAlert("later-onset", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(13), testTimePtr(14)), testAlert("later-onset", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(13), testTimePtr(14), testTimePtr(14)),
testAlert("ends-preferred", "Alert", testTimePtr(9), testTimePtr(10), nil, testTimePtr(14), testTimePtr(11)),
testAlert("ends-at-boundary", "Alert", testTimePtr(9), testTimePtr(10), nil, testTimePtr(12), testTimePtr(14)),
}) })
} }
@@ -533,7 +556,7 @@ func testAlertRunWithAlerts(alerts []model.WeatherAlert) *model.WeatherAlertRun
} }
} }
func testAlert(id string, messageType string, sent *time.Time, effective *time.Time, onset *time.Time, expires *time.Time) model.WeatherAlert { func testAlert(id string, messageType string, sent *time.Time, effective *time.Time, onset *time.Time, ends *time.Time, expires *time.Time) model.WeatherAlert {
refSent := testTime(8) refSent := testTime(8)
return model.WeatherAlert{ return model.WeatherAlert{
ID: id, ID: id,
@@ -551,6 +574,7 @@ func testAlert(id string, messageType string, sent *time.Time, effective *time.T
Sent: sent, Sent: sent,
Effective: effective, Effective: effective,
Onset: onset, Onset: onset,
Ends: ends,
Expires: expires, Expires: expires,
AreaDescription: "St. Louis City", AreaDescription: "St. Louis City",
SenderName: "NWS St. Louis", SenderName: "NWS St. Louis",

View File

@@ -11,6 +11,9 @@ Headline: {{$alert.Headline}}
{{- if $alert.Severity}} {{- if $alert.Severity}}
Severity: {{$alert.Severity}} Severity: {{$alert.Severity}}
{{- end}} {{- end}}
{{- if $alert.Ends}}
Ends: {{$alert.Ends}}
{{- end}}
{{- if $alert.Expires}} {{- if $alert.Expires}}
Expires: {{$alert.Expires}} Expires: {{$alert.Expires}}
{{- end}} {{- end}}