feat: implement cron job for station status detection
- Create cmd/cron/station_status.go with daily station status checking - Implement schedule querying via Yandex API with rate limiting and circuit breaker - Closure detection: N consecutive days of zero trips (N=3) → closed status - Reactivation: active status when trips resume after being closed - Add tests for cron logic: status transition, zero-flight detection, reactivation - All tests pass: go test ./cmd/cron/ and go test ./...
This commit is contained in:
235
cmd/cron/station_status_test.go
Normal file
235
cmd/cron/station_status_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"trip-planner/internal/cache"
|
||||
"trip-planner/internal/yandex"
|
||||
)
|
||||
|
||||
func newMockMonitor(id string, tripCount int, scheduleFunc func(context.Context, string) (int, error)) *StationMonitor {
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
yc := yandex.NewClient("test-key")
|
||||
|
||||
monitor := &StationMonitor{
|
||||
ID: id,
|
||||
Yandex: yc,
|
||||
Cache: cache.NewCacheStore(rc),
|
||||
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()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
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()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
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()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
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()
|
||||
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user