Add convective outlook Postgres read adapter
This commit is contained in:
174
internal/adapters/outbound/postgres/outlooks_mapper_test.go
Normal file
174
internal/adapters/outbound/postgres/outlooks_mapper_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user