Wire active alert filtering into HTTP
This commit is contained in:
@@ -4,6 +4,7 @@ package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedapi/endpoint"
|
||||
"gitea.maximumdirect.net/ejr/feedapi/render"
|
||||
@@ -11,12 +12,14 @@ import (
|
||||
"gitea.maximumdirect.net/ejr/weatherapi/internal/adapters/inbound/httpapi/presenter"
|
||||
)
|
||||
|
||||
var alertNow = time.Now
|
||||
|
||||
func alertsDefinition(svc Service) endpoint.Definition {
|
||||
return endpoint.GET(
|
||||
"/alerts/active",
|
||||
bindQuery,
|
||||
func(ctx context.Context, req queryRequest) (any, error) {
|
||||
run, err := svc.LatestAlertRun(ctx)
|
||||
run, err := svc.LatestActiveAlertRun(ctx, alertNow().UTC())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type fakeService struct {
|
||||
weatherStoryRun *model.WeatherStoryRun
|
||||
weatherStory *model.WeatherStory
|
||||
alerts *model.WeatherAlertRun
|
||||
activeAlertAt []time.Time
|
||||
outlookRun *model.WeatherOutlookRun
|
||||
outlookFilters []app.OutlookFilter
|
||||
conditions *app.CurrentConditions
|
||||
@@ -64,6 +65,11 @@ func (s *fakeService) LatestAlertRun(context.Context) (*model.WeatherAlertRun, e
|
||||
return s.alerts, s.err
|
||||
}
|
||||
|
||||
func (s *fakeService) LatestActiveAlertRun(_ context.Context, activeAt time.Time) (*model.WeatherAlertRun, error) {
|
||||
s.activeAlertAt = append(s.activeAlertAt, activeAt)
|
||||
return s.alerts, s.err
|
||||
}
|
||||
|
||||
func (s *fakeService) LatestConvectiveOutlook(_ context.Context, filter app.OutlookFilter) (*model.WeatherOutlookRun, error) {
|
||||
s.outlookFilters = append(s.outlookFilters, filter)
|
||||
return s.outlookRun, s.err
|
||||
@@ -315,6 +321,82 @@ func TestAlertsUSUnitsKeepSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertsRouteRegistered(t *testing.T) {
|
||||
def := definitionForPath(t, Definitions(&fakeService{}), "/alerts/active")
|
||||
if len(def.Methods) != 1 || def.Methods[0] != http.MethodGet {
|
||||
t.Fatalf("expected GET definition, got %+v", def.Methods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertsHandlerPassesCurrentUTCInstant(t *testing.T) {
|
||||
now := time.Date(2026, 6, 11, 7, 30, 0, 0, time.FixedZone("LOCAL", -5*60*60))
|
||||
setAlertNowForTest(t, now)
|
||||
svc := &fakeService{
|
||||
alerts: &model.WeatherAlertRun{AsOf: time.Now().UTC()},
|
||||
}
|
||||
h := newHandler(t, svc, "/alerts/active")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/alerts/active", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if len(svc.activeAlertAt) != 1 {
|
||||
t.Fatalf("expected one active-alert service call, got %d", len(svc.activeAlertAt))
|
||||
}
|
||||
want := now.UTC()
|
||||
if !svc.activeAlertAt[0].Equal(want) {
|
||||
t.Fatalf("expected activeAt %v, got %v", want, svc.activeAlertAt[0])
|
||||
}
|
||||
if svc.activeAlertAt[0].Location() != time.UTC {
|
||||
t.Fatalf("expected UTC location, got %v", svc.activeAlertAt[0].Location())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertsNoDataReturnsNullEnvelopeData(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{}, "/alerts/active")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/alerts/active", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data *json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode envelope: %v", err)
|
||||
}
|
||||
if payload.Data != nil {
|
||||
t.Fatalf("expected data null, got %s", string(*payload.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertsTextRendersEmptyActiveSet(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
alerts: &model.WeatherAlertRun{
|
||||
AsOf: time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC),
|
||||
Alerts: []model.WeatherAlert{},
|
||||
},
|
||||
}, "/alerts/active")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/alerts/active?format=text", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "Alerts: 0") {
|
||||
t.Fatalf("expected empty alert count in text body, got %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestObservationUSUnitsWithXMLFormat(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
observation: &model.WeatherObservation{
|
||||
@@ -1265,17 +1347,24 @@ func TestPrecisionValidationRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertsRejectPrecisionQueryParameter(t *testing.T) {
|
||||
func TestAlertsRejectUnsupportedQueryParameters(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
alerts: &model.WeatherAlertRun{AsOf: time.Now().UTC()},
|
||||
}, "/alerts/active")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/alerts/active?precision=1", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
for _, rawURL := range []string{
|
||||
"/alerts/active?precision=1",
|
||||
"/alerts/active?tz=CDT",
|
||||
"/alerts/active?TZ=CDT",
|
||||
"/alerts/active?bogus=1",
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, rawURL, nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", w.Code)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("%s: expected 400, got %d", rawURL, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2263,7 +2352,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}}",
|
||||
"weatherstories.txt.tmpl": "Weather Stories",
|
||||
"weatherstories_latest.txt.tmpl": "Latest Weather Story",
|
||||
"alerts_active.txt.tmpl": "Alerts text",
|
||||
"alerts_active.txt.tmpl": "{{if .Data}}Active Alerts\nAlerts: {{len .Data.Alerts}}{{else}}No active alerts data available.{{end}}",
|
||||
"conditions_current.txt.tmpl": "Conditions text",
|
||||
} {
|
||||
tmpl, err := template.New(name).Parse(body)
|
||||
@@ -2297,6 +2386,13 @@ func setOutlookNowForTest(t *testing.T, now time.Time) {
|
||||
t.Cleanup(func() { outlookNow = original })
|
||||
}
|
||||
|
||||
func setAlertNowForTest(t *testing.T, now time.Time) {
|
||||
t.Helper()
|
||||
original := alertNow
|
||||
alertNow = func() time.Time { return now }
|
||||
t.Cleanup(func() { alertNow = original })
|
||||
}
|
||||
|
||||
func testOutlookRun() *model.WeatherOutlookRun {
|
||||
issuedAt := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC)
|
||||
discussionUpdatedAt := issuedAt.Add(30 * time.Minute)
|
||||
|
||||
@@ -4,6 +4,7 @@ package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherapi/internal/app"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
@@ -18,6 +19,7 @@ type Service interface {
|
||||
LatestWeatherStoryRun(ctx context.Context) (*model.WeatherStoryRun, error)
|
||||
LatestWeatherStory(ctx context.Context) (*model.WeatherStory, error)
|
||||
LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error)
|
||||
LatestActiveAlertRun(ctx context.Context, activeAt time.Time) (*model.WeatherAlertRun, error)
|
||||
LatestConvectiveOutlook(ctx context.Context, filter app.OutlookFilter) (*model.WeatherOutlookRun, error)
|
||||
CurrentConditions(ctx context.Context) (*app.CurrentConditions, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user