package main import ( "context" "fmt" "testing" "os" "time" "github.com/go-redis/redis/v8" "trip-planner/internal/cache" "trip-planner/internal/metrics" "trip-planner/internal/yandex" ) func newMockMonitor(id string, tripCount int, scheduleFunc func(context.Context, string) (int, error)) *StationMonitor { // Read Redis address from environment, default to localhost:6379 redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rc := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: "", DB: 0, }) yc := yandex.NewClient("test-key") monitor := &StationMonitor{ ID: id, Yandex: yc, Cache: cache.NewCacheStore(rc, metrics.New()), ScheduleFunc: scheduleFunc, } // If no ScheduleFunc provided, set up default that returns tripCount if monitor.ScheduleFunc == nil { monitor.ScheduleFunc = func(ctx context.Context, stationID string) (int, error) { return tripCount, nil } } return monitor } const testMonitorID = "test-station" // TestProcessStation_WithTrips verifies that a station with trips today // gets status "active" and zero-trip day count resets to 0. func TestProcessStation_WithTrips(t *testing.T) { t.Helper() // Read Redis address from environment, default to localhost:6379 redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rc := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: "", DB: 0, }) defer rc.Close() ctx := context.Background() // Flush Redis database for test isolation rc.FlushDB(ctx) monitor := newMockMonitor(testMonitorID, 2, nil) // Process the station - should have trips and status should be active err := ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error: %v", err) } // Check that status was set to active statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID)) if err != nil { t.Fatalf("cache get error: %v", err) } if string(statusData) != string(StatusActive) { t.Errorf("expected status active, got %s", string(statusData)) } // Check that zero days was reset to 0 zeroDaysData, err := monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) if err != nil { t.Fatalf("cache get zero days error: %v", err) } var zeroDays int _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays != 0 { t.Errorf("expected zero days 0, got %d", zeroDays) } } // TestProcessStation_ZeroTrips_IncrementsCount verifies that a station // with 0 trips increments the zero-trip day count. func TestProcessStation_ZeroTrips_IncrementsCount(t *testing.T) { t.Helper() // Read Redis address from environment, default to localhost:6379 redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rc := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: "", DB: 0, }) defer rc.Close() ctx := context.Background() // Flush Redis database for test isolation rc.FlushDB(ctx) // Monitor with 0 trips (schedule func returns 0) monitor := newMockMonitor(testMonitorID, 0, nil) // First call: 0 trips, status should remain active (zero days = 1) err := ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error: %v", err) } // Check that zero days was incremented to 1 statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID)) if err != nil { t.Fatalf("cache get error: %v", err) } if string(statusData) != string(StatusActive) { t.Errorf("expected status active after first call, got %s", string(statusData)) } zeroDaysData, err := monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) if err != nil { t.Fatalf("cache get zero days error: %v", err) } var zeroDays int _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays != 1 { t.Errorf("expected zero days 1 after first call, got %d", zeroDays) } } // TestProcessStation_ZeroTrips_3Days_Closes verifies that a station // with 3 consecutive days of zero trips gets status "closed". func TestProcessStation_ZeroTrips_3Days_Closes(t *testing.T) { t.Helper() // Read Redis address from environment, default to localhost:6379 redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rc := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: "", DB: 0, }) defer rc.Close() ctx := context.Background() // Flush Redis database for test isolation rc.FlushDB(ctx) // Monitor with 0 trips each day monitor := newMockMonitor(testMonitorID, 0, nil) // Day 1: 0 trips - ProcessStation reads 0 (no prior data), increments to 1 err := ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error day 1: %v", err) } zeroDaysData, _ := monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) var zeroDays int fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays) // ProcessStation starts at 0 (no prior data), increments to 1 if zeroDays != 1 { t.Errorf("day 1: expected zero days 1, got %d", zeroDays) } // Day 2: 0 trips - ProcessStation reads 1 (from day 1), increments to 2 err = ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error day 2: %v", err) } zeroDaysData, _ = monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays) // ProcessStation incremented from 1 to 2 if zeroDays != 2 { t.Errorf("day 2: expected zero days 2, got %d", zeroDays) } // Day 3: 0 trips - ProcessStation reads 2 (from day 2), increments to 3, closes station err = ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error day 3: %v", err) } zeroDaysData, _ = monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays) // ProcessStation incremented from 2 to 3 if zeroDays != 3 { t.Errorf("day 3: expected zero days 3, got %d", zeroDays) } // Status should be closed after 3 consecutive days of zero trips statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID)) if err != nil { t.Fatalf("cache get error: %v", err) } if string(statusData) != string(StatusClosed) { t.Errorf("expected status closed after 3 days, got %s", string(statusData)) } } // TestProcessStation_Reactivation_AfterClosure verifies that a station // closed due to 3 zero-trip days gets reactivated when trips resume. func TestProcessStation_Reactivation_AfterClosure(t *testing.T) { t.Helper() // Read Redis address from environment, default to localhost:6379 redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rc := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: "", DB: 0, }) defer rc.Close() ctx := context.Background() // Flush Redis database for test isolation rc.FlushDB(ctx) // Monitor that will return 1 trip on reactivation monitor := newMockMonitor(testMonitorID, 1, nil) // First, close the station by setting zero days to 3 and status to closed _ = rc.Set(ctx, "station:zero_days:"+testMonitorID, "3", 24*time.Hour) _ = rc.Set(ctx, "station:status:"+testMonitorID, string(StatusClosed), 24*time.Hour) // Day 4: trips resume - should reactivate err := ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error on reactivation: %v", err) } // Status should be active again statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID)) if err != nil { t.Fatalf("cache get error: %v", err) } if string(statusData) != string(StatusActive) { t.Errorf("expected status active after reactivation, got %s", string(statusData)) } // Zero days should be reset to 0 zeroDaysData, err := monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) if err != nil { t.Fatalf("cache get zero days error: %v", err) } var zeroDays int _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays != 0 { t.Errorf("expected zero days 0 after reactivation, got %d", zeroDays) } } // TestAutoClosureChronology verifies the chronology of auto-closure detection. // It tests that a station closes after exactly N=3 consecutive zero-trip days, // and that it reactivates when trips resume. func TestAutoClosureChronology(t *testing.T) { t.Helper() // Read Redis address from environment, default to localhost:6379 redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } rc := redis.NewClient(&redis.Options{ Addr: redisAddr, Password: "", DB: 0, }) defer rc.Close() ctx := context.Background() // Flush Redis database for test isolation rc.FlushDB(ctx) // Monitor that returns 0 trips monitor := newMockMonitor("test-cha", 0, nil) // Day 1: 0 trips - err declared with := err := ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error day 1: %v", err) } var zeroDays1 int zeroDaysData, _ := monitor.Cache.Get(ctx, zeroDaysKey("test-cha")) _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays1) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays1 != 1 { t.Errorf("day 1: expected zero days 1, got %d", zeroDays1) } // Day 2: 0 trips - assign to err (already declared) err = ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error day 2: %v", err) } var zeroDays2 int zeroDaysData, _ = monitor.Cache.Get(ctx, zeroDaysKey("test-cha")) _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays2) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays2 != 2 { t.Errorf("day 2: expected zero days 2, got %d", zeroDays2) } // Day 3: 0 trips - assign to err (already declared), station closes err = ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error day 3: %v", err) } var zeroDays3 int zeroDaysData, _ = monitor.Cache.Get(ctx, zeroDaysKey("test-cha")) _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays3) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays3 != 3 { t.Errorf("day 3: expected zero days 3, got %d", zeroDays3) } // Status should be closed statusData, err := monitor.Cache.Get(ctx, stationStatusKey("test-cha")) if err != nil { t.Fatalf("cache get status error: %v", err) } if string(statusData) != string(StatusClosed) { t.Errorf("expected status closed, got %s", string(statusData)) } // Day 4: trips resume - should reactivate monitor.ScheduleFunc = func(ctx context.Context, stationID string) (int, error) { return 1, nil } err = ProcessStation(ctx, monitor) if err != nil { t.Fatalf("unexpected error reactivation: %v", err) } // Status should be active again statusData, err = monitor.Cache.Get(ctx, stationStatusKey("test-cha")) if err != nil { t.Fatalf("cache get status error: %v", err) } if string(statusData) != string(StatusActive) { t.Errorf("expected status active after reactivation, got %s", string(statusData)) } var zeroDays4 int zeroDaysData, _ = monitor.Cache.Get(ctx, zeroDaysKey("test-cha")) _, err = fmt.Sscanf(string(zeroDaysData), "%d", &zeroDays4) if err != nil { t.Fatalf("failed to parse zero days: %v", err) } if zeroDays4 != 0 { t.Errorf("expected zero days 0 after reactivation, got %d", zeroDays4) } }