156 lines
5.3 KiB
Go
156 lines
5.3 KiB
Go
package weatherdata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestConvectiveOutlookRunJSONPreservesGeometry(t *testing.T) {
|
|
severity := 3
|
|
validFrom := mustParseBundleTime("2026-06-12T13:00:00Z")
|
|
validTo := mustParseBundleTime("2026-06-13T12:00:00Z")
|
|
issuedAt := mustParseBundleTime("2026-06-12T12:30:00Z")
|
|
updatedAt := mustParseBundleTime("2026-06-12T12:45:00Z")
|
|
run := ConvectiveOutlookRun{
|
|
LocationID: "test-grid",
|
|
LocationName: "Brentwood",
|
|
AsOf: &updatedAt,
|
|
IssuedAt: &issuedAt,
|
|
Product: "convective_outlook",
|
|
Outlooks: []ConvectiveOutlook{{
|
|
ID: "day1-categorical-slight",
|
|
Provider: "spc",
|
|
Product: "convective_outlook",
|
|
Day: 1,
|
|
OutlookType: "categorical",
|
|
Label: "SLGT",
|
|
LabelText: "Slight Risk",
|
|
Forecaster: "Smith",
|
|
SeverityRank: &severity,
|
|
ValidFrom: validFrom,
|
|
ValidTo: validTo,
|
|
IssuedAt: &issuedAt,
|
|
ExpiresAt: &validTo,
|
|
SourceURL: "https://example.test/source",
|
|
ImageURL: "https://example.test/image.png",
|
|
ContainsLocation: true,
|
|
Geometry: json.RawMessage(`{"type":"Polygon","coordinates":[[[-90.1,38.1],[-90.0,38.2],[-90.1,38.1]]]}`),
|
|
}},
|
|
Discussions: []ConvectiveOutlookDiscussion{{
|
|
Day: 1,
|
|
Headline: "Severe storms possible",
|
|
Summary: "Scattered severe storms are possible.",
|
|
Discussion: "Discussion text.",
|
|
UpdatedAt: &updatedAt,
|
|
}},
|
|
}
|
|
|
|
data, err := json.Marshal(run)
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
|
|
var decoded ConvectiveOutlookRun
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if len(decoded.Outlooks) != 1 || string(decoded.Outlooks[0].Geometry) == "" {
|
|
t.Fatalf("decoded outlooks = %#v, want preserved geometry", decoded.Outlooks)
|
|
}
|
|
if got := string(decoded.Outlooks[0].Geometry); got != `{"type":"Polygon","coordinates":[[[-90.1,38.1],[-90.0,38.2],[-90.1,38.1]]]}` {
|
|
t.Fatalf("Geometry = %s, want preserved GeoJSON coordinates", got)
|
|
}
|
|
if decoded.Outlooks[0].SeverityRank == nil || *decoded.Outlooks[0].SeverityRank != severity {
|
|
t.Fatalf("SeverityRank = %#v, want %d", decoded.Outlooks[0].SeverityRank, severity)
|
|
}
|
|
if len(decoded.Discussions) != 1 || decoded.Discussions[0].Headline != "Severe storms possible" {
|
|
t.Fatalf("Discussions = %#v, want decoded discussion", decoded.Discussions)
|
|
}
|
|
}
|
|
|
|
func TestForecastPeriodHasUsableTimeBounds(t *testing.T) {
|
|
start := mustParseBundleTime("2026-05-29T13:00:00Z")
|
|
end := mustParseBundleTime("2026-05-29T14:00:00Z")
|
|
tests := []struct {
|
|
name string
|
|
period ForecastPeriod
|
|
want bool
|
|
}{
|
|
{name: "valid", period: ForecastPeriod{StartTime: start, EndTime: end}, want: true},
|
|
{name: "missing start", period: ForecastPeriod{EndTime: end}},
|
|
{name: "missing end", period: ForecastPeriod{StartTime: start}},
|
|
{name: "empty range", period: ForecastPeriod{StartTime: start, EndTime: start}},
|
|
{name: "reversed range", period: ForecastPeriod{StartTime: end, EndTime: start}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.period.HasUsableTimeBounds(); got != tt.want {
|
|
t.Fatalf("HasUsableTimeBounds() = %t, want %t", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForecastPeriodHasValidPrecipitationProbability(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
value *float64
|
|
want bool
|
|
}{
|
|
{name: "missing", want: true},
|
|
{name: "zero", value: ptrProbability(0), want: true},
|
|
{name: "one hundred", value: ptrProbability(100), want: true},
|
|
{name: "negative", value: ptrProbability(-0.1)},
|
|
{name: "above one hundred", value: ptrProbability(100.1)},
|
|
{name: "not a number", value: ptrProbability(math.NaN())},
|
|
{name: "infinite", value: ptrProbability(math.Inf(1))},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
period := ForecastPeriod{ProbabilityOfPrecipitationPercent: tt.value}
|
|
if got := period.HasValidPrecipitationProbability(); got != tt.want {
|
|
t.Fatalf("HasValidPrecipitationProbability() = %t, want %t", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWeatherStoryHasUsableContent(t *testing.T) {
|
|
start := mustParseBundleTime("2026-05-29T13:00:00Z")
|
|
end := mustParseBundleTime("2026-05-29T14:00:00Z")
|
|
for _, tt := range []struct {
|
|
name string
|
|
story WeatherStory
|
|
want bool
|
|
}{
|
|
{name: "empty"},
|
|
{name: "whitespace title", story: WeatherStory{Title: " \t "}},
|
|
{name: "title", story: WeatherStory{Title: "Rain Chances"}, want: true},
|
|
{name: "description", story: WeatherStory{Description: "Showers expected."}, want: true},
|
|
{name: "alternate text", story: WeatherStory{AltText: "Rain chances graphic"}, want: true},
|
|
{name: "download URL", story: WeatherStory{DownloadURL: "https://example.invalid/story.png"}, want: true},
|
|
{name: "valid period", story: WeatherStory{StartTime: start, EndTime: end}, want: true},
|
|
{name: "reversed period", story: WeatherStory{StartTime: end, EndTime: start}},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.story.HasUsableContent(); got != tt.want {
|
|
t.Fatalf("HasUsableContent() = %t, want %t", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustParseBundleTime(value string) time.Time {
|
|
parsed, err := time.Parse(time.RFC3339, value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func ptrProbability(value float64) *float64 {
|
|
return &value
|
|
}
|