Simplified the forecast schema and removed fields deprecated upstream in weatherfeeder
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -394,6 +394,51 @@ func TestForecastPrecisionTwo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForecastJSONOmitsLegacyDescriptionFields(t *testing.T) {
|
||||||
|
h := newHandler(t, &fakeService{
|
||||||
|
forecast: &model.WeatherForecastRun{
|
||||||
|
Product: model.ForecastProductHourly,
|
||||||
|
IssuedAt: time.Now().UTC(),
|
||||||
|
Periods: []model.WeatherForecastPeriod{{
|
||||||
|
StartTime: time.Now().UTC(),
|
||||||
|
EndTime: time.Now().UTC().Add(time.Hour),
|
||||||
|
ConditionCode: model.WMOUnknown,
|
||||||
|
TextDescription: "Cloudy",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}, "/forecast/hourly")
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/forecast/hourly", nil)
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Data struct {
|
||||||
|
Periods []map[string]any `json:"periods"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode forecast payload: %v", err)
|
||||||
|
}
|
||||||
|
if len(payload.Data.Periods) == 0 {
|
||||||
|
t.Fatalf("expected at least one period")
|
||||||
|
}
|
||||||
|
|
||||||
|
period := payload.Data.Periods[0]
|
||||||
|
for _, key := range []string{"conditionText", "providerRawDescription", "detailedText", "iconUrl"} {
|
||||||
|
if _, ok := period[key]; ok {
|
||||||
|
t.Fatalf("unexpected legacy field %q in forecast response period: %#v", key, period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if period["textDescription"] != "Cloudy" {
|
||||||
|
t.Fatalf("expected textDescription Cloudy, got %#v", period["textDescription"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestForecastTimezoneOffsetUppercaseTZConvertsAllTimes(t *testing.T) {
|
func TestForecastTimezoneOffsetUppercaseTZConvertsAllTimes(t *testing.T) {
|
||||||
issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC)
|
issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC)
|
||||||
updatedAt := issuedAt.Add(30 * time.Minute)
|
updatedAt := issuedAt.Add(30 * time.Minute)
|
||||||
|
|||||||
@@ -28,11 +28,7 @@ type WeatherForecastPeriodUS struct {
|
|||||||
Name string `json:"name,omitempty" xml:"name,omitempty"`
|
Name string `json:"name,omitempty" xml:"name,omitempty"`
|
||||||
IsDay *bool `json:"isDay,omitempty" xml:"isDay,omitempty"`
|
IsDay *bool `json:"isDay,omitempty" xml:"isDay,omitempty"`
|
||||||
ConditionCode model.WMOCode `json:"conditionCode" xml:"conditionCode"`
|
ConditionCode model.WMOCode `json:"conditionCode" xml:"conditionCode"`
|
||||||
ConditionText string `json:"conditionText,omitempty" xml:"conditionText,omitempty"`
|
|
||||||
ProviderRawDescription string `json:"providerRawDescription,omitempty" xml:"providerRawDescription,omitempty"`
|
|
||||||
TextDescription string `json:"textDescription,omitempty" xml:"textDescription,omitempty"`
|
TextDescription string `json:"textDescription,omitempty" xml:"textDescription,omitempty"`
|
||||||
DetailedText string `json:"detailedText,omitempty" xml:"detailedText,omitempty"`
|
|
||||||
IconURL string `json:"iconUrl,omitempty" xml:"iconUrl,omitempty"`
|
|
||||||
TemperatureF *float64 `json:"temperatureF,omitempty" xml:"temperatureF,omitempty"`
|
TemperatureF *float64 `json:"temperatureF,omitempty" xml:"temperatureF,omitempty"`
|
||||||
TemperatureFMin *float64 `json:"temperatureFMin,omitempty" xml:"temperatureFMin,omitempty"`
|
TemperatureFMin *float64 `json:"temperatureFMin,omitempty" xml:"temperatureFMin,omitempty"`
|
||||||
TemperatureFMax *float64 `json:"temperatureFMax,omitempty" xml:"temperatureFMax,omitempty"`
|
TemperatureFMax *float64 `json:"temperatureFMax,omitempty" xml:"temperatureFMax,omitempty"`
|
||||||
@@ -74,11 +70,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int,
|
|||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsDay: copyBoolPtr(p.IsDay),
|
IsDay: copyBoolPtr(p.IsDay),
|
||||||
ConditionCode: p.ConditionCode,
|
ConditionCode: p.ConditionCode,
|
||||||
ConditionText: p.ConditionText,
|
|
||||||
ProviderRawDescription: p.ProviderRawDescription,
|
|
||||||
TextDescription: p.TextDescription,
|
TextDescription: p.TextDescription,
|
||||||
DetailedText: p.DetailedText,
|
|
||||||
IconURL: p.IconURL,
|
|
||||||
TemperatureF: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureC), precision),
|
TemperatureF: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureC), precision),
|
||||||
TemperatureFMin: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMin), precision),
|
TemperatureFMin: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMin), precision),
|
||||||
TemperatureFMax: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMax), precision),
|
TemperatureFMax: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMax), precision),
|
||||||
@@ -119,11 +111,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int,
|
|||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
IsDay: copyBoolPtr(p.IsDay),
|
IsDay: copyBoolPtr(p.IsDay),
|
||||||
ConditionCode: p.ConditionCode,
|
ConditionCode: p.ConditionCode,
|
||||||
ConditionText: p.ConditionText,
|
|
||||||
ProviderRawDescription: p.ProviderRawDescription,
|
|
||||||
TextDescription: p.TextDescription,
|
TextDescription: p.TextDescription,
|
||||||
DetailedText: p.DetailedText,
|
|
||||||
IconURL: p.IconURL,
|
|
||||||
TemperatureC: roundedPtr(copyFloat64Ptr(p.TemperatureC), precision),
|
TemperatureC: roundedPtr(copyFloat64Ptr(p.TemperatureC), precision),
|
||||||
TemperatureCMin: roundedPtr(copyFloat64Ptr(p.TemperatureCMin), precision),
|
TemperatureCMin: roundedPtr(copyFloat64Ptr(p.TemperatureCMin), precision),
|
||||||
TemperatureCMax: roundedPtr(copyFloat64Ptr(p.TemperatureCMax), precision),
|
TemperatureCMax: roundedPtr(copyFloat64Ptr(p.TemperatureCMax), precision),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package presenter
|
package presenter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -83,6 +84,22 @@ func TestForecastPayloadUS(t *testing.T) {
|
|||||||
assertApprox(t, period.SnowfallDepthIn, 2.0, 0.0001)
|
assertApprox(t, period.SnowfallDepthIn, 2.0, 0.0001)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForecastPayloadOmitsLegacyDescriptionFields(t *testing.T) {
|
||||||
|
run := &model.WeatherForecastRun{
|
||||||
|
Product: model.ForecastProductHourly,
|
||||||
|
IssuedAt: time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC),
|
||||||
|
Periods: []model.WeatherForecastPeriod{{
|
||||||
|
StartTime: time.Date(2026, 3, 20, 12, 0, 0, 0, time.UTC),
|
||||||
|
EndTime: time.Date(2026, 3, 20, 13, 0, 0, 0, time.UTC),
|
||||||
|
ConditionCode: model.WMOUnknown,
|
||||||
|
TextDescription: "Cloudy",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
assertForecastPayloadHasNoLegacyDescriptionFields(t, ForecastPayload(run, UnitsMetric, 0, nil))
|
||||||
|
assertForecastPayloadHasNoLegacyDescriptionFields(t, ForecastPayload(run, UnitsUS, 0, nil))
|
||||||
|
}
|
||||||
|
|
||||||
func TestForecastPayloadTimezoneConversionMetricAndUS(t *testing.T) {
|
func TestForecastPayloadTimezoneConversionMetricAndUS(t *testing.T) {
|
||||||
loc := time.FixedZone("UTC-05:00", -5*60*60)
|
loc := time.FixedZone("UTC-05:00", -5*60*60)
|
||||||
issuedAt := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
|
issuedAt := time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC)
|
||||||
@@ -308,3 +325,34 @@ func assertOffsetSeconds(t *testing.T, ts time.Time, want int) {
|
|||||||
t.Fatalf("expected offset %d, got %d for %s", want, got, ts.Format(time.RFC3339))
|
t.Fatalf("expected offset %d, got %d for %s", want, got, ts.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertForecastPayloadHasNoLegacyDescriptionFields(t *testing.T, payload any) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
b, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal(payload) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var root map[string]any
|
||||||
|
if err := json.Unmarshal(b, &root); err != nil {
|
||||||
|
t.Fatalf("json.Unmarshal(payload) error = %v", err)
|
||||||
|
}
|
||||||
|
periodsRaw, ok := root["periods"].([]any)
|
||||||
|
if !ok || len(periodsRaw) == 0 {
|
||||||
|
t.Fatalf("expected non-empty periods in payload: %#v", root["periods"])
|
||||||
|
}
|
||||||
|
period, ok := periodsRaw[0].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected first period map, got %#v", periodsRaw[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range []string{"conditionText", "providerRawDescription", "detailedText", "iconUrl"} {
|
||||||
|
if _, exists := period[key]; exists {
|
||||||
|
t.Fatalf("unexpected legacy field %q in payload period: %#v", key, period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if period["textDescription"] != "Cloudy" {
|
||||||
|
t.Fatalf("expected textDescription Cloudy, got %#v", period["textDescription"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,11 +24,7 @@ func mapForecastPeriodRow(row forecastPeriodRow) model.WeatherForecastPeriod {
|
|||||||
Name: stringValue(row.Name),
|
Name: stringValue(row.Name),
|
||||||
IsDay: boolPtr(row.IsDay),
|
IsDay: boolPtr(row.IsDay),
|
||||||
ConditionCode: model.WMOCode(row.ConditionCode),
|
ConditionCode: model.WMOCode(row.ConditionCode),
|
||||||
ConditionText: stringValue(row.ConditionText),
|
|
||||||
ProviderRawDescription: stringValue(row.ProviderRawDescription),
|
|
||||||
TextDescription: stringValue(row.TextDescription),
|
TextDescription: stringValue(row.TextDescription),
|
||||||
DetailedText: stringValue(row.DetailedText),
|
|
||||||
IconURL: stringValue(row.IconURL),
|
|
||||||
TemperatureC: float64Ptr(row.TemperatureC),
|
TemperatureC: float64Ptr(row.TemperatureC),
|
||||||
TemperatureCMin: float64Ptr(row.TemperatureCMin),
|
TemperatureCMin: float64Ptr(row.TemperatureCMin),
|
||||||
TemperatureCMax: float64Ptr(row.TemperatureCMax),
|
TemperatureCMax: float64Ptr(row.TemperatureCMax),
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ SELECT
|
|||||||
name,
|
name,
|
||||||
is_day,
|
is_day,
|
||||||
condition_code,
|
condition_code,
|
||||||
condition_text,
|
|
||||||
provider_raw_description,
|
|
||||||
text_description,
|
text_description,
|
||||||
detailed_text,
|
|
||||||
icon_url,
|
|
||||||
temperature_c,
|
temperature_c,
|
||||||
temperature_c_min,
|
temperature_c_min,
|
||||||
temperature_c_max,
|
temperature_c_max,
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ func (r *Repository) loadForecastPeriods(ctx context.Context, eventID string) ([
|
|||||||
&row.Name,
|
&row.Name,
|
||||||
&row.IsDay,
|
&row.IsDay,
|
||||||
&row.ConditionCode,
|
&row.ConditionCode,
|
||||||
&row.ConditionText,
|
|
||||||
&row.ProviderRawDescription,
|
|
||||||
&row.TextDescription,
|
&row.TextDescription,
|
||||||
&row.DetailedText,
|
|
||||||
&row.IconURL,
|
|
||||||
&row.TemperatureC,
|
&row.TemperatureC,
|
||||||
&row.TemperatureCMin,
|
&row.TemperatureCMin,
|
||||||
&row.TemperatureCMax,
|
&row.TemperatureCMax,
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ type forecastPeriodRow struct {
|
|||||||
Name sql.NullString
|
Name sql.NullString
|
||||||
IsDay sql.NullBool
|
IsDay sql.NullBool
|
||||||
ConditionCode int
|
ConditionCode int
|
||||||
ConditionText sql.NullString
|
|
||||||
ProviderRawDescription sql.NullString
|
|
||||||
TextDescription sql.NullString
|
TextDescription sql.NullString
|
||||||
DetailedText sql.NullString
|
|
||||||
IconURL sql.NullString
|
|
||||||
TemperatureC sql.NullFloat64
|
TemperatureC sql.NullFloat64
|
||||||
TemperatureCMin sql.NullFloat64
|
TemperatureCMin sql.NullFloat64
|
||||||
TemperatureCMax sql.NullFloat64
|
TemperatureCMax sql.NullFloat64
|
||||||
|
|||||||
Reference in New Issue
Block a user