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:23:49 +00:00
func TestRunGenerateDailyWritesBriefing ( t * testing . T ) {
server := dailyServer ( t )
configPath := filepath . Join ( t . TempDir ( ) , "config.yml" )
if err := os . WriteFile ( configPath , [ ] byte ( "weather_api:\n base_url: " + server . URL + "/\n timezone: America/Chicago\n" ) , 0 o600 ) ; err != nil {
t . Fatalf ( "write config: %v" , err )
}
outPath := filepath . Join ( t . TempDir ( ) , "daily.briefing.json" )
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 {
t . Fatalf ( "read briefing: %v" , err )
}
if ! strings . Contains ( string ( data ) , ` "schemaVersion" ` ) || ! strings . Contains ( string ( data ) , ` "daily_today" ` ) {
t . Fatalf ( "briefing output missing expected content:\n%s" , string ( data ) )
}
}
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
}