175 lines
6.5 KiB
Go
175 lines
6.5 KiB
Go
// outlooks_mapper_test.go validates outlook row mapping.
|
|
// Layer: adapters/outbound/postgres outlook mapper tests.
|
|
package postgres
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMapOutlookRunParentRowNullables(t *testing.T) {
|
|
asOf := time.Date(2026, 6, 11, 11, 30, 0, 0, time.FixedZone("CDT", -5*3600))
|
|
issuedAt := asOf.Add(-30 * time.Minute)
|
|
latitude := 38.62
|
|
|
|
run := mapOutlookRunParentRow(outlookRunParentRow{
|
|
EventID: "evt-outlook-run",
|
|
LocationID: sql.NullString{String: "stl", Valid: true},
|
|
LocationName: sql.NullString{String: "St. Louis", Valid: true},
|
|
Latitude: sql.NullFloat64{Float64: latitude, Valid: true},
|
|
Longitude: sql.NullFloat64{Valid: false},
|
|
AsOf: asOf,
|
|
IssuedAt: sql.NullTime{Time: issuedAt, Valid: true},
|
|
})
|
|
|
|
if run.LocationID != "stl" || run.LocationName != "St. Louis" {
|
|
t.Fatalf("unexpected location metadata: %+v", run)
|
|
}
|
|
if run.Latitude == nil || *run.Latitude != latitude {
|
|
t.Fatalf("expected latitude pointer %v, got %v", latitude, run.Latitude)
|
|
}
|
|
if run.Longitude != nil {
|
|
t.Fatalf("expected nil longitude, got %v", *run.Longitude)
|
|
}
|
|
if run.AsOf.Location().String() != "UTC" {
|
|
t.Fatalf("expected asOf UTC normalization, got %s", run.AsOf.Location())
|
|
}
|
|
if run.IssuedAt == nil || run.IssuedAt.Location().String() != "UTC" {
|
|
t.Fatalf("expected issuedAt UTC pointer, got %v", run.IssuedAt)
|
|
}
|
|
if run.Outlooks != nil {
|
|
t.Fatalf("expected nil outlooks before child load, got %+v", run.Outlooks)
|
|
}
|
|
}
|
|
|
|
func TestMapOutlookRunParentRowMissingOptionals(t *testing.T) {
|
|
run := mapOutlookRunParentRow(outlookRunParentRow{
|
|
AsOf: time.Date(2026, 6, 11, 16, 30, 0, 0, time.UTC),
|
|
})
|
|
|
|
if run.LocationID != "" || run.LocationName != "" {
|
|
t.Fatalf("expected empty location metadata, got %+v", run)
|
|
}
|
|
if run.Latitude != nil || run.Longitude != nil || run.IssuedAt != nil {
|
|
t.Fatalf("expected nil optional pointers, got %+v", run)
|
|
}
|
|
}
|
|
|
|
func TestMapOutlookRowMapsFields(t *testing.T) {
|
|
validFrom := time.Date(2026, 6, 11, 7, 0, 0, 0, time.FixedZone("CDT", -5*3600))
|
|
validTo := validFrom.Add(12 * time.Hour)
|
|
issuedAt := validFrom.Add(-1 * time.Hour)
|
|
expiresAt := validTo
|
|
severityRank := int64(5)
|
|
geometry := `{"type":"Polygon","coordinates":[[[-91,38],[-90,38],[-90,39],[-91,38]]]}`
|
|
|
|
outlook, err := mapOutlookRow(outlookRow{
|
|
OutlookIndex: 3,
|
|
OutlookID: "spc-20260611-1300-day1-cat-slight",
|
|
Provider: "spc",
|
|
Product: "convective",
|
|
Day: 1,
|
|
OutlookType: "categorical",
|
|
Label: "SLGT",
|
|
LabelText: sql.NullString{String: "Slight Risk", Valid: true},
|
|
SeverityRank: sql.NullInt64{Int64: severityRank, Valid: true},
|
|
ValidFrom: validFrom,
|
|
ValidTo: validTo,
|
|
IssuedAt: issuedAt,
|
|
ExpiresAt: expiresAt,
|
|
Forecaster: sql.NullString{String: "DIAL", Valid: true},
|
|
Headline: sql.NullString{String: "Severe storms possible", Valid: true},
|
|
Summary: sql.NullString{String: "Scattered severe storms are possible.", Valid: true},
|
|
Discussion: sql.NullString{String: "Discussion text.", Valid: true},
|
|
SourceURL: sql.NullString{String: "https://www.spc.noaa.gov/products/outlook/day1otlk.html", Valid: true},
|
|
ImageURL: sql.NullString{String: "https://www.spc.noaa.gov/products/outlook/day1otlk.gif", Valid: true},
|
|
ContainsLocation: true,
|
|
GeometryJSON: geometry,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if outlook.ID != "spc-20260611-1300-day1-cat-slight" {
|
|
t.Fatalf("unexpected outlook id: %q", outlook.ID)
|
|
}
|
|
if outlook.Provider != "spc" || outlook.Product != "convective" || outlook.Day != 1 || outlook.OutlookType != "categorical" {
|
|
t.Fatalf("unexpected core fields: %+v", outlook)
|
|
}
|
|
if outlook.Label != "SLGT" || outlook.LabelText != "Slight Risk" {
|
|
t.Fatalf("unexpected label fields: %+v", outlook)
|
|
}
|
|
if outlook.SeverityRank == nil || *outlook.SeverityRank != int(severityRank) {
|
|
t.Fatalf("expected severity rank %d, got %v", severityRank, outlook.SeverityRank)
|
|
}
|
|
if outlook.Forecaster != "DIAL" || outlook.Headline == "" || outlook.Summary == "" || outlook.Discussion == "" {
|
|
t.Fatalf("unexpected text fields: %+v", outlook)
|
|
}
|
|
if outlook.SourceURL == "" || outlook.ImageURL == "" {
|
|
t.Fatalf("expected source and image URLs: %+v", outlook)
|
|
}
|
|
if !outlook.ContainsLocation {
|
|
t.Fatal("expected containsLocation true")
|
|
}
|
|
if outlook.ValidFrom.Location().String() != "UTC" ||
|
|
outlook.ValidTo.Location().String() != "UTC" ||
|
|
outlook.IssuedAt.Location().String() != "UTC" ||
|
|
outlook.ExpiresAt.Location().String() != "UTC" {
|
|
t.Fatalf("expected UTC timestamps, got %s %s %s %s", outlook.ValidFrom, outlook.ValidTo, outlook.IssuedAt, outlook.ExpiresAt)
|
|
}
|
|
if string(outlook.Geometry) != geometry {
|
|
t.Fatalf("expected geometry bytes preserved, got %s", outlook.Geometry)
|
|
}
|
|
}
|
|
|
|
func TestMapOutlookRowMissingOptionals(t *testing.T) {
|
|
outlook, err := mapOutlookRow(outlookRow{
|
|
OutlookID: "tor-1",
|
|
Provider: "spc",
|
|
Product: "convective",
|
|
Day: 1,
|
|
OutlookType: "tornado",
|
|
Label: "2%",
|
|
ValidFrom: time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC),
|
|
ValidTo: time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
|
|
IssuedAt: time.Date(2026, 6, 11, 11, 0, 0, 0, time.UTC),
|
|
ExpiresAt: time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
|
|
ContainsLocation: false,
|
|
GeometryJSON: `{"type":"Point","coordinates":[-90.2,38.6]}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if outlook.LabelText != "" || outlook.Forecaster != "" || outlook.Headline != "" ||
|
|
outlook.Summary != "" || outlook.Discussion != "" || outlook.SourceURL != "" || outlook.ImageURL != "" {
|
|
t.Fatalf("expected optional strings to map to empty values, got %+v", outlook)
|
|
}
|
|
if outlook.SeverityRank != nil {
|
|
t.Fatalf("expected nil severity rank, got %v", *outlook.SeverityRank)
|
|
}
|
|
if outlook.ContainsLocation {
|
|
t.Fatal("expected containsLocation false")
|
|
}
|
|
}
|
|
|
|
func TestMapOutlookRowRejectsInvalidGeometry(t *testing.T) {
|
|
_, err := mapOutlookRow(outlookRow{
|
|
OutlookID: "bad-geometry",
|
|
Provider: "spc",
|
|
Product: "convective",
|
|
Day: 1,
|
|
OutlookType: "categorical",
|
|
Label: "SLGT",
|
|
ValidFrom: time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC),
|
|
ValidTo: time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
|
|
IssuedAt: time.Date(2026, 6, 11, 11, 0, 0, 0, time.UTC),
|
|
ExpiresAt: time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC),
|
|
GeometryJSON: `{"type":"Point"`,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected invalid geometry error")
|
|
}
|
|
}
|