All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
package nws
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/ejr/feedkit/config"
|
|
"gitea.maximumdirect.net/ejr/feedkit/event"
|
|
"gitea.maximumdirect.net/ejr/weatherfeeder/standards"
|
|
)
|
|
|
|
type forecastPoller interface {
|
|
Poll(ctx context.Context) ([]event.Event, error)
|
|
}
|
|
|
|
func TestForecastSourcesEmitExpectedSchemaAndPreferGeneratedAt(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
driver string
|
|
wantSchema string
|
|
newSource func(config.SourceConfig) (forecastPoller, error)
|
|
}{
|
|
{
|
|
name: "hourly",
|
|
driver: "nws_forecast_hourly",
|
|
wantSchema: standards.SchemaRawNWSHourlyForecastV1,
|
|
newSource: func(cfg config.SourceConfig) (forecastPoller, error) {
|
|
return NewHourlyForecastSource(cfg)
|
|
},
|
|
},
|
|
{
|
|
name: "narrative",
|
|
driver: "nws_forecast_narrative",
|
|
wantSchema: standards.SchemaRawNWSNarrativeForecastV1,
|
|
newSource: func(cfg config.SourceConfig) (forecastPoller, error) {
|
|
return NewNarrativeForecastSource(cfg)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"properties":{"generatedAt":"2026-03-28T12:00:00Z","updateTime":"2026-03-28T11:00:00Z"}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
src, err := tt.newSource(forecastSourceConfig(tt.driver, srv.URL))
|
|
if err != nil {
|
|
t.Fatalf("newSource() error = %v", err)
|
|
}
|
|
if ks, ok := src.(interface{ Kinds() []event.Kind }); !ok {
|
|
t.Fatalf("source does not implement Kinds()")
|
|
} else if gotKinds := ks.Kinds(); len(gotKinds) != 1 || gotKinds[0] != event.Kind("forecast") {
|
|
t.Fatalf("Kinds() = %#v, want [forecast]", gotKinds)
|
|
}
|
|
|
|
got, err := src.Poll(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Poll() error = %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("Poll() len = %d, want 1", len(got))
|
|
}
|
|
if got[0].Schema != tt.wantSchema {
|
|
t.Fatalf("Poll() schema = %q, want %q", got[0].Schema, tt.wantSchema)
|
|
}
|
|
if got[0].Kind != event.Kind("forecast") {
|
|
t.Fatalf("Poll() kind = %q, want forecast", got[0].Kind)
|
|
}
|
|
|
|
wantEffectiveAt := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
|
if got[0].EffectiveAt == nil || !got[0].EffectiveAt.Equal(wantEffectiveAt) {
|
|
t.Fatalf("Poll() effectiveAt = %v, want %s", got[0].EffectiveAt, wantEffectiveAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForecastSourcePollEffectiveAtFallbackOrder(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
wantEffectiveAt *time.Time
|
|
}{
|
|
{
|
|
name: "updateTime fallback",
|
|
body: `{"properties":{"updateTime":"2026-03-28T11:00:00Z"}}`,
|
|
wantEffectiveAt: func() *time.Time {
|
|
t := time.Date(2026, 3, 28, 11, 0, 0, 0, time.UTC)
|
|
return &t
|
|
}(),
|
|
},
|
|
{
|
|
name: "updated fallback",
|
|
body: `{"properties":{"updated":"2026-03-28T10:00:00Z"}}`,
|
|
wantEffectiveAt: func() *time.Time {
|
|
t := time.Date(2026, 3, 28, 10, 0, 0, 0, time.UTC)
|
|
return &t
|
|
}(),
|
|
},
|
|
{
|
|
name: "omitted when metadata lacks timestamps",
|
|
body: `{"properties":{}}`,
|
|
wantEffectiveAt: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(tt.body))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
src, err := NewHourlyForecastSource(forecastSourceConfig("nws_forecast_hourly", srv.URL))
|
|
if err != nil {
|
|
t.Fatalf("NewHourlyForecastSource() error = %v", err)
|
|
}
|
|
|
|
got, err := src.Poll(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Poll() error = %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("Poll() len = %d, want 1", len(got))
|
|
}
|
|
if tt.wantEffectiveAt == nil {
|
|
if got[0].EffectiveAt != nil {
|
|
t.Fatalf("Poll() effectiveAt = %v, want nil", got[0].EffectiveAt)
|
|
}
|
|
return
|
|
}
|
|
if got[0].EffectiveAt == nil || !got[0].EffectiveAt.Equal(*tt.wantEffectiveAt) {
|
|
t.Fatalf("Poll() effectiveAt = %v, want %s", got[0].EffectiveAt, *tt.wantEffectiveAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestForecastSourcePollMetadataDecodeFailureStillEmitsRawEvent(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`not-json`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
src, err := NewNarrativeForecastSource(forecastSourceConfig("nws_forecast_narrative", srv.URL))
|
|
if err != nil {
|
|
t.Fatalf("NewNarrativeForecastSource() error = %v", err)
|
|
}
|
|
|
|
got, err := src.Poll(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Poll() error = %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("Poll() len = %d, want 1", len(got))
|
|
}
|
|
if got[0].EffectiveAt != nil {
|
|
t.Fatalf("Poll() effectiveAt = %v, want nil", got[0].EffectiveAt)
|
|
}
|
|
if got[0].Schema != standards.SchemaRawNWSNarrativeForecastV1 {
|
|
t.Fatalf("Poll() schema = %q, want %q", got[0].Schema, standards.SchemaRawNWSNarrativeForecastV1)
|
|
}
|
|
|
|
raw, ok := got[0].Payload.(json.RawMessage)
|
|
if !ok {
|
|
t.Fatalf("Poll() payload type = %T, want json.RawMessage", got[0].Payload)
|
|
}
|
|
if string(raw) != "not-json" {
|
|
t.Fatalf("Poll() payload = %q, want %q", string(raw), "not-json")
|
|
}
|
|
}
|
|
|
|
func forecastSourceConfig(driver, url string) config.SourceConfig {
|
|
return config.SourceConfig{
|
|
Name: "test-forecast-source",
|
|
Driver: driver,
|
|
Mode: config.SourceModePoll,
|
|
Params: map[string]any{
|
|
"url": url,
|
|
"user_agent": "test-agent",
|
|
},
|
|
}
|
|
}
|