package collect import ( "context" "net/http" "net/http/httptest" "strings" "testing" "gitea.maximumdirect.net/eric/weatherreporter/internal/config" ) func TestRunFetchesBundle(t *testing.T) { server := collectionTestServer(t, nil) defer server.Close() cfg := config.Defaults() cfg.WeatherAPI.BaseURL = server.URL + "/" result, err := Run(context.Background(), Request{Config: cfg}) if err != nil { t.Fatalf("Run() error = %v", err) } if result == nil || result.Bundle == nil { t.Fatal("Run() result bundle = nil, want fetched bundle") } if result.Bundle.Hourly == nil { t.Fatal("Hourly = nil, want fetched hourly forecast") } if result.Bundle.WeatherStory == nil || result.Bundle.WeatherStory.Title != "Several Chances for Rain Through Monday" { t.Fatalf("WeatherStory = %#v, want fetched weather story", result.Bundle.WeatherStory) } } func TestRunWrapsAdapterConstructionError(t *testing.T) { cfg := config.Defaults() cfg.WeatherAPI.BaseURL = "" cfg.Secrets.Directory = "super-secret-directory" _, err := Run(context.Background(), Request{Config: cfg}) if err == nil { t.Fatal("Run() error = nil, want adapter construction error") } if !strings.Contains(err.Error(), "prepare weather collection") { t.Fatalf("error = %q, want collection setup context", err.Error()) } if strings.Contains(err.Error(), cfg.Secrets.Directory) { t.Fatalf("error = %q, want no secret path leakage", err.Error()) } } func TestRunWrapsFetchError(t *testing.T) { server := collectionTestServer(t, map[string]int{"/observations": http.StatusBadGateway}) defer server.Close() cfg := config.Defaults() cfg.WeatherAPI.BaseURL = server.URL + "/" _, err := Run(context.Background(), Request{Config: cfg}) if err == nil { t.Fatal("Run() error = nil, want fetch error") } if !strings.Contains(err.Error(), "collect weather bundle") { t.Fatalf("error = %q, want collection fetch context", err.Error()) } if !strings.Contains(err.Error(), "/observations") { t.Fatalf("error = %q, want source endpoint context", err.Error()) } } func collectionTestServer(t *testing.T, statusByPath map[string]int) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if status := statusByPath[r.URL.Path]; status != 0 { http.Error(w, "upstream failure", status) return } switch r.URL.Path { case "/observations": _, _ = w.Write([]byte(`{"data":{"timestamp":"2026-05-29T14:00:00Z","conditionCode":3}}`)) case "/conditions/current": _, _ = w.Write([]byte(`{"data":{"conditionText":"Clear"}}`)) case "/forecast/hourly": _, _ = w.Write([]byte(`{"data":{"issuedAt":"2026-05-29T10:30:00-05:00","product":"hourly","periods":[{"startTime":"2026-05-29T13:00:00-05:00","endTime":"2026-05-29T14:00:00-05:00"}]}}`)) case "/forecast/narrative": _, _ = w.Write([]byte(`{"data":{"issuedAt":"2026-05-29T10:30:00-05:00","product":"narrative","periods":[]}}`)) case "/alerts/active": _, _ = w.Write([]byte(`{"data":{"alerts":[]}}`)) case "/discussion": _, _ = w.Write([]byte(`{"data":{"product":"discussion","issuedAt":"2026-05-29T09:25:00-05:00","keyMessages":[],"shortTerm":{"qualifier":"(Short Term)","text":"Short-term AFD narrative for saved bundle."},"longTerm":{"qualifier":"(Long Term)","text":"Long-term AFD narrative for saved bundle."}}}`)) case "/weatherstories/latest": _, _ = w.Write([]byte(`{"data":{"officeId":"LSX","startTime":"2026-05-30T08:46:00Z","endTime":"2026-05-31T11:00:00Z","updatedAt":"2026-05-30T09:00:34Z","title":"Several Chances for Rain Through Monday","description":"Scattered showers and thunderstorms remain possible.","altText":"Forecast weather story graphic.","priority":false,"order":1,"downloadUrl":"https://api.weather.gov/offices/LSX/weatherstories/download/test"}}`)) case "/outlooks/convective": _, _ = w.Write([]byte(`{"data":{"asOf":"2026-05-29T16:00:00Z","outlooks":[],"discussions":[]}}`)) default: http.NotFound(w, r) } })) }