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()