From beeaeeaecbd5099731072e076ac73dc36db8b20e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 12 Jun 2026 13:02:12 +0000 Subject: [PATCH] Cover outlook discussions in endpoint responses --- .../inbound/httpapi/endpoints_test.go | 96 +++++++++++++++++-- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/internal/adapters/inbound/httpapi/endpoints_test.go b/internal/adapters/inbound/httpapi/endpoints_test.go index 55608bb..251e1e3 100644 --- a/internal/adapters/inbound/httpapi/endpoints_test.go +++ b/internal/adapters/inbound/httpapi/endpoints_test.go @@ -1314,10 +1314,9 @@ func TestOutlookRoutesJSONSuccess(t *testing.T) { var payload struct { Data struct { - LocationID string `json:"locationId"` - Outlooks []struct { - ID string `json:"id"` - } `json:"outlooks"` + LocationID string `json:"locationId"` + Outlooks []map[string]any `json:"outlooks"` + Discussions []outlookDiscussionCheck `json:"discussions"` } `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil { @@ -1326,9 +1325,20 @@ func TestOutlookRoutesJSONSuccess(t *testing.T) { if payload.Data.LocationID != "stl" { t.Fatalf("expected locationId stl, got %q", payload.Data.LocationID) } - if len(payload.Data.Outlooks) != 1 || payload.Data.Outlooks[0].ID != "cat-1" { + if len(payload.Data.Outlooks) != 1 || payload.Data.Outlooks[0]["id"] != "cat-1" { t.Fatalf("unexpected outlooks payload: %+v", payload.Data.Outlooks) } + for _, field := range []string{"headline", "summary", "discussion"} { + if _, ok := payload.Data.Outlooks[0][field]; ok { + t.Fatalf("expected outlook polygon to omit %s, got %+v", field, payload.Data.Outlooks[0]) + } + } + if len(payload.Data.Discussions) != 1 { + t.Fatalf("expected one discussion, got %+v", payload.Data.Discussions) + } + if payload.Data.Discussions[0].Day != 1 || payload.Data.Discussions[0].Headline != "Day 1 headline" { + t.Fatalf("unexpected discussions payload: %+v", payload.Data.Discussions) + } }) } } @@ -1369,7 +1379,8 @@ func TestOutlookTextResponseUsesTemplate(t *testing.T) { t.Fatalf("expected text/plain content type, got %q", w.Header().Get("Content-Type")) } body := w.Body.String() - if !strings.Contains(body, "Convective Outlook") || !strings.Contains(body, "Outlooks: 1") { + if !strings.Contains(body, "Convective Outlook") || !strings.Contains(body, "Outlooks: 1") || + !strings.Contains(body, "Discussions: 1") || !strings.Contains(body, "Day 1 discussion") { t.Fatalf("expected outlook text template body, got %q", body) } } @@ -1390,11 +1401,15 @@ func TestOutlookXMLResponseRenders(t *testing.T) { if !strings.Contains(w.Body.String(), "stl") { t.Fatalf("expected outlook XML payload, got %q", w.Body.String()) } + if !strings.Contains(w.Body.String(), "") || !strings.Contains(w.Body.String(), "Day 1 headline") { + t.Fatalf("expected outlook XML discussions, got %q", w.Body.String()) + } } func TestOutlookFilteredNoMatchReturnsEmptyOutlooks(t *testing.T) { run := testOutlookRun() run.Outlooks = []model.WeatherOutlook{} + run.Discussions = []model.WeatherOutlookDiscussion{} h := newHandler(t, &fakeService{outlookRun: run}, "/outlooks/convective") w := httptest.NewRecorder() @@ -1407,8 +1422,9 @@ func TestOutlookFilteredNoMatchReturnsEmptyOutlooks(t *testing.T) { var payload struct { Data struct { - LocationID string `json:"locationId"` - Outlooks []model.WeatherOutlook `json:"outlooks"` + LocationID string `json:"locationId"` + Outlooks []model.WeatherOutlook `json:"outlooks"` + Discussions []model.WeatherOutlookDiscussion `json:"discussions"` } `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil { @@ -1420,6 +1436,39 @@ func TestOutlookFilteredNoMatchReturnsEmptyOutlooks(t *testing.T) { if payload.Data.Outlooks == nil || len(payload.Data.Outlooks) != 0 { t.Fatalf("expected empty outlooks slice, got %+v", payload.Data.Outlooks) } + if payload.Data.Discussions == nil || len(payload.Data.Discussions) != 0 { + t.Fatalf("expected empty discussions slice, got %+v", payload.Data.Discussions) + } +} + +func TestOutlookTimezoneQuery(t *testing.T) { + h := newHandler(t, &fakeService{outlookRun: testOutlookRun()}, "/outlooks/convective") + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/outlooks/convective?tz=CDT", nil) + h.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + + var payload outlookTimePayload + if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil { + t.Fatalf("decode outlook payload: %v", err) + } + assertOffsetSeconds(t, payload.Data.AsOf, -5*60*60) + assertOffsetSeconds(t, *payload.Data.IssuedAt, -5*60*60) + if len(payload.Data.Outlooks) != 1 { + t.Fatalf("expected one outlook, got %+v", payload.Data.Outlooks) + } + assertOffsetSeconds(t, payload.Data.Outlooks[0].ValidFrom, -5*60*60) + assertOffsetSeconds(t, payload.Data.Outlooks[0].ValidTo, -5*60*60) + assertOffsetSeconds(t, payload.Data.Outlooks[0].IssuedAt, -5*60*60) + assertOffsetSeconds(t, payload.Data.Outlooks[0].ExpiresAt, -5*60*60) + if len(payload.Data.Discussions) != 1 || payload.Data.Discussions[0].UpdatedAt == nil { + t.Fatalf("expected one discussion with updatedAt, got %+v", payload.Data.Discussions) + } + assertOffsetSeconds(t, *payload.Data.Discussions[0].UpdatedAt, -5*60*60) } func TestOutlookQueryParamsConstructFilter(t *testing.T) { @@ -2211,7 +2260,7 @@ func testRenderers(t *testing.T) *render.Registry { "discussion_long_term.txt.tmpl": "Forecast Discussion Long Term", "forecast_hourly.txt.tmpl": "Forecast text", "forecast_narrative.txt.tmpl": "Narrative Forecast", - "outlooks_convective.txt.tmpl": "Convective Outlook\n{{if .Data}}Outlooks: {{len .Data.Outlooks}}{{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_latest.txt.tmpl": "Latest Weather Story", "alerts_active.txt.tmpl": "Alerts text", @@ -2250,6 +2299,7 @@ func setOutlookNowForTest(t *testing.T, now time.Time) { func testOutlookRun() *model.WeatherOutlookRun { issuedAt := time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC) + discussionUpdatedAt := issuedAt.Add(30 * time.Minute) return &model.WeatherOutlookRun{ LocationID: "stl", LocationName: "St. Louis", @@ -2270,6 +2320,13 @@ func testOutlookRun() *model.WeatherOutlookRun { ContainsLocation: true, Geometry: []byte(`{"type":"Point","coordinates":[-90.2,38.6]}`), }}, + Discussions: []model.WeatherOutlookDiscussion{{ + Day: 1, + Headline: "Day 1 headline", + Summary: "Day 1 summary", + Discussion: "Day 1 discussion", + UpdatedAt: &discussionUpdatedAt, + }}, } } @@ -2343,6 +2400,27 @@ type discussionFocusedTimePayload struct { } `json:"data"` } +type outlookDiscussionCheck struct { + Day int `json:"day"` + Headline string `json:"headline"` +} + +type outlookTimePayload struct { + Data struct { + AsOf time.Time `json:"asOf"` + IssuedAt *time.Time `json:"issuedAt"` + Outlooks []struct { + ValidFrom time.Time `json:"validFrom"` + ValidTo time.Time `json:"validTo"` + IssuedAt time.Time `json:"issuedAt"` + ExpiresAt time.Time `json:"expiresAt"` + } `json:"outlooks"` + Discussions []struct { + UpdatedAt *time.Time `json:"updatedAt"` + } `json:"discussions"` + } `json:"data"` +} + func decodeForecastTimePayload(t *testing.T, w *httptest.ResponseRecorder) forecastTimePayload { t.Helper()