package nws import ( "encoding/json" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/ejr/feedkit/event" "gitea.maximumdirect.net/ejr/weatherfeeder/model" "gitea.maximumdirect.net/ejr/weatherfeeder/standards" ) func TestForecastDiscussionNormalizerProducesCanonicalSchema(t *testing.T) { rawHTML := loadForecastDiscussionSampleHTML(t) out, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{ ID: "evt-discussion-1", Kind: event.Kind("forecast_discussion"), Source: "nws-discussion-test", EmittedAt: time.Date(2026, 3, 28, 19, 25, 0, 0, time.UTC), Schema: standards.SchemaRawNWSForecastDiscussionV1, Payload: rawHTML, }) if err != nil { t.Fatalf("Normalize() error = %v", err) } if out == nil { t.Fatalf("Normalize() returned nil output") } if out.Schema != standards.SchemaWeatherForecastDiscussionV1 { t.Fatalf("Schema = %q, want %q", out.Schema, standards.SchemaWeatherForecastDiscussionV1) } if out.Kind != event.Kind("forecast_discussion") { t.Fatalf("Kind = %q, want forecast_discussion", out.Kind) } payload, ok := out.Payload.(model.WeatherForecastDiscussion) if !ok { t.Fatalf("Payload type = %T, want model.WeatherForecastDiscussion", out.Payload) } if payload.OfficeID != "LSX" { t.Fatalf("OfficeID = %q, want LSX", payload.OfficeID) } if payload.Product != model.ForecastDiscussionProductAFD { t.Fatalf("Product = %q, want %q", payload.Product, model.ForecastDiscussionProductAFD) } if len(payload.KeyMessages) != 3 { t.Fatalf("KeyMessages len = %d, want 3", len(payload.KeyMessages)) } if payload.ShortTerm == nil || payload.LongTerm == nil { t.Fatalf("ShortTerm=%v LongTerm=%v, want both populated", payload.ShortTerm, payload.LongTerm) } if payload.ShortTerm.Qualifier != "(Through Late Sunday Night)" { t.Fatalf("ShortTerm.Qualifier = %q", payload.ShortTerm.Qualifier) } if !strings.Contains(payload.ShortTerm.Text, "After a chilly morning") { t.Fatalf("ShortTerm.Text = %q, want normalized prose", payload.ShortTerm.Text) } if strings.Contains(payload.ShortTerm.Text, "BRC") { t.Fatalf("ShortTerm.Text contains signature: %q", payload.ShortTerm.Text) } if strings.Contains(payload.LongTerm.Text, "AVIATION") || strings.Contains(payload.LongTerm.Text, "WATCHES/WARNINGS/ADVISORIES") { t.Fatalf("LongTerm.Text includes downstream sections: %q", payload.LongTerm.Text) } wantEffectiveAt := time.Date(2026, 3, 28, 19, 24, 0, 0, time.UTC) if out.EffectiveAt == nil || !out.EffectiveAt.Equal(wantEffectiveAt) { t.Fatalf("EffectiveAt = %v, want %s", out.EffectiveAt, wantEffectiveAt.Format(time.RFC3339)) } } func TestForecastDiscussionNormalizerRejectsMissingIssueTime(t *testing.T) { _, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{ ID: "evt-discussion-bad", Kind: event.Kind("forecast_discussion"), Source: "nws-discussion-test", EmittedAt: time.Date(2026, 3, 28, 19, 25, 0, 0, time.UTC), Schema: standards.SchemaRawNWSForecastDiscussionV1, Payload: "
National Weather Service Saint Louis MO
", }) if err == nil { t.Fatalf("Normalize() error = nil, want error") } if !strings.Contains(err.Error(), "issue time") { t.Fatalf("error = %q, want issue time context", err) } } func TestForecastDiscussionNormalizerWireShapeHasNoUnexpectedKeys(t *testing.T) { rawHTML := loadForecastDiscussionSampleHTML(t) out, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{ ID: "evt-discussion-2", Kind: event.Kind("forecast_discussion"), Source: "nws-discussion-test", EmittedAt: time.Date(2026, 3, 28, 19, 25, 0, 0, time.UTC), Schema: standards.SchemaRawNWSForecastDiscussionV1, Payload: rawHTML, }) if err != nil { t.Fatalf("Normalize() error = %v", err) } b, err := json.Marshal(out.Payload) if err != nil { t.Fatalf("json.Marshal(payload) error = %v", err) } var got map[string]any if err := json.Unmarshal(b, &got); err != nil { t.Fatalf("json.Unmarshal(payload) error = %v", err) } for _, key := range []string{"sections", "aviation"} { if _, ok := got[key]; ok { t.Fatalf("unexpected key %q in canonical payload", key) } } } func loadForecastDiscussionSampleHTML(t *testing.T) string { t.Helper() path := filepath.Join("..", "..", "providers", "nws", "testdata", "forecast_discussion_sample.html") b, err := os.ReadFile(path) if err != nil { t.Fatalf("os.ReadFile(%q) error = %v", path, err) } return string(b) }