230 lines
7.9 KiB
Go
230 lines
7.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
|
)
|
|
|
|
func TestFetchAndSaveBundle(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
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":[]}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
|
path := filepath.Join(t.TempDir(), "bundle.json")
|
|
|
|
bundle, err := FetchAndSaveBundle(context.Background(), FetchBundleRequest{Config: cfg, OutputPath: path})
|
|
if err != nil {
|
|
t.Fatalf("FetchAndSaveBundle() error = %v", err)
|
|
}
|
|
if bundle.Hourly == nil {
|
|
t.Fatal("Hourly = nil, want fetched bundle")
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read saved bundle: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `"product": "hourly"`) {
|
|
t.Fatalf("saved bundle missing hourly product:\n%s", string(data))
|
|
}
|
|
}
|
|
|
|
func TestFetchAndSaveBundleRequiresOutputPath(t *testing.T) {
|
|
_, err := FetchAndSaveBundle(context.Background(), FetchBundleRequest{Config: config.Defaults()})
|
|
if err == nil {
|
|
t.Fatal("FetchAndSaveBundle() error = nil, want output path error")
|
|
}
|
|
if !strings.Contains(err.Error(), "output path") {
|
|
t.Fatalf("error = %q, want output path context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestGenerateDailyBriefingWritesArtifact(t *testing.T) {
|
|
server := dailyBundleServer(t)
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
|
resolved, err := ResolveGenerate(GenerateRequest{
|
|
Config: cfg,
|
|
Report: ReportDaily,
|
|
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
|
}, mustParse("2026-05-29T05:00:00-05:00"))
|
|
if err != nil {
|
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "daily.briefing.json")
|
|
|
|
result, err := GenerateDailyBriefing(context.Background(), DailyBriefingRequest{
|
|
Config: cfg,
|
|
Resolved: resolved,
|
|
OutputPath: path,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GenerateDailyBriefing() error = %v", err)
|
|
}
|
|
if result.OutputPath != path {
|
|
t.Fatalf("OutputPath = %q, want %q", result.OutputPath, path)
|
|
}
|
|
if result.Package.Daily == nil {
|
|
t.Fatal("Daily = nil")
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read briefing artifact: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `"schemaVersion"`) || !strings.Contains(string(data), `"daily"`) {
|
|
t.Fatalf("briefing artifact missing expected fields:\n%s", string(data))
|
|
}
|
|
}
|
|
|
|
func TestGenerateDailyBriefingDefaultPath(t *testing.T) {
|
|
server := dailyBundleServer(t)
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
|
cfg.Workspace.Root = t.TempDir()
|
|
resolved, err := ResolveGenerate(GenerateRequest{
|
|
Config: cfg,
|
|
Report: ReportDaily,
|
|
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
|
}, mustParse("2026-05-29T05:00:00-05:00"))
|
|
if err != nil {
|
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
|
}
|
|
|
|
result, err := GenerateDailyBriefing(context.Background(), DailyBriefingRequest{
|
|
Config: cfg,
|
|
Resolved: resolved,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GenerateDailyBriefing() error = %v", err)
|
|
}
|
|
if !strings.HasSuffix(result.OutputPath, filepath.Join("snapshots", "daily", "2026-05-29", "2026-05-29.daily_today.briefing.json")) {
|
|
t.Fatalf("OutputPath = %q, want deterministic daily briefing path", result.OutputPath)
|
|
}
|
|
}
|
|
|
|
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
|
now := mustParse("2026-05-29T18:00:00-05:00")
|
|
|
|
resolved, err := ResolveGenerate(GenerateRequest{
|
|
Config: cfg,
|
|
Report: ReportTomorrow,
|
|
}, now)
|
|
if err != nil {
|
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
|
}
|
|
if resolved.Definition.ID != report.DailyTomorrow {
|
|
t.Fatalf("ID = %q, want daily_tomorrow", resolved.Definition.ID)
|
|
}
|
|
if resolved.Definition.PromptID != "weather.daily_report" {
|
|
t.Fatalf("PromptID = %q, want weather.daily_report", resolved.Definition.PromptID)
|
|
}
|
|
if got := resolved.ValidPeriod.Start.Format("2006-01-02"); got != "2026-05-30" {
|
|
t.Fatalf("valid start date = %s, want 2026-05-30", got)
|
|
}
|
|
}
|
|
|
|
func dailyBundleServer(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
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":{"locationId":"test-grid","locationName":"Testville","issuedAt":"2026-05-29T10:30:00-05:00","product":"hourly","periods":[{"startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T07:00:00-05:00","textDescription":"Showers and thunderstorms","temperatureF":66,"probabilityOfPrecipitationPercent":80,"windGustMph":32}]}}`))
|
|
case "/forecast/narrative":
|
|
_, _ = w.Write([]byte(`{"data":{"issuedAt":"2026-05-29T10:30:00-05:00","product":"narrative","periods":[{"startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T18:00:00-05:00","textDescription":"Morning storms, then partly sunny."}]}}`))
|
|
case "/alerts/active":
|
|
_, _ = w.Write([]byte(`{"data":{"alerts":[{"event":"Flood Watch","effective":"2026-05-29T05:00:00-05:00","expires":"2026-05-29T09:00:00-05:00"}]}}`))
|
|
case "/discussion":
|
|
_, _ = w.Write([]byte(`{"data":{"product":"discussion","issuedAt":"2026-05-29T09:25:00-05:00","keyMessages":["Storms are most likely during the morning."]}}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
return server
|
|
}
|
|
|
|
func TestResolveGenerateStorm(t *testing.T) {
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
|
now := mustParse("2026-05-29T12:00:00-05:00")
|
|
start := mustParse("2026-05-29T18:00:00-05:00")
|
|
end := mustParse("2026-05-30T06:00:00-05:00")
|
|
|
|
resolved, err := ResolveGenerate(GenerateRequest{
|
|
Config: cfg,
|
|
Report: ReportStorm,
|
|
StormStart: start,
|
|
StormEnd: end,
|
|
}, now)
|
|
if err != nil {
|
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
|
}
|
|
if resolved.Definition.ID != report.Storm {
|
|
t.Fatalf("ID = %q, want storm", resolved.Definition.ID)
|
|
}
|
|
if !resolved.ValidPeriod.Start.Equal(start) || !resolved.ValidPeriod.End.Equal(end) {
|
|
t.Fatalf("period = %#v, want storm window", resolved.ValidPeriod)
|
|
}
|
|
}
|
|
|
|
func TestResolveBatchMorningSkipsWeekendOnSunday(t *testing.T) {
|
|
cfg := config.Defaults()
|
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
|
now := mustParse("2026-05-31T06:00:00-05:00")
|
|
|
|
resolved, err := ResolveBatch(BatchRequest{Config: cfg, Batch: BatchMorning}, now)
|
|
if err != nil {
|
|
t.Fatalf("ResolveBatch() error = %v", err)
|
|
}
|
|
if len(resolved) != 2 {
|
|
t.Fatalf("resolved length = %d, want 2", len(resolved))
|
|
}
|
|
for _, item := range resolved {
|
|
if item.Definition.ID == report.Weekend {
|
|
t.Fatal("morning batch included weekend on Sunday")
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustParse(value string) time.Time {
|
|
parsed, err := time.Parse(time.RFC3339, value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return parsed
|
|
}
|