Implement support for NWS weather stories

This commit is contained in:
2026-05-30 07:47:49 -05:00
parent 9ff90d33fc
commit 63749a9572
14 changed files with 214 additions and 37 deletions

View File

@@ -49,11 +49,17 @@ func TestFetchBundleFromFixtures(t *testing.T) {
if bundle.Discussion.LongTerm == nil || bundle.Discussion.LongTerm.Text != "Warmer temperatures and periodic rain chances continue into the weekend." {
t.Fatalf("Discussion.LongTerm = %#v, want long-term AFD text", bundle.Discussion.LongTerm)
}
if bundle.WeatherStory == nil || bundle.WeatherStory.Title != "Several Chances for Rain Through Monday" {
t.Fatalf("WeatherStory = %#v, want latest weather story", bundle.WeatherStory)
}
if bundle.WeatherStory.UpdatedAt == nil {
t.Fatalf("WeatherStory.UpdatedAt = nil, want update timestamp")
}
if len(bundle.Sources) != 8 {
t.Fatalf("Sources length = %d, want 8", len(bundle.Sources))
}
if len(bundle.Warnings) != 2 {
t.Fatalf("Warnings length = %d, want daily and weather story warnings", len(bundle.Warnings))
if len(bundle.Warnings) != 1 {
t.Fatalf("Warnings length = %d, want daily warning", len(bundle.Warnings))
}
if !containsPath(requested, "/forecast/hourly") || containsPath(requested, "/forecast/hourly/today") {
t.Fatalf("requested paths = %v, want full hourly endpoint only", requested)
@@ -61,6 +67,9 @@ func TestFetchBundleFromFixtures(t *testing.T) {
if !containsPath(requested, "/forecast/narrative") || containsPath(requested, "/forecast/narrative/today") {
t.Fatalf("requested paths = %v, want full narrative endpoint only", requested)
}
if !containsPath(requested, "/weatherstories/latest") {
t.Fatalf("requested paths = %v, want weather story endpoint", requested)
}
}
func TestFetchBundleBuildsExpectedQueries(t *testing.T) {
@@ -74,8 +83,17 @@ func TestFetchBundleBuildsExpectedQueries(t *testing.T) {
}
for _, rawURL := range requested {
if !strings.Contains(rawURL, "format=json") || !strings.Contains(rawURL, "units=us") {
t.Fatalf("request %q missing format=json or units=us", rawURL)
if !strings.Contains(rawURL, "format=json") {
t.Fatalf("request %q missing format=json", rawURL)
}
if strings.HasPrefix(rawURL, "/weatherstories/") {
if strings.Contains(rawURL, "units=") || strings.Contains(rawURL, "precision=") || strings.Contains(rawURL, "tz=") {
t.Fatalf("weather story request %q should use format only", rawURL)
}
continue
}
if !strings.Contains(rawURL, "units=us") {
t.Fatalf("request %q missing units=us", rawURL)
}
if strings.HasPrefix(rawURL, "/forecast/") {
if !strings.Contains(rawURL, "precision=1") || !strings.Contains(rawURL, "tz=America%2FChicago") {
@@ -99,6 +117,16 @@ func TestFetchBundleRecordsSourceHash(t *testing.T) {
if observation.DataSHA256 != want {
t.Fatalf("DataSHA256 = %q, want %q", observation.DataSHA256, want)
}
story := sourceByName(t, bundle.Sources, "weather_story")
if story.Endpoint != "/weatherstories/latest" {
t.Fatalf("weather story endpoint = %q, want /weatherstories/latest", story.Endpoint)
}
if story.DataSHA256 != hashFixtureData(t, "weather_story.json") {
t.Fatalf("weather story DataSHA256 = %q, want fixture hash", story.DataSHA256)
}
if story.IssuedAt == nil || story.UpdatedAt == nil {
t.Fatalf("weather story source timestamps = issued %#v updated %#v, want both", story.IssuedAt, story.UpdatedAt)
}
}
func TestHTTPErrorIsActionable(t *testing.T) {
@@ -169,7 +197,7 @@ func TestMissingSourcePolicyWarnNoneError(t *testing.T) {
wantWarns int
wantSource bool
}{
{name: "warn", policy: config.MissingSourceWarn, wantWarns: 3, wantSource: true},
{name: "warn", policy: config.MissingSourceWarn, wantWarns: 2, wantSource: true},
{name: "none", policy: config.MissingSourceNone, wantWarns: 0, wantSource: true},
{name: "error", policy: config.MissingSourceError, wantErr: true},
}
@@ -230,6 +258,45 @@ func TestMalformedNonRequiredSourceUsesPolicy(t *testing.T) {
}
}
func TestMissingWeatherStoryUsesPolicy(t *testing.T) {
server := fixtureServer(t, map[string]handlerOverride{
"/weatherstories/latest": {status: http.StatusOK, body: `{"data": null}`},
}, nil)
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
"weather_story": config.MissingSourceWarn,
})
bundle, err := client.FetchBundle(context.Background())
if err != nil {
t.Fatalf("FetchBundle() error = %v", err)
}
if bundle.WeatherStory != nil {
t.Fatalf("WeatherStory = %#v, want nil for missing source", bundle.WeatherStory)
}
source := sourceByName(t, bundle.Sources, "weather_story")
if !source.Missing || len(source.Warnings) != 1 {
t.Fatalf("weather_story source = %#v, want missing source warning", source)
}
}
func TestMalformedWeatherStoryUsesPolicy(t *testing.T) {
server := fixtureServer(t, map[string]handlerOverride{
"/weatherstories/latest": {status: http.StatusOK, body: `{"data": {"startTime": 123}}`},
}, nil)
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
"weather_story": config.MissingSourceWarn,
})
bundle, err := client.FetchBundle(context.Background())
if err != nil {
t.Fatalf("FetchBundle() error = %v", err)
}
source := sourceByName(t, bundle.Sources, "weather_story")
if !source.Missing || len(source.Warnings) != 1 || source.Warnings[0].Code != "malformed_source" {
t.Fatalf("weather_story source = %#v, want malformed source warning", source)
}
}
func TestContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-r.Context().Done()
@@ -296,12 +363,13 @@ type handlerOverride struct {
func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested *[]string) *httptest.Server {
t.Helper()
fixtures := map[string]string{
"/observations": "observations.json",
"/conditions/current": "current.json",
"/forecast/hourly": "hourly.json",
"/forecast/narrative": "narrative.json",
"/alerts/active": "alerts.json",
"/discussion": "discussion.json",
"/observations": "observations.json",
"/conditions/current": "current.json",
"/forecast/hourly": "hourly.json",
"/forecast/narrative": "narrative.json",
"/alerts/active": "alerts.json",
"/discussion": "discussion.json",
"/weatherstories/latest": "weather_story.json",
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if requested != nil {