Filter active alerts in app service
This commit is contained in:
@@ -68,6 +68,26 @@ func (s *Service) LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, e
|
||||
return s.repo.LatestAlertRun(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) LatestActiveAlertRun(ctx context.Context, activeAt time.Time) (*model.WeatherAlertRun, error) {
|
||||
run, err := s.repo.LatestAlertRun(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if run == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
out := cloneAlertRun(run)
|
||||
alerts := out.Alerts[:0]
|
||||
for _, alert := range out.Alerts {
|
||||
if isActiveAlert(alert, activeAt) {
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
}
|
||||
out.Alerts = alerts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error) {
|
||||
run, err := s.repo.LatestConvectiveOutlookRun(ctx)
|
||||
if err != nil {
|
||||
@@ -113,6 +133,53 @@ func normalizeOutlookType(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func cloneAlertRun(run *model.WeatherAlertRun) *model.WeatherAlertRun {
|
||||
out := *run
|
||||
out.Latitude = copyFloat64(run.Latitude)
|
||||
out.Longitude = copyFloat64(run.Longitude)
|
||||
if run.Alerts != nil {
|
||||
out.Alerts = make([]model.WeatherAlert, len(run.Alerts))
|
||||
for i := range run.Alerts {
|
||||
out.Alerts[i] = cloneAlert(run.Alerts[i])
|
||||
}
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
func cloneAlert(alert model.WeatherAlert) model.WeatherAlert {
|
||||
out := alert
|
||||
out.Sent = copyTime(alert.Sent)
|
||||
out.Effective = copyTime(alert.Effective)
|
||||
out.Onset = copyTime(alert.Onset)
|
||||
out.Expires = copyTime(alert.Expires)
|
||||
if alert.References != nil {
|
||||
out.References = make([]model.AlertReference, len(alert.References))
|
||||
for i := range alert.References {
|
||||
out.References[i] = cloneAlertReference(alert.References[i])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneAlertReference(ref model.AlertReference) model.AlertReference {
|
||||
out := ref
|
||||
out.Sent = copyTime(ref.Sent)
|
||||
return out
|
||||
}
|
||||
|
||||
func isActiveAlert(alert model.WeatherAlert, activeAt time.Time) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(alert.MessageType), "Cancel") {
|
||||
return false
|
||||
}
|
||||
if alert.Effective != nil && activeAt.Before(*alert.Effective) {
|
||||
return false
|
||||
}
|
||||
if alert.Expires != nil && !activeAt.Before(*alert.Expires) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cloneOutlookRun(run *model.WeatherOutlookRun) *model.WeatherOutlookRun {
|
||||
out := *run
|
||||
out.Latitude = copyFloat64(run.Latitude)
|
||||
|
||||
@@ -25,6 +25,7 @@ type fakeRepository struct {
|
||||
err error
|
||||
|
||||
currentConditionsWindow int
|
||||
alertRunCalls int
|
||||
outlookRunCalls int
|
||||
}
|
||||
|
||||
@@ -53,6 +54,7 @@ func (r *fakeRepository) LatestWeatherStory(context.Context) (*model.WeatherStor
|
||||
}
|
||||
|
||||
func (r *fakeRepository) LatestAlertRun(context.Context) (*model.WeatherAlertRun, error) {
|
||||
r.alertRunCalls++
|
||||
return r.alerts, r.err
|
||||
}
|
||||
|
||||
@@ -118,6 +120,137 @@ func TestServiceDelegatesAlerts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceLatestActiveAlertRunDelegatesAndFilters(t *testing.T) {
|
||||
activeAt := testTime(12)
|
||||
repo := &fakeRepository{alerts: testAlertRun()}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestActiveAlertRun(context.Background(), activeAt)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if repo.alertRunCalls != 1 {
|
||||
t.Fatalf("expected one repository call, got %d", repo.alertRunCalls)
|
||||
}
|
||||
assertAlertIDs(t, run, []string{"current", "effective-at-boundary", "missing-effective", "missing-expires", "later-onset"})
|
||||
}
|
||||
|
||||
func TestServiceLatestActiveAlertRunNoData(t *testing.T) {
|
||||
repo := &fakeRepository{}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestActiveAlertRun(context.Background(), testTime(12))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if run != nil {
|
||||
t.Fatalf("expected nil alert run, got %+v", run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceLatestActiveAlertRunPropagatesErrors(t *testing.T) {
|
||||
want := errors.New("alert read failed")
|
||||
repo := &fakeRepository{err: want}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestActiveAlertRun(context.Background(), testTime(12))
|
||||
if !errors.Is(err, want) {
|
||||
t.Fatalf("expected error %v, got %v", want, err)
|
||||
}
|
||||
if run != nil {
|
||||
t.Fatalf("expected nil alert run on error, got %+v", run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceLatestActiveAlertRunKeepsMetadataWithEmptyAlerts(t *testing.T) {
|
||||
activeAt := testTime(12)
|
||||
repo := &fakeRepository{alerts: testAlertRunWithAlerts([]model.WeatherAlert{
|
||||
testAlert("expired", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12)),
|
||||
testAlert("cancel", "Cancel", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)),
|
||||
testAlert("future", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(14)),
|
||||
})}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestActiveAlertRun(context.Background(), activeAt)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if run == nil {
|
||||
t.Fatal("expected alert run")
|
||||
}
|
||||
if run.LocationID != "stl" || run.LocationName != "St. Louis" || !run.AsOf.Equal(testTime(10)) {
|
||||
t.Fatalf("unexpected run metadata: %+v", run)
|
||||
}
|
||||
if run.Latitude == nil || *run.Latitude != 38.62 {
|
||||
t.Fatalf("unexpected latitude: %v", run.Latitude)
|
||||
}
|
||||
if run.Longitude == nil || *run.Longitude != -90.2 {
|
||||
t.Fatalf("unexpected longitude: %v", run.Longitude)
|
||||
}
|
||||
if run.Alerts == nil {
|
||||
t.Fatal("expected empty alert slice, got nil")
|
||||
}
|
||||
if len(run.Alerts) != 0 {
|
||||
t.Fatalf("expected no alerts, got %+v", run.Alerts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceLatestActiveAlertRunDoesNotMutateRepositoryRun(t *testing.T) {
|
||||
original := testAlertRun()
|
||||
repo := &fakeRepository{alerts: original}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestActiveAlertRun(context.Background(), testTime(12))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(run.Alerts) == 0 {
|
||||
t.Fatal("expected active alerts")
|
||||
}
|
||||
|
||||
*run.Latitude = 99
|
||||
*run.Longitude = -99
|
||||
*run.Alerts[0].Sent = testTime(1)
|
||||
*run.Alerts[0].Effective = testTime(2)
|
||||
*run.Alerts[0].Onset = testTime(3)
|
||||
*run.Alerts[0].Expires = testTime(4)
|
||||
*run.Alerts[0].References[0].Sent = testTime(5)
|
||||
run.Alerts[0].ID = "changed"
|
||||
run.Alerts[0].References[0].ID = "changed"
|
||||
run.Alerts = run.Alerts[:1]
|
||||
|
||||
if *original.Latitude != 38.62 {
|
||||
t.Fatalf("expected original latitude unchanged, got %v", *original.Latitude)
|
||||
}
|
||||
if *original.Longitude != -90.2 {
|
||||
t.Fatalf("expected original longitude unchanged, got %v", *original.Longitude)
|
||||
}
|
||||
if original.Alerts[0].ID != "current" {
|
||||
t.Fatalf("expected original alert ID unchanged, got %q", original.Alerts[0].ID)
|
||||
}
|
||||
if original.Alerts[0].Sent == nil || !original.Alerts[0].Sent.Equal(testTime(9)) {
|
||||
t.Fatalf("expected original sent unchanged, got %v", original.Alerts[0].Sent)
|
||||
}
|
||||
if original.Alerts[0].Effective == nil || !original.Alerts[0].Effective.Equal(testTime(10)) {
|
||||
t.Fatalf("expected original effective unchanged, got %v", original.Alerts[0].Effective)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if original.Alerts[0].Expires == nil || !original.Alerts[0].Expires.Equal(testTime(13)) {
|
||||
t.Fatalf("expected original expires unchanged, got %v", original.Alerts[0].Expires)
|
||||
}
|
||||
if original.Alerts[0].References[0].ID != "ref-current" {
|
||||
t.Fatalf("expected original reference ID unchanged, got %q", original.Alerts[0].References[0].ID)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if len(original.Alerts) != 8 {
|
||||
t.Fatalf("expected original alert slice unchanged, got %d entries", len(original.Alerts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDelegatesLatestConvectiveOutlookRun(t *testing.T) {
|
||||
repo := &fakeRepository{outlookRun: testOutlookRun()}
|
||||
svc := NewService(repo)
|
||||
@@ -402,6 +535,67 @@ func TestServicePropagatesErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testAlertRun() *model.WeatherAlertRun {
|
||||
return testAlertRunWithAlerts([]model.WeatherAlert{
|
||||
testAlert("current", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)),
|
||||
testAlert("expired", "Update", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(12)),
|
||||
testAlert("future-effective", "Alert", testTimePtr(9), testTimePtr(13), testTimePtr(13), testTimePtr(15)),
|
||||
testAlert("canceled", " cancel ", testTimePtr(9), testTimePtr(10), testTimePtr(11), testTimePtr(13)),
|
||||
testAlert("effective-at-boundary", "Alert", testTimePtr(9), testTimePtr(12), testTimePtr(12), testTimePtr(14)),
|
||||
testAlert("missing-effective", "Alert", testTimePtr(9), nil, nil, testTimePtr(14)),
|
||||
testAlert("missing-expires", "Alert", testTimePtr(9), testTimePtr(10), nil, nil),
|
||||
testAlert("later-onset", "Alert", testTimePtr(9), testTimePtr(10), testTimePtr(13), testTimePtr(14)),
|
||||
})
|
||||
}
|
||||
|
||||
func testAlertRunWithAlerts(alerts []model.WeatherAlert) *model.WeatherAlertRun {
|
||||
latitude := 38.62
|
||||
longitude := -90.2
|
||||
return &model.WeatherAlertRun{
|
||||
LocationID: "stl",
|
||||
LocationName: "St. Louis",
|
||||
AsOf: testTime(10),
|
||||
Latitude: &latitude,
|
||||
Longitude: &longitude,
|
||||
Alerts: alerts,
|
||||
}
|
||||
}
|
||||
|
||||
func testAlert(id string, messageType string, sent *time.Time, effective *time.Time, onset *time.Time, expires *time.Time) model.WeatherAlert {
|
||||
refSent := testTime(8)
|
||||
return model.WeatherAlert{
|
||||
ID: id,
|
||||
Event: "Thunderstorm Warning",
|
||||
Headline: "Storm headline",
|
||||
Severity: "Severe",
|
||||
Urgency: "Immediate",
|
||||
Certainty: "Likely",
|
||||
Status: "Actual",
|
||||
MessageType: messageType,
|
||||
Category: "Met",
|
||||
Response: "Shelter",
|
||||
Description: "Storm description",
|
||||
Instruction: "Take shelter",
|
||||
Sent: sent,
|
||||
Effective: effective,
|
||||
Onset: onset,
|
||||
Expires: expires,
|
||||
AreaDescription: "St. Louis City",
|
||||
SenderName: "NWS St. Louis",
|
||||
References: []model.AlertReference{{
|
||||
ID: "ref-" + id,
|
||||
Identifier: "identifier-" + id,
|
||||
Sender: "sender-" + id,
|
||||
Sent: &refSent,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func testTimePtr(hour int) *time.Time {
|
||||
value := testTime(hour)
|
||||
return &value
|
||||
}
|
||||
|
||||
func testOutlookRun() *model.WeatherOutlookRun {
|
||||
latitude := 38.62
|
||||
longitude := -90.2
|
||||
@@ -493,3 +687,18 @@ func assertDiscussionDays(t *testing.T, run *model.WeatherOutlookRun, want []int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertAlertIDs(t *testing.T, run *model.WeatherAlertRun, want []string) {
|
||||
t.Helper()
|
||||
if run == nil {
|
||||
t.Fatal("expected alert run")
|
||||
}
|
||||
if len(run.Alerts) != len(want) {
|
||||
t.Fatalf("expected alert IDs %v, got %+v", want, run.Alerts)
|
||||
}
|
||||
for i := range want {
|
||||
if run.Alerts[i].ID != want[i] {
|
||||
t.Fatalf("expected alert IDs %v, got %+v", want, run.Alerts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user