562 lines
19 KiB
Go
562 lines
19 KiB
Go
package weatherapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
|
)
|
|
|
|
func TestFetchBundleFromFixtures(t *testing.T) {
|
|
var requested []string
|
|
server := fixtureServer(t, nil, &requested)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
|
|
if bundle.Observation == nil || bundle.Observation.StationID != "KSTL" {
|
|
t.Fatalf("Observation = %#v, want KSTL observation", bundle.Observation)
|
|
}
|
|
if bundle.Current == nil || bundle.Current.ConditionText != "Partly cloudy" {
|
|
t.Fatalf("Current = %#v, want current conditions", bundle.Current)
|
|
}
|
|
if bundle.Hourly == nil || len(bundle.Hourly.Periods) != 1 {
|
|
t.Fatalf("Hourly = %#v, want one hourly period", bundle.Hourly)
|
|
}
|
|
if bundle.Narrative == nil || bundle.Narrative.Product != "narrative" {
|
|
t.Fatalf("Narrative = %#v, want narrative product", bundle.Narrative)
|
|
}
|
|
if bundle.Alerts == nil || bundle.Alerts.AsOf == nil {
|
|
t.Fatalf("Alerts = %#v, want alert run", bundle.Alerts)
|
|
}
|
|
if bundle.Discussion == nil || len(bundle.Discussion.KeyMessages) != 2 {
|
|
t.Fatalf("Discussion = %#v, want key messages", bundle.Discussion)
|
|
}
|
|
if bundle.Discussion.ShortTerm == nil || bundle.Discussion.ShortTerm.Text != "A weak boundary may trigger isolated showers." {
|
|
t.Fatalf("Discussion.ShortTerm = %#v, want short-term AFD text", bundle.Discussion.ShortTerm)
|
|
}
|
|
if bundle.Discussion.LongTerm == nil || bundle.Discussion.LongTerm.Text != "Warmer temperatures and periodic rain chances continue into the weekend." {
|
|
t.Fatalf("Discussion.LongTerm = %#v, want long-term AFD text", bundle.Discussion.LongTerm)
|
|
}
|
|
if bundle.WeatherStory == nil || bundle.WeatherStory.Title != "Several Chances for Rain Through Monday" {
|
|
t.Fatalf("WeatherStory = %#v, want latest weather story", bundle.WeatherStory)
|
|
}
|
|
if bundle.WeatherStory.UpdatedAt == nil {
|
|
t.Fatalf("WeatherStory.UpdatedAt = nil, want update timestamp")
|
|
}
|
|
if bundle.SPCConvectiveOutlooks == nil || len(bundle.SPCConvectiveOutlooks.Outlooks) != 1 {
|
|
t.Fatalf("SPCConvectiveOutlooks = %#v, want one outlook", bundle.SPCConvectiveOutlooks)
|
|
}
|
|
if len(bundle.SPCConvectiveOutlooks.Outlooks[0].Geometry) == 0 {
|
|
t.Fatalf("SPCConvectiveOutlooks.Outlooks[0].Geometry is empty, want GeoJSON")
|
|
}
|
|
if len(bundle.SPCConvectiveOutlooks.Discussions) != 1 || bundle.SPCConvectiveOutlooks.Discussions[0].Headline != "Severe storms possible" {
|
|
t.Fatalf("SPCConvectiveOutlooks.Discussions = %#v, want one discussion", bundle.SPCConvectiveOutlooks.Discussions)
|
|
}
|
|
if len(bundle.Sources) != 8 {
|
|
t.Fatalf("Sources length = %d, want 8", len(bundle.Sources))
|
|
}
|
|
if len(bundle.Warnings) != 0 {
|
|
t.Fatalf("Warnings length = %d, want no warnings", len(bundle.Warnings))
|
|
}
|
|
wantPaths := []string{
|
|
"/observations",
|
|
"/conditions/current",
|
|
"/forecast/hourly",
|
|
"/forecast/narrative",
|
|
"/alerts/active",
|
|
"/discussion",
|
|
"/weatherstories/latest",
|
|
convectiveOutlooksEndpoint,
|
|
}
|
|
if len(requested) != len(wantPaths) {
|
|
t.Fatalf("requested paths = %v, want %d source endpoints", requested, len(wantPaths))
|
|
}
|
|
for _, want := range wantPaths {
|
|
if !containsPath(requested, want) {
|
|
t.Fatalf("requested paths = %v, want %s", requested, want)
|
|
}
|
|
}
|
|
if !containsPath(requested, "/forecast/hourly") || containsPath(requested, "/forecast/hourly/today") {
|
|
t.Fatalf("requested paths = %v, want full hourly endpoint only", requested)
|
|
}
|
|
if !containsPath(requested, "/forecast/narrative") || containsPath(requested, "/forecast/narrative/today") {
|
|
t.Fatalf("requested paths = %v, want full narrative endpoint only", requested)
|
|
}
|
|
if !containsPath(requested, "/weatherstories/latest") {
|
|
t.Fatalf("requested paths = %v, want weather story endpoint", requested)
|
|
}
|
|
if !containsPath(requested, convectiveOutlooksEndpoint) {
|
|
t.Fatalf("requested paths = %v, want convective outlook endpoint", requested)
|
|
}
|
|
}
|
|
|
|
func TestFetchBundleBuildsExpectedQueries(t *testing.T) {
|
|
var requested []string
|
|
server := fixtureServer(t, nil, &requested)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
_, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
|
|
for _, rawURL := range requested {
|
|
if !strings.Contains(rawURL, "format=json") {
|
|
t.Fatalf("request %q missing format=json", rawURL)
|
|
}
|
|
if strings.HasPrefix(rawURL, "/weatherstories/") {
|
|
if strings.Contains(rawURL, "units=") || strings.Contains(rawURL, "precision=") || strings.Contains(rawURL, "tz=") {
|
|
t.Fatalf("weather story request %q should use format only", rawURL)
|
|
}
|
|
continue
|
|
}
|
|
if strings.HasPrefix(rawURL, convectiveOutlooksEndpoint) {
|
|
if strings.Contains(rawURL, "units=") || strings.Contains(rawURL, "precision=") || !strings.Contains(rawURL, "tz=America%2FChicago") {
|
|
t.Fatalf("convective outlook request %q should use format and tz only", rawURL)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.Contains(rawURL, "units=us") {
|
|
t.Fatalf("request %q missing units=us", rawURL)
|
|
}
|
|
if strings.HasPrefix(rawURL, "/forecast/") {
|
|
if !strings.Contains(rawURL, "precision=1") || !strings.Contains(rawURL, "tz=America%2FChicago") {
|
|
t.Fatalf("forecast request %q missing precision or tz", rawURL)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFetchBundleRecordsSourceHash(t *testing.T) {
|
|
server := fixtureServer(t, nil, nil)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
|
|
observation := sourceByName(t, bundle.Sources, "observations")
|
|
want := hashFixtureData(t, "observations.json")
|
|
if observation.DataSHA256 != want {
|
|
t.Fatalf("DataSHA256 = %q, want %q", observation.DataSHA256, want)
|
|
}
|
|
story := sourceByName(t, bundle.Sources, "weather_story")
|
|
if story.Endpoint != "/weatherstories/latest" {
|
|
t.Fatalf("weather story endpoint = %q, want /weatherstories/latest", story.Endpoint)
|
|
}
|
|
if story.DataSHA256 != hashFixtureData(t, "weather_story.json") {
|
|
t.Fatalf("weather story DataSHA256 = %q, want fixture hash", story.DataSHA256)
|
|
}
|
|
if story.IssuedAt == nil || story.UpdatedAt == nil {
|
|
t.Fatalf("weather story source timestamps = issued %#v updated %#v, want both", story.IssuedAt, story.UpdatedAt)
|
|
}
|
|
outlooks := sourceByName(t, bundle.Sources, sourceSPCConvectiveOutlooks)
|
|
if outlooks.Endpoint != convectiveOutlooksEndpoint {
|
|
t.Fatalf("convective outlook endpoint = %q, want %s", outlooks.Endpoint, convectiveOutlooksEndpoint)
|
|
}
|
|
if outlooks.Query["format"] != "json" || outlooks.Query["tz"] != "America/Chicago" || outlooks.Query["units"] != "" || outlooks.Query["precision"] != "" {
|
|
t.Fatalf("convective outlook query = %#v, want format and tz only", outlooks.Query)
|
|
}
|
|
if outlooks.DataSHA256 != hashFixtureData(t, "convective_outlooks.json") {
|
|
t.Fatalf("convective outlook DataSHA256 = %q, want fixture hash", outlooks.DataSHA256)
|
|
}
|
|
if outlooks.Missing {
|
|
t.Fatal("convective outlook source Missing = true, want false")
|
|
}
|
|
if outlooks.IssuedAt == nil || outlooks.IssuedAt.Format(time.RFC3339) != "2026-05-29T15:45:00Z" {
|
|
t.Fatalf("convective outlook IssuedAt = %#v, want run issuedAt", outlooks.IssuedAt)
|
|
}
|
|
if outlooks.UpdatedAt == nil || outlooks.UpdatedAt.Format(time.RFC3339) != "2026-05-29T16:05:00Z" {
|
|
t.Fatalf("convective outlook UpdatedAt = %#v, want run updatedAt", outlooks.UpdatedAt)
|
|
}
|
|
}
|
|
|
|
func TestHTTPErrorIsActionable(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/conditions/current": {status: http.StatusBadGateway, body: `upstream failed`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
_, err := client.FetchBundle(context.Background())
|
|
if err == nil {
|
|
t.Fatal("FetchBundle() error = nil, want HTTP error")
|
|
}
|
|
if !strings.Contains(err.Error(), "/conditions/current") || !strings.Contains(err.Error(), "502") {
|
|
t.Fatalf("error = %q, want endpoint and status", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRequiredHourlyForecast(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/forecast/hourly": {status: http.StatusOK, body: `{"data": null}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
_, err := client.FetchBundle(context.Background())
|
|
if err == nil {
|
|
t.Fatal("FetchBundle() error = nil, want required hourly error")
|
|
}
|
|
if !strings.Contains(err.Error(), "hourly forecast data") {
|
|
t.Fatalf("error = %q, want hourly context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestNullAlertsMeansNoActiveAlerts(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/alerts/active": {status: http.StatusOK, body: `{"data": null}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
if bundle.Alerts == nil {
|
|
t.Fatal("Alerts = nil, want checked empty alert run")
|
|
}
|
|
if len(bundle.Alerts.Alerts) != 0 {
|
|
t.Fatalf("Alerts length = %d, want no active alerts", len(bundle.Alerts.Alerts))
|
|
}
|
|
source := sourceByName(t, bundle.Sources, "alerts")
|
|
if source.Missing {
|
|
t.Fatalf("alerts source Missing = true, want false")
|
|
}
|
|
if source.DataSHA256 == "" {
|
|
t.Fatal("alerts DataSHA256 is empty, want hash for explicit null payload")
|
|
}
|
|
for _, warning := range bundle.Warnings {
|
|
if warning.Source == "alerts" {
|
|
t.Fatalf("warnings = %#v, want no alerts warning", bundle.Warnings)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMissingSPCConvectiveOutlooksUsesPolicy(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
convectiveOutlooksEndpoint: {status: http.StatusOK, body: `{"data": null}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
|
|
sourceSPCConvectiveOutlooks: config.MissingSourceWarn,
|
|
})
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
if bundle.SPCConvectiveOutlooks != nil {
|
|
t.Fatalf("SPCConvectiveOutlooks = %#v, want nil for missing source", bundle.SPCConvectiveOutlooks)
|
|
}
|
|
source := sourceByName(t, bundle.Sources, sourceSPCConvectiveOutlooks)
|
|
if !source.Missing || len(source.Warnings) != 1 {
|
|
t.Fatalf("convective outlook source = %#v, want missing source warning", source)
|
|
}
|
|
}
|
|
|
|
func TestEmptySPCConvectiveOutlooksAreCheckedData(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
convectiveOutlooksEndpoint: {status: http.StatusOK, body: `{"data":{"asOf":"2026-05-29T16:00:00Z","outlooks":[],"discussions":[]}}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
|
|
sourceSPCConvectiveOutlooks: config.MissingSourceWarn,
|
|
})
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
if bundle.SPCConvectiveOutlooks == nil {
|
|
t.Fatal("SPCConvectiveOutlooks = nil, want checked empty run")
|
|
}
|
|
if len(bundle.SPCConvectiveOutlooks.Outlooks) != 0 || len(bundle.SPCConvectiveOutlooks.Discussions) != 0 {
|
|
t.Fatalf("SPCConvectiveOutlooks = %#v, want empty arrays", bundle.SPCConvectiveOutlooks)
|
|
}
|
|
source := sourceByName(t, bundle.Sources, sourceSPCConvectiveOutlooks)
|
|
if source.Missing || len(source.Warnings) != 0 {
|
|
t.Fatalf("convective outlook source = %#v, want non-missing source without warnings", source)
|
|
}
|
|
if source.IssuedAt == nil || source.IssuedAt.Format(time.RFC3339) != "2026-05-29T16:00:00Z" {
|
|
t.Fatalf("convective outlook IssuedAt = %#v, want fallback to asOf", source.IssuedAt)
|
|
}
|
|
for _, warning := range bundle.Warnings {
|
|
if warning.Source == sourceSPCConvectiveOutlooks {
|
|
t.Fatalf("warnings = %#v, want no convective outlook warning", bundle.Warnings)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMissingSourcePolicyWarnNoneError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy config.MissingSourcePolicy
|
|
wantErr bool
|
|
wantWarns int
|
|
wantSource bool
|
|
}{
|
|
{name: "warn", policy: config.MissingSourceWarn, wantWarns: 1, wantSource: true},
|
|
{name: "none", policy: config.MissingSourceNone, wantWarns: 0, wantSource: true},
|
|
{name: "error", policy: config.MissingSourceError, wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/observations": {status: http.StatusOK, body: `{"data": null}`},
|
|
}, nil)
|
|
cfg := testConfig(server.URL + "/")
|
|
cfg.MissingSource.Default = tt.policy
|
|
cfg.MissingSource.Sources = map[string]config.MissingSourcePolicy{
|
|
"hourly": tt.policy,
|
|
}
|
|
client, err := New(cfg, WithClock(fixedNow))
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatal("FetchBundle() error = nil, want policy error")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
if len(bundle.Warnings) != tt.wantWarns {
|
|
t.Fatalf("Warnings length = %d, want %d", len(bundle.Warnings), tt.wantWarns)
|
|
}
|
|
if tt.wantSource {
|
|
source := sourceByName(t, bundle.Sources, "observations")
|
|
if !source.Missing {
|
|
t.Fatalf("observations source Missing = false, want true")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMalformedNonRequiredSourceUsesPolicy(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/conditions/current": {status: http.StatusOK, body: `{"data": {"temperatureF": "hot"}}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
|
|
"current": config.MissingSourceWarn,
|
|
})
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
source := sourceByName(t, bundle.Sources, "current")
|
|
if !source.Missing || len(source.Warnings) != 1 {
|
|
t.Fatalf("current source = %#v, want missing source warning", source)
|
|
}
|
|
}
|
|
|
|
func TestMissingWeatherStoryUsesPolicy(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/weatherstories/latest": {status: http.StatusOK, body: `{"data": null}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
|
|
"weather_story": config.MissingSourceWarn,
|
|
})
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
if bundle.WeatherStory != nil {
|
|
t.Fatalf("WeatherStory = %#v, want nil for missing source", bundle.WeatherStory)
|
|
}
|
|
source := sourceByName(t, bundle.Sources, "weather_story")
|
|
if !source.Missing || len(source.Warnings) != 1 {
|
|
t.Fatalf("weather_story source = %#v, want missing source warning", source)
|
|
}
|
|
}
|
|
|
|
func TestMalformedWeatherStoryUsesPolicy(t *testing.T) {
|
|
server := fixtureServer(t, map[string]handlerOverride{
|
|
"/weatherstories/latest": {status: http.StatusOK, body: `{"data": {"startTime": 123}}`},
|
|
}, nil)
|
|
client := newTestClient(t, server.URL+"/", map[string]config.MissingSourcePolicy{
|
|
"weather_story": config.MissingSourceWarn,
|
|
})
|
|
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
source := sourceByName(t, bundle.Sources, "weather_story")
|
|
if !source.Missing || len(source.Warnings) != 1 || source.Warnings[0].Code != "malformed_source" {
|
|
t.Fatalf("weather_story source = %#v, want malformed source warning", source)
|
|
}
|
|
}
|
|
|
|
func TestContextCancellation(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
<-r.Context().Done()
|
|
}))
|
|
defer server.Close()
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
_, err := client.FetchBundle(ctx)
|
|
if err == nil {
|
|
t.Fatal("FetchBundle() error = nil, want cancellation error")
|
|
}
|
|
}
|
|
|
|
func TestHTTPTimeout(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(50 * time.Millisecond)
|
|
}))
|
|
defer server.Close()
|
|
|
|
cfg := testConfig(server.URL + "/")
|
|
cfg.WeatherAPI.Timeout = time.Nanosecond
|
|
client, err := New(cfg, WithClock(fixedNow))
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
|
|
_, err = client.FetchBundle(context.Background())
|
|
if err == nil {
|
|
t.Fatal("FetchBundle() error = nil, want timeout error")
|
|
}
|
|
if !strings.Contains(err.Error(), "/observations") {
|
|
t.Fatalf("error = %q, want endpoint context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestSaveBundle(t *testing.T) {
|
|
server := fixtureServer(t, nil, nil)
|
|
client := newTestClient(t, server.URL+"/", nil)
|
|
bundle, err := client.FetchBundle(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("FetchBundle() error = %v", err)
|
|
}
|
|
|
|
path := filepath.Join(t.TempDir(), "nested", "bundle.json")
|
|
if err := SaveBundle(path, bundle); err != nil {
|
|
t.Fatalf("SaveBundle() error = %v", err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read saved bundle: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `"hourly"`) {
|
|
t.Fatalf("saved bundle missing hourly source:\n%s", string(data))
|
|
}
|
|
}
|
|
|
|
type handlerOverride struct {
|
|
status int
|
|
body string
|
|
}
|
|
|
|
func fixtureServer(t *testing.T, overrides map[string]handlerOverride, requested *[]string) *httptest.Server {
|
|
t.Helper()
|
|
fixtures := map[string]string{
|
|
"/observations": "observations.json",
|
|
"/conditions/current": "current.json",
|
|
"/forecast/hourly": "hourly.json",
|
|
"/forecast/narrative": "narrative.json",
|
|
"/alerts/active": "alerts.json",
|
|
"/discussion": "discussion.json",
|
|
"/weatherstories/latest": "weather_story.json",
|
|
convectiveOutlooksEndpoint: "convective_outlooks.json",
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if requested != nil {
|
|
*requested = append(*requested, r.URL.String())
|
|
}
|
|
if override, ok := overrides[r.URL.Path]; ok {
|
|
w.WriteHeader(override.status)
|
|
_, _ = w.Write([]byte(override.body))
|
|
return
|
|
}
|
|
name, ok := fixtures[r.URL.Path]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
http.ServeFile(w, r, filepath.Join("testdata", name))
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
return server
|
|
}
|
|
|
|
func newTestClient(t *testing.T, baseURL string, sourcePolicies map[string]config.MissingSourcePolicy) *Client {
|
|
t.Helper()
|
|
cfg := testConfig(baseURL)
|
|
for source, policy := range sourcePolicies {
|
|
cfg.MissingSource.Sources[source] = policy
|
|
}
|
|
client, err := New(cfg, WithClock(fixedNow))
|
|
if err != nil {
|
|
t.Fatalf("New() error = %v", err)
|
|
}
|
|
return client
|
|
}
|
|
|
|
func testConfig(baseURL string) config.Config {
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.BaseURL = baseURL
|
|
return cfg
|
|
}
|
|
|
|
func fixedNow() time.Time {
|
|
return time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
|
}
|
|
|
|
func containsPath(requested []string, path string) bool {
|
|
for _, rawURL := range requested {
|
|
if strings.HasPrefix(rawURL, path+"?") || rawURL == path {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sourceByName(t *testing.T, sources []weatherdata.Source, name string) weatherdata.Source {
|
|
t.Helper()
|
|
for _, source := range sources {
|
|
if source.Name == name {
|
|
return source
|
|
}
|
|
}
|
|
t.Fatalf("source %q not found in %#v", name, sources)
|
|
return weatherdata.Source{}
|
|
}
|
|
|
|
func hashFixtureData(t *testing.T, fixture string) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(filepath.Join("testdata", fixture))
|
|
if err != nil {
|
|
t.Fatalf("read fixture: %v", err)
|
|
}
|
|
var env envelope
|
|
if err := json.Unmarshal(data, &env); err != nil {
|
|
t.Fatalf("decode fixture envelope: %v", err)
|
|
}
|
|
hash, err := sourceHash(env.Data)
|
|
if err != nil {
|
|
t.Fatalf("hash fixture data: %v", err)
|
|
}
|
|
return hash
|
|
}
|