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) {
|
||||
issuedAt := time.Date(2026, 7, 10, 15, 0, 0, 0, time.UTC)
|
||||
updatedAt := issuedAt.Add(30 * time.Minute)
|
||||
|
||||
@@ -28,11 +28,7 @@ type WeatherForecastPeriodUS struct {
|
||||
Name string `json:"name,omitempty" xml:"name,omitempty"`
|
||||
IsDay *bool `json:"isDay,omitempty" xml:"isDay,omitempty"`
|
||||
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"`
|
||||
DetailedText string `json:"detailedText,omitempty" xml:"detailedText,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty" xml:"iconUrl,omitempty"`
|
||||
TemperatureF *float64 `json:"temperatureF,omitempty" xml:"temperatureF,omitempty"`
|
||||
TemperatureFMin *float64 `json:"temperatureFMin,omitempty" xml:"temperatureFMin,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,
|
||||
IsDay: copyBoolPtr(p.IsDay),
|
||||
ConditionCode: p.ConditionCode,
|
||||
ConditionText: p.ConditionText,
|
||||
ProviderRawDescription: p.ProviderRawDescription,
|
||||
TextDescription: p.TextDescription,
|
||||
DetailedText: p.DetailedText,
|
||||
IconURL: p.IconURL,
|
||||
TemperatureF: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureC), precision),
|
||||
TemperatureFMin: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMin), precision),
|
||||
TemperatureFMax: roundedPtr(celsiusToFahrenheitPtr(p.TemperatureCMax), precision),
|
||||
@@ -119,11 +111,7 @@ func ForecastPayload(run *model.WeatherForecastRun, units Units, precision int,
|
||||
Name: p.Name,
|
||||
IsDay: copyBoolPtr(p.IsDay),
|
||||
ConditionCode: p.ConditionCode,
|
||||
ConditionText: p.ConditionText,
|
||||
ProviderRawDescription: p.ProviderRawDescription,
|
||||
TextDescription: p.TextDescription,
|
||||
DetailedText: p.DetailedText,
|
||||
IconURL: p.IconURL,
|
||||
TemperatureC: roundedPtr(copyFloat64Ptr(p.TemperatureC), precision),
|
||||
TemperatureCMin: roundedPtr(copyFloat64Ptr(p.TemperatureCMin), precision),
|
||||
TemperatureCMax: roundedPtr(copyFloat64Ptr(p.TemperatureCMax), precision),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package presenter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -83,6 +84,22 @@ func TestForecastPayloadUS(t *testing.T) {
|
||||
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) {
|
||||
loc := time.FixedZone("UTC-05:00", -5*60*60)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
IsDay: boolPtr(row.IsDay),
|
||||
ConditionCode: model.WMOCode(row.ConditionCode),
|
||||
ConditionText: stringValue(row.ConditionText),
|
||||
ProviderRawDescription: stringValue(row.ProviderRawDescription),
|
||||
TextDescription: stringValue(row.TextDescription),
|
||||
DetailedText: stringValue(row.DetailedText),
|
||||
IconURL: stringValue(row.IconURL),
|
||||
TemperatureC: float64Ptr(row.TemperatureC),
|
||||
TemperatureCMin: float64Ptr(row.TemperatureCMin),
|
||||
TemperatureCMax: float64Ptr(row.TemperatureCMax),
|
||||
|
||||
@@ -27,11 +27,7 @@ SELECT
|
||||
name,
|
||||
is_day,
|
||||
condition_code,
|
||||
condition_text,
|
||||
provider_raw_description,
|
||||
text_description,
|
||||
detailed_text,
|
||||
icon_url,
|
||||
temperature_c,
|
||||
temperature_c_min,
|
||||
temperature_c_max,
|
||||
|
||||
@@ -63,11 +63,7 @@ func (r *Repository) loadForecastPeriods(ctx context.Context, eventID string) ([
|
||||
&row.Name,
|
||||
&row.IsDay,
|
||||
&row.ConditionCode,
|
||||
&row.ConditionText,
|
||||
&row.ProviderRawDescription,
|
||||
&row.TextDescription,
|
||||
&row.DetailedText,
|
||||
&row.IconURL,
|
||||
&row.TemperatureC,
|
||||
&row.TemperatureCMin,
|
||||
&row.TemperatureCMax,
|
||||
|
||||
@@ -26,11 +26,7 @@ type forecastPeriodRow struct {
|
||||
Name sql.NullString
|
||||
IsDay sql.NullBool
|
||||
ConditionCode int
|
||||
ConditionText sql.NullString
|
||||
ProviderRawDescription sql.NullString
|
||||
TextDescription sql.NullString
|
||||
DetailedText sql.NullString
|
||||
IconURL sql.NullString
|
||||
TemperatureC sql.NullFloat64
|
||||
TemperatureCMin sql.NullFloat64
|
||||
TemperatureCMax sql.NullFloat64
|
||||
|
||||
Reference in New Issue
Block a user