2026-05-29 16:52:02 +00:00
package cli
import (
"bytes"
"context"
2026-05-29 17:23:49 +00:00
"net/http"
"net/http/httptest"
"os"
"path/filepath"
2026-05-29 16:52:02 +00:00
"strings"
"testing"
2026-05-29 16:58:18 +00:00
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
2026-05-29 16:52:02 +00:00
)
func TestRunHelpLongFlag ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
err := Run ( context . Background ( ) , [ ] string { "--help" } , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-05-29 16:58:18 +00:00
if ! strings . Contains ( stdout . String ( ) , "generate daily" ) {
t . Fatalf ( "help output missing generate command:\n%s" , stdout . String ( ) )
2026-05-29 16:52:02 +00:00
}
}
func TestRunHelpShortFlag ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
err := Run ( context . Background ( ) , [ ] string { "-h" } , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
2026-05-29 16:58:18 +00:00
if ! strings . Contains ( stdout . String ( ) , "weatherreporter run evening" ) {
t . Fatalf ( "help output missing run command:\n%s" , stdout . String ( ) )
}
}
func TestRunUnknownCommand ( t * testing . T ) {
var stdout bytes . Buffer
var stderr bytes . Buffer
err := Run ( context . Background ( ) , [ ] string { "inspect" } , & stdout , & stderr )
if err == nil {
t . Fatal ( "Run() error = nil, want unknown command error" )
}
if ! strings . Contains ( err . Error ( ) , ` unknown command "inspect" ` ) {
t . Fatalf ( "Run() error = %q, want unknown command message" , err . Error ( ) )
2026-05-29 16:52:02 +00:00
}
}
2026-05-29 16:58:18 +00:00
func TestRunGenerateReturnsNotImplementedAfterResolution ( t * testing . T ) {
2026-05-29 16:52:02 +00:00
var stdout bytes . Buffer
var stderr bytes . Buffer
2026-05-29 16:58:18 +00:00
runner := Runner { Clock : fixedClock ( ) }
err := runner . Run ( context . Background ( ) , [ ] string { "generate" , "tomorrow" , "--units" , "metric" } , & stdout , & stderr )
if err == nil {
t . Fatal ( "Run() error = nil, want app not implemented error" )
}
if ! strings . Contains ( err . Error ( ) , "generate is not implemented" ) {
t . Fatalf ( "Run() error = %q, want app not implemented error" , err . Error ( ) )
}
}
2026-05-29 17:32:57 +00:00
func TestRunGenerateDailyWritesDataPackageAndRunsPreflight ( t * testing . T ) {
2026-05-29 17:23:49 +00:00
server := dailyServer ( t )
2026-05-29 17:32:57 +00:00
tempDir := t . TempDir ( )
scriptoriumPath := writeFakeScriptorium ( t , tempDir )
configPath := filepath . Join ( tempDir , "config.yml" )
workspaceRoot := filepath . Join ( tempDir , "workspace" )
configBody := "weather_api:\n base_url: " + server . URL + "/\n timezone: America/Chicago\nscriptorium:\n binary: " + scriptoriumPath + "\nworkspace:\n root: " + workspaceRoot + "\n"
if err := os . WriteFile ( configPath , [ ] byte ( configBody ) , 0 o600 ) ; err != nil {
2026-05-29 17:23:49 +00:00
t . Fatalf ( "write config: %v" , err )
}
2026-05-29 17:32:57 +00:00
outPath := filepath . Join ( tempDir , "daily.data_package.json" )
2026-05-29 17:23:49 +00:00
var stdout bytes . Buffer
var stderr bytes . Buffer
runner := Runner { Clock : fixedClock ( ) }
err := runner . Run ( context . Background ( ) , [ ] string {
"generate" , "daily" ,
"--config" , configPath ,
"--date" , "2026-05-29" ,
"--out" , outPath ,
} , & stdout , & stderr )
if err != nil {
t . Fatalf ( "Run() error = %v" , err )
}
data , err := os . ReadFile ( outPath )
if err != nil {
2026-05-29 17:32:57 +00:00
t . Fatalf ( "read data package: %v" , err )
2026-05-29 17:23:49 +00:00
}
2026-05-29 17:32:57 +00:00
if ! strings . Contains ( string ( data ) , ` data_package.v1 ` ) || ! strings . Contains ( string ( data ) , ` "daily_today" ` ) {
t . Fatalf ( "data package output missing expected content:\n%s" , string ( data ) )
}
preflightMatches , err := filepath . Glob ( filepath . Join ( workspaceRoot , "preflight" , "daily" , "2026-05-29" , "*.render.json" ) )
if err != nil {
t . Fatalf ( "glob preflight: %v" , err )
}
if len ( preflightMatches ) != 1 {
t . Fatalf ( "preflight files = %#v, want one render output" , preflightMatches )
}
preflight , err := os . ReadFile ( preflightMatches [ 0 ] )
if err != nil {
t . Fatalf ( "read preflight: %v" , err )
}
if ! strings . Contains ( string ( preflight ) , ` ok ` ) {
t . Fatalf ( "preflight missing fake render output:\n%s" , string ( preflight ) )
2026-05-29 17:23:49 +00:00
}
}
2026-05-29 16:58:18 +00:00
func TestResolveGenerateCommands ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
tests := [ ] struct {
name string
args [ ] string
want app . ReportKind
} {
{ name : "daily" , args : [ ] string { "daily" , "--date" , "2026-05-29" } , want : app . ReportDaily } ,
{ name : "tomorrow" , args : [ ] string { "tomorrow" } , want : app . ReportTomorrow } ,
{ name : "three-day" , args : [ ] string { "three-day" } , want : app . ReportThreeDay } ,
{ name : "weekend" , args : [ ] string { "weekend" } , want : app . ReportWeekend } ,
{ name : "storm" , args : [ ] string { "storm" , "--start" , "2026-05-29T18:00" , "--end" , "2026-05-30T06:00" } , want : app . ReportStorm } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
req , err := runner . resolveGenerate ( tt . args )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
if req . Report != tt . want {
t . Fatalf ( "Report = %q, want %q" , req . Report , tt . want )
}
} )
}
}
func TestResolveGenerateDailyDefaultsDateInConfiguredTimezone ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
req , err := runner . resolveGenerate ( [ ] string { "daily" } )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
if got := req . Date . Format ( timeutil . DateLayout ) ; got != "2026-05-29" {
t . Fatalf ( "Date = %s, want 2026-05-29" , got )
}
}
2026-05-29 16:52:02 +00:00
2026-05-29 16:58:18 +00:00
func TestResolveGenerateAppliesSharedFlags ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
req , err := runner . resolveGenerate ( [ ] string { "daily" , "--units" , "metric" , "--tz" , "UTC" , "--out" , "./daily.md" } )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
}
if req . Config . WeatherAPI . Units != "metric" {
t . Fatalf ( "Units = %q, want metric" , req . Config . WeatherAPI . Units )
}
if req . Config . WeatherAPI . Timezone != "UTC" {
t . Fatalf ( "Timezone = %q, want UTC" , req . Config . WeatherAPI . Timezone )
}
if req . OutputPath != "./daily.md" {
t . Fatalf ( "OutputPath = %q, want ./daily.md" , req . OutputPath )
}
}
func TestResolveGenerateStormRequiresStartAndEnd ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
_ , err := runner . resolveGenerate ( [ ] string { "storm" , "--start" , "2026-05-29T18:00" } )
2026-05-29 16:52:02 +00:00
if err == nil {
2026-05-29 16:58:18 +00:00
t . Fatal ( "resolveGenerate() error = nil, want missing end error" )
}
if ! strings . Contains ( err . Error ( ) , "requires --end" ) {
t . Fatalf ( "error = %q, want missing end" , err . Error ( ) )
2026-05-29 16:52:02 +00:00
}
2026-05-29 16:58:18 +00:00
}
func TestResolveGenerateStormParsesRFC3339 ( t * testing . T ) {
runner := Runner { Clock : fixedClock ( ) }
2026-05-29 16:52:02 +00:00
2026-05-29 16:58:18 +00:00
req , err := runner . resolveGenerate ( [ ] string {
"storm" ,
"--start" , "2026-05-29T18:00:00-05:00" ,
"--end" , "2026-05-30T06:00:00-05:00" ,
} )
if err != nil {
t . Fatalf ( "resolveGenerate() error = %v" , err )
2026-05-29 16:52:02 +00:00
}
2026-05-29 16:58:18 +00:00
if ! req . StormEnd . After ( req . StormStart ) {
t . Fatalf ( "StormEnd = %s, want after %s" , req . StormEnd , req . StormStart )
}
}
func TestResolveRunCommands ( t * testing . T ) {
tests := [ ] struct {
name string
args [ ] string
want app . BatchKind
} {
{ name : "morning" , args : [ ] string { "morning" } , want : app . BatchMorning } ,
{ name : "evening" , args : [ ] string { "evening" , "--tz" , "UTC" } , want : app . BatchEvening } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
req , err := resolveRun ( tt . args )
if err != nil {
t . Fatalf ( "resolveRun() error = %v" , err )
}
if req . Batch != tt . want {
t . Fatalf ( "Batch = %q, want %q" , req . Batch , tt . want )
}
} )
}
}
func TestResolveRunRejectsOutputFlag ( t * testing . T ) {
_ , err := resolveRun ( [ ] string { "morning" , "--out" , "./report.md" } )
if err == nil {
t . Fatal ( "resolveRun() error = nil, want flag error" )
}
if ! strings . Contains ( err . Error ( ) , "flag provided but not defined" ) {
t . Fatalf ( "error = %q, want undefined flag error" , err . Error ( ) )
}
}
func fixedClock ( ) timeutil . Clock {
return timeutil . FixedClock { Time : time . Date ( 2026 , 5 , 29 , 12 , 0 , 0 , 0 , time . UTC ) }
2026-05-29 16:52:02 +00:00
}
2026-05-29 17:23:49 +00:00
func dailyServer ( 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":[]}} ` ) )
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
}
2026-05-29 17:32:57 +00:00
func writeFakeScriptorium ( t * testing . T , dir string ) string {
t . Helper ( )
path := filepath . Join ( dir , "scriptorium" )
body := "#!/bin/sh\nprintf '{\"ok\":true,\"argv\":\"%s\"}' \"$*\"\n"
if err := os . WriteFile ( path , [ ] byte ( body ) , 0 o700 ) ; err != nil {
t . Fatalf ( "write fake scriptorium: %v" , err )
}
return path
}