Files
trip-planner/cmd/cron/station_status_test.go
Vladimir Zagainov ca8b137e55
Some checks failed
CI / lint (push) Successful in 14s
CI / test (push) Failing after 1m13s
CI / build (push) Has been skipped
CI / docker (push) Has been skipped
fmt: apply gofmt to test files
2026-08-20 18:04:23 +03:00

394 lines
11 KiB
Go

package main
import (
"context"
"fmt"
"os"
"testing"
"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)
}
}