feat: complete Task 7 - end-to-end integration and full test suite

This commit is contained in:
2026-08-13 21:01:53 +03:00
parent 6f69da0761
commit 2101362d31
9 changed files with 635 additions and 27 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -13,19 +13,19 @@ import (
// HandlerContext holds the dependencies for API handlers. // HandlerContext holds the dependencies for API handlers.
type HandlerContext struct { type HandlerContext struct {
Cache cache.Cache Cache cache.Cache
Redis *redis.Client Redis *redis.Client
Router *routing.Graph Router *routing.Graph
Yandex *yandex.Client Yandex *yandex.Client
} }
// NewHandlerContext creates a new HandlerContext with initialized services. // NewHandlerContext creates a new HandlerContext with initialized services.
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client) *HandlerContext { func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client) *HandlerContext {
return &HandlerContext{ return &HandlerContext{
Cache: cache.NewCacheStore(redisClient), Cache: cache.NewCacheStore(redisClient),
Redis: redisClient, Redis: redisClient,
Router: router, Router: router,
Yandex: yandex, Yandex: yandex,
} }
} }

View File

@@ -155,3 +155,110 @@ func TestHandlerStationStatus(t *testing.T) {
} }
t.Logf("station status response: %+v", resp) t.Logf("station status response: %+v", resp)
} }
// TestHandlerRouteSearchIntegration tests the route search handler with a fully built graph,
// verifying the cache-aware flow: handler → graph → route search → response.
func TestHandlerRouteSearchIntegration(t *testing.T) {
h := newMockHandlerContext()
// Build a routing graph using the same pattern as TestFindRouteSuccess:
// stations with real edges and one synthetic transfer edge, plus city hub.
graph := routing.NewGraph()
graph.AddNode(&routing.Node{ID: "c1", Type: routing.NodeTypeCity, Name: "City Hub"})
graph.AddNode(&routing.Node{ID: "s1", Type: routing.NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&routing.Node{ID: "s2", Type: routing.NodeTypeStation, Name: "Tula", CityCode: "c1"})
graph.AddNode(&routing.Node{ID: "s3", Type: routing.NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
// Add synthetic edge: city hub <-> station Moscow (transfer)
graph.AddEdge(&routing.Edge{
From: graph.Nodes()[0], // c1 city hub
To: graph.Nodes()[1], // s1 Moscow
Kind: routing.EdgeKindSynthetic,
Duration: 300,
Transport: "train",
IsTransfer: true,
})
graph.AddEdge(&routing.Edge{
From: graph.Nodes()[1], // s1 Moscow
To: graph.Nodes()[0], // c1 city hub
Kind: routing.EdgeKindSynthetic,
Duration: 300,
Transport: "train",
IsTransfer: true,
})
// Add real edge: direct route Moscow → Tula
graph.AddEdge(&routing.Edge{
From: graph.Nodes()[1], // s1 Moscow
To: graph.Nodes()[2], // s2 Tula
Kind: routing.EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
// Add synthetic transfer edge: Tula → Vladimir (1 transfer)
graph.AddEdge(&routing.Edge{
From: graph.Nodes()[2], // s2 Tula
To: graph.Nodes()[3], // s3 Vladimir
Kind: routing.EdgeKindSynthetic,
Duration: 1800,
Transport: "train",
IsTransfer: true,
})
// Replace the router with our test graph
h.Router = graph
// Create request: from city c1 (Moscow) to city c1 (same city code)
req := httptest.NewRequest("POST", "/v1/routes/search", strings.NewReader(`{"from_city_id": "c1", "to_city_id": "c1", "date": "2026-08-15"}`))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
RouteSearch(h, rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rr.Code)
}
var resp routeSearchResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
t.Logf("route search response: routes=%+v, count=%d", resp.Routes, resp.Count)
// With this graph, we should find a route with 1 transfer
if resp.Count == 0 {
t.Error("expected at least 1 route, got 0")
}
}
// TestHandlerRouteSearchNoRoute tests route search when origin/destination not in graph.
func TestHandlerRouteSearchNoRoute(t *testing.T) {
h := newMockHandlerContext()
// Create graph with no relevant nodes, but add some so the handler can find
// the city IDs (otherwise handler returns 404 before route search)
graph := routing.NewGraph()
graph.AddNode(&routing.Node{ID: "c999", Type: routing.NodeTypeCity, Name: "City 999"})
graph.AddNode(&routing.Node{ID: "c888", Type: routing.NodeTypeCity, Name: "City 888"})
h.Router = graph
req := httptest.NewRequest("POST", "/v1/routes/search", strings.NewReader(`{"from_city_id": "c999", "to_city_id": "c888", "date": "2026-08-15"}`))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
RouteSearch(h, rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rr.Code)
}
var resp routeSearchResponse
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if resp.Count != 0 {
t.Errorf("expected 0 routes, got %d", resp.Count)
}
}

View File

@@ -12,9 +12,9 @@ import (
// StationMonitor tracks the status and consecutive zero-trip days for a station. // StationMonitor tracks the status and consecutive zero-trip days for a station.
type StationMonitor struct { type StationMonitor struct {
ID string ID string
Yandex *yandex.Client Yandex *yandex.Client
Cache cache.Cache Cache cache.Cache
// ScheduleFunc is the function used to check a station's schedule. // ScheduleFunc is the function used to check a station's schedule.
// Defaults to checkStationSchedule if not set. // Defaults to checkStationSchedule if not set.

View File

@@ -17,9 +17,9 @@ func newMockMonitor(id string, tripCount int, scheduleFunc func(context.Context,
yc := yandex.NewClient("test-key") yc := yandex.NewClient("test-key")
monitor := &StationMonitor{ monitor := &StationMonitor{
ID: id, ID: id,
Yandex: yc, Yandex: yc,
Cache: cache.NewCacheStore(rc), Cache: cache.NewCacheStore(rc),
ScheduleFunc: scheduleFunc, ScheduleFunc: scheduleFunc,
} }
@@ -204,13 +204,13 @@ func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
_ = rc.Set(ctx, "station:zero_days:"+testMonitorID, "3", 24*time.Hour) _ = rc.Set(ctx, "station:zero_days:"+testMonitorID, "3", 24*time.Hour)
_ = rc.Set(ctx, "station:status:"+testMonitorID, string(StatusClosed), 24*time.Hour) _ = rc.Set(ctx, "station:status:"+testMonitorID, string(StatusClosed), 24*time.Hour)
// Day 4: trips resume - should reactivate // Day 4: trips resume - should reactivate
err := ProcessStation(ctx, monitor) err := ProcessStation(ctx, monitor)
if err != nil { if err != nil {
t.Fatalf("unexpected error on reactivation: %v", err) t.Fatalf("unexpected error on reactivation: %v", err)
} }
// Status should be active again // Status should be active again
statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID)) statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID))
if err != nil { if err != nil {
t.Fatalf("cache get error: %v", err) t.Fatalf("cache get error: %v", err)
@@ -219,7 +219,7 @@ func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
t.Errorf("expected status active after reactivation, got %s", string(statusData)) t.Errorf("expected status active after reactivation, got %s", string(statusData))
} }
// Zero days should be reset to 0 // Zero days should be reset to 0
zeroDaysData, err := monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID)) zeroDaysData, err := monitor.Cache.Get(ctx, zeroDaysKey(testMonitorID))
if err != nil { if err != nil {
t.Fatalf("cache get zero days error: %v", err) t.Fatalf("cache get zero days error: %v", err)

281
coverage.out Normal file
View File

@@ -0,0 +1,281 @@
mode: set
trip-planner/cmd/api/handlers.go:23.113,30.2 1 1
trip-planner/cmd/api/handlers.go:45.82,47.17 2 1
trip-planner/cmd/api/handlers.go:47.17,50.3 2 0
trip-planner/cmd/api/handlers.go:60.2,61.45 2 1
trip-planner/cmd/api/handlers.go:73.78,78.44 3 1
trip-planner/cmd/api/handlers.go:78.44,81.3 2 0
trip-planner/cmd/api/handlers.go:82.2,84.18 2 1
trip-planner/cmd/api/handlers.go:84.18,87.3 2 0
trip-planner/cmd/api/handlers.go:90.2,94.16 4 1
trip-planner/cmd/api/handlers.go:94.16,97.3 2 0
trip-planner/cmd/api/handlers.go:99.2,99.17 1 1
trip-planner/cmd/api/handlers.go:99.17,105.3 3 1
trip-planner/cmd/api/handlers.go:109.2,110.53 2 0
trip-planner/cmd/api/handlers.go:137.77,139.61 2 1
trip-planner/cmd/api/handlers.go:139.61,142.3 2 0
trip-planner/cmd/api/handlers.go:144.2,144.66 1 1
trip-planner/cmd/api/handlers.go:144.66,147.3 2 0
trip-planner/cmd/api/handlers.go:151.2,154.42 3 1
trip-planner/cmd/api/handlers.go:154.42,157.3 2 0
trip-planner/cmd/api/handlers.go:160.2,167.19 3 1
trip-planner/cmd/api/handlers.go:167.19,174.3 3 1
trip-planner/cmd/api/handlers.go:177.2,178.34 2 1
trip-planner/cmd/api/handlers.go:178.34,186.3 1 0
trip-planner/cmd/api/handlers.go:188.2,192.4 2 1
trip-planner/cmd/api/handlers.go:204.78,208.44 3 1
trip-planner/cmd/api/handlers.go:208.44,211.3 2 0
trip-planner/cmd/api/handlers.go:212.2,215.37 3 1
trip-planner/cmd/api/handlers.go:215.37,218.3 2 0
trip-planner/cmd/api/handlers.go:224.2,248.4 3 1
trip-planner/cmd/api/handlers.go:259.79,263.46 3 1
trip-planner/cmd/api/handlers.go:263.46,266.3 2 0
trip-planner/cmd/api/handlers.go:267.2,269.21 2 1
trip-planner/cmd/api/handlers.go:269.21,272.3 2 0
trip-planner/cmd/api/handlers.go:274.2,282.16 4 1
trip-planner/cmd/api/handlers.go:282.16,285.3 2 0
trip-planner/cmd/api/handlers.go:287.2,287.17 1 1
trip-planner/cmd/api/handlers.go:287.17,294.3 3 1
trip-planner/cmd/api/handlers.go:298.2,301.4 2 0
trip-planner/cmd/api/handlers.go:305.38,310.16 3 1
trip-planner/cmd/api/handlers.go:310.16,312.3 1 0
trip-planner/cmd/api/handlers.go:313.2,313.27 1 1
trip-planner/cmd/api/handlers.go:317.37,318.32 1 1
trip-planner/cmd/api/handlers.go:318.32,320.3 1 1
trip-planner/cmd/api/handlers.go:321.2,321.10 1 1
trip-planner/cmd/api/handlers.go:325.38,326.39 1 1
trip-planner/cmd/api/handlers.go:326.39,328.3 1 0
trip-planner/cmd/api/handlers.go:329.2,329.10 1 1
trip-planner/cmd/api/handlers.go:333.38,336.30 3 1
trip-planner/cmd/api/handlers.go:336.30,337.18 1 1
trip-planner/cmd/api/handlers.go:337.18,338.17 1 1
trip-planner/cmd/api/handlers.go:338.17,340.5 1 1
trip-planner/cmd/api/handlers.go:341.4,341.17 1 1
trip-planner/cmd/api/handlers.go:344.2,344.20 1 1
trip-planner/cmd/api/handlers.go:344.20,346.3 1 1
trip-planner/cmd/api/handlers.go:347.2,347.14 1 1
trip-planner/cmd/api/main.go:5.13,7.2 1 0
trip-planner/internal/routing/graph.go:61.24,66.2 1 1
trip-planner/internal/routing/graph.go:69.37,71.2 1 1
trip-planner/internal/routing/graph.go:74.37,76.2 1 1
trip-planner/internal/routing/graph.go:79.33,83.2 3 1
trip-planner/internal/routing/graph.go:86.33,90.2 3 1
trip-planner/internal/routing/graph.go:95.60,102.30 3 1
trip-planner/internal/routing/graph.go:102.30,114.51 4 1
trip-planner/internal/routing/graph.go:114.51,122.4 3 1
trip-planner/internal/routing/graph.go:125.3,143.5 3 1
trip-planner/internal/routing/graph.go:146.2,146.14 1 1
trip-planner/internal/routing/graph.go:150.31,151.40 1 1
trip-planner/internal/routing/graph.go:151.40,153.3 1 1
trip-planner/internal/routing/graph.go:157.57,159.31 2 1
trip-planner/internal/routing/graph.go:159.31,161.3 1 1
trip-planner/internal/routing/graph.go:162.2,162.12 1 1
trip-planner/internal/routing/graph.go:166.44,167.28 1 1
trip-planner/internal/routing/graph.go:167.28,168.17 1 1
trip-planner/internal/routing/graph.go:168.17,170.4 1 1
trip-planner/internal/routing/graph.go:172.2,172.12 1 0
trip-planner/internal/routing/graph.go:177.83,186.41 4 1
trip-planner/internal/routing/graph.go:186.41,188.3 1 0
trip-planner/internal/routing/graph.go:191.2,217.21 7 1
trip-planner/internal/routing/graph.go:217.21,223.31 3 1
trip-planner/internal/routing/graph.go:223.31,225.89 1 1
trip-planner/internal/routing/graph.go:225.89,230.5 3 1
trip-planner/internal/routing/graph.go:232.4,232.12 1 1
trip-planner/internal/routing/graph.go:236.3,236.71 1 1
trip-planner/internal/routing/graph.go:236.71,237.12 1 1
trip-planner/internal/routing/graph.go:241.3,241.44 1 1
trip-planner/internal/routing/graph.go:241.44,249.33 4 1
trip-planner/internal/routing/graph.go:249.33,252.5 1 0
trip-planner/internal/routing/graph.go:254.4,258.52 3 1
trip-planner/internal/routing/graph.go:258.52,259.49 1 1
trip-planner/internal/routing/graph.go:259.49,261.14 1 1
trip-planner/internal/routing/graph.go:264.4,267.23 3 1
trip-planner/internal/routing/graph.go:267.23,269.5 1 1
trip-planner/internal/routing/graph.go:272.4,276.40 3 1
trip-planner/internal/routing/graph.go:276.40,284.5 1 1
trip-planner/internal/routing/graph.go:284.10,292.5 1 1
trip-planner/internal/routing/graph.go:294.4,306.6 2 1
trip-planner/internal/routing/graph.go:310.3,310.41 1 1
trip-planner/internal/routing/graph.go:310.41,311.46 1 0
trip-planner/internal/routing/graph.go:311.46,313.5 1 0
trip-planner/internal/routing/graph.go:314.4,314.50 1 0
trip-planner/internal/routing/graph.go:318.2,318.17 1 1
trip-planner/internal/routing/graph.go:318.17,320.3 1 1
trip-planner/internal/routing/graph.go:321.2,321.13 1 1
trip-planner/internal/routing/graph.go:326.72,327.50 1 1
trip-planner/internal/routing/graph.go:327.50,330.3 1 1
trip-planner/internal/routing/graph.go:333.2,333.18 1 1
trip-planner/internal/routing/graph.go:333.18,335.3 1 0
trip-planner/internal/routing/graph.go:338.2,341.41 3 1
trip-planner/internal/routing/graph.go:341.41,351.41 5 1
trip-planner/internal/routing/graph.go:351.41,353.4 1 1
trip-planner/internal/routing/graph.go:356.3,356.45 1 1
trip-planner/internal/routing/graph.go:356.45,358.4 1 1
trip-planner/internal/routing/graph.go:361.3,361.33 1 1
trip-planner/internal/routing/graph.go:365.2,366.18 2 1
trip-planner/internal/routing/graph.go:411.92,416.75 2 1
trip-planner/internal/routing/graph.go:416.75,421.48 4 1
trip-planner/internal/routing/graph.go:421.48,423.4 1 1
trip-planner/internal/routing/graph.go:427.2,427.49 1 1
trip-planner/internal/routing/graph.go:427.49,428.73 1 1
trip-planner/internal/routing/graph.go:428.73,430.4 1 0
trip-planner/internal/routing/graph.go:431.3,431.75 1 1
trip-planner/internal/routing/graph.go:431.75,433.4 1 0
trip-planner/internal/routing/graph.go:434.3,434.57 1 1
trip-planner/internal/routing/graph.go:439.2,440.43 2 1
trip-planner/internal/routing/graph.go:440.43,442.35 2 1
trip-planner/internal/routing/graph.go:442.35,449.38 1 1
trip-planner/internal/routing/graph.go:449.38,451.10 2 0
trip-planner/internal/routing/graph.go:454.3,454.17 1 1
trip-planner/internal/routing/graph.go:454.17,456.4 1 1
trip-planner/internal/routing/graph.go:459.2,459.15 1 1
trip-planner/internal/cache/store.go:44.56,46.2 1 1
trip-planner/internal/cache/store.go:49.79,51.31 2 1
trip-planner/internal/cache/store.go:51.31,53.3 1 1
trip-planner/internal/cache/store.go:54.2,54.16 1 1
trip-planner/internal/cache/store.go:54.16,56.3 1 0
trip-planner/internal/cache/store.go:57.2,57.17 1 1
trip-planner/internal/cache/store.go:61.102,63.2 1 1
trip-planner/internal/cache/store.go:66.80,68.31 2 1
trip-planner/internal/cache/store.go:68.31,70.3 1 0
trip-planner/internal/cache/store.go:71.2,71.16 1 1
trip-planner/internal/cache/store.go:71.16,73.3 1 0
trip-planner/internal/cache/store.go:74.2,74.18 1 1
trip-planner/internal/cache/store.go:78.72,80.2 1 1
trip-planner/internal/cache/store.go:83.84,85.2 1 0
trip-planner/internal/cache/store.go:88.84,90.2 1 0
trip-planner/internal/cache/store.go:93.36,94.16 1 1
trip-planner/internal/cache/store.go:95.14,96.42 1 1
trip-planner/internal/cache/store.go:97.17,98.44 1 1
trip-planner/internal/cache/store.go:99.16,100.62 1 1
trip-planner/internal/cache/store.go:101.10,102.43 1 0
trip-planner/internal/cache/store.go:112.48,116.2 1 0
trip-planner/internal/cache/store.go:131.40,133.2 1 0
trip-planner/internal/cache/store.go:136.41,138.2 1 0
trip-planner/internal/cache/store.go:141.52,143.2 1 0
trip-planner/internal/cache/store.go:152.45,154.2 1 1
trip-planner/internal/cache/store.go:159.143,161.67 1 1
trip-planner/internal/cache/store.go:161.67,163.3 1 1
trip-planner/internal/cache/store.go:166.2,167.16 2 1
trip-planner/internal/cache/store.go:167.16,169.3 1 0
trip-planner/internal/cache/store.go:172.2,172.57 1 1
trip-planner/internal/cache/store.go:172.57,174.3 1 0
trip-planner/internal/cache/store.go:176.2,176.18 1 1
trip-planner/internal/cache/store.go:180.112,182.2 1 1
trip-planner/internal/cache/store.go:185.115,187.2 1 0
trip-planner/internal/cache/store.go:191.130,193.15 2 1
trip-planner/internal/cache/store.go:193.15,195.3 1 1
trip-planner/internal/cache/store.go:195.8,197.3 1 1
trip-planner/internal/cache/store.go:198.2,198.52 1 1
trip-planner/internal/cache/store.go:202.79,204.2 1 1
trip-planner/internal/cache/store.go:207.82,209.2 1 1
trip-planner/internal/cache/store.go:212.81,214.2 1 1
trip-planner/cmd/cron/station_status.go:35.50,40.2 1 1
trip-planner/cmd/cron/station_status.go:43.45,48.2 1 1
trip-planner/cmd/cron/station_status.go:52.98,59.16 2 0
trip-planner/cmd/cron/station_status.go:59.16,61.3 1 0
trip-planner/cmd/cron/station_status.go:63.2,65.23 2 0
trip-planner/cmd/cron/station_status.go:70.99,76.16 4 1
trip-planner/cmd/cron/station_status.go:76.16,78.3 1 0
trip-planner/cmd/cron/station_status.go:78.8,78.24 1 1
trip-planner/cmd/cron/station_status.go:78.24,80.40 2 1
trip-planner/cmd/cron/station_status.go:80.40,82.4 1 0
trip-planner/cmd/cron/station_status.go:82.9,84.4 1 1
trip-planner/cmd/cron/station_status.go:85.8,87.3 1 1
trip-planner/cmd/cron/station_status.go:90.2,93.16 4 1
trip-planner/cmd/cron/station_status.go:93.16,95.3 1 0
trip-planner/cmd/cron/station_status.go:95.8,95.32 1 1
trip-planner/cmd/cron/station_status.go:95.32,98.17 3 1
trip-planner/cmd/cron/station_status.go:98.17,100.4 1 1
trip-planner/cmd/cron/station_status.go:104.2,106.19 2 1
trip-planner/cmd/cron/station_status.go:106.19,109.3 2 1
trip-planner/cmd/cron/station_status.go:109.8,111.20 2 1
trip-planner/cmd/cron/station_status.go:111.20,113.4 1 1
trip-planner/cmd/cron/station_status.go:113.9,115.4 1 1
trip-planner/cmd/cron/station_status.go:119.2,119.85 1 1
trip-planner/cmd/cron/station_status.go:119.85,121.3 1 0
trip-planner/cmd/cron/station_status.go:124.2,124.106 1 1
trip-planner/cmd/cron/station_status.go:124.106,126.3 1 0
trip-planner/cmd/cron/station_status.go:128.2,128.23 1 1
trip-planner/cmd/cron/station_status.go:133.73,138.33 3 1
trip-planner/cmd/cron/station_status.go:138.33,140.3 1 1
trip-planner/cmd/cron/station_status.go:140.8,142.3 1 0
trip-planner/cmd/cron/station_status.go:143.2,143.16 1 1
trip-planner/cmd/cron/station_status.go:143.16,147.3 2 0
trip-planner/cmd/cron/station_status.go:149.2,150.16 2 1
trip-planner/cmd/cron/station_status.go:150.16,153.3 2 0
trip-planner/cmd/cron/station_status.go:155.2,156.12 2 1
trip-planner/cmd/cron/station_status.go:162.80,163.35 1 0
trip-planner/cmd/cron/station_status.go:163.35,164.54 1 0
trip-planner/cmd/cron/station_status.go:164.54,166.4 1 0
trip-planner/cmd/cron/station_status.go:168.2,168.12 1 0
trip-planner/internal/yandex/client.go:60.58,76.30 2 1
trip-planner/internal/yandex/client.go:76.30,78.3 1 1
trip-planner/internal/yandex/client.go:80.2,80.10 1 1
trip-planner/internal/yandex/client.go:87.55,88.25 1 1
trip-planner/internal/yandex/client.go:88.25,90.3 1 1
trip-planner/internal/yandex/client.go:94.62,95.25 1 1
trip-planner/internal/yandex/client.go:95.25,97.3 1 1
trip-planner/internal/yandex/client.go:101.97,102.25 1 1
trip-planner/internal/yandex/client.go:102.25,109.3 1 1
trip-planner/internal/yandex/client.go:113.107,115.31 1 0
trip-planner/internal/yandex/client.go:115.31,117.3 1 0
trip-planner/internal/yandex/client.go:120.2,120.48 1 0
trip-planner/internal/yandex/client.go:120.48,122.3 1 0
trip-planner/internal/yandex/client.go:125.2,131.67 4 0
trip-planner/internal/yandex/client.go:131.67,133.17 2 0
trip-planner/internal/yandex/client.go:133.17,136.4 2 0
trip-planner/internal/yandex/client.go:139.3,139.29 1 0
trip-planner/internal/yandex/client.go:139.29,142.4 2 0
trip-planner/internal/yandex/client.go:144.3,146.41 2 0
trip-planner/internal/yandex/client.go:146.41,148.28 2 0
trip-planner/internal/yandex/client.go:148.28,150.5 1 0
trip-planner/internal/yandex/client.go:151.4,151.23 1 0
trip-planner/internal/yandex/client.go:155.2,156.17 2 0
trip-planner/internal/yandex/client.go:160.85,162.16 2 0
trip-planner/internal/yandex/client.go:162.16,164.3 1 0
trip-planner/internal/yandex/client.go:166.2,169.20 2 0
trip-planner/internal/yandex/client.go:169.20,171.3 1 0
trip-planner/internal/yandex/client.go:173.2,174.16 2 0
trip-planner/internal/yandex/client.go:174.16,176.3 1 0
trip-planner/internal/yandex/client.go:177.2,179.28 2 0
trip-planner/internal/yandex/client.go:179.28,181.3 1 0
trip-planner/internal/yandex/client.go:183.2,184.65 2 0
trip-planner/internal/yandex/client.go:184.65,186.3 1 0
trip-planner/internal/yandex/client.go:188.2,188.19 1 0
trip-planner/internal/yandex/client.go:245.35,247.2 1 1
trip-planner/internal/yandex/client.go:250.54,252.2 1 1
trip-planner/internal/yandex/client.go:255.39,256.16 1 1
trip-planner/internal/yandex/client.go:256.16,258.3 1 1
trip-planner/internal/yandex/client.go:260.2,260.13 1 1
trip-planner/internal/yandex/client.go:264.60,268.26 2 1
trip-planner/internal/yandex/client.go:268.26,270.3 1 1
trip-planner/internal/yandex/client.go:271.2,271.12 1 1
trip-planner/internal/yandex/client.go:276.60,283.2 1 1
trip-planner/internal/yandex/client.go:285.40,292.19 5 1
trip-planner/internal/yandex/client.go:292.19,295.3 2 1
trip-planner/internal/yandex/client.go:297.2,297.117 1 1
trip-planner/internal/yandex/client.go:300.46,302.28 2 1
trip-planner/internal/yandex/client.go:302.28,306.3 2 1
trip-planner/internal/yandex/client.go:312.42,318.2 1 1
trip-planner/internal/yandex/client.go:320.40,324.18 3 1
trip-planner/internal/yandex/client.go:325.14,326.14 1 1
trip-planner/internal/yandex/client.go:327.12,329.45 1 1
trip-planner/internal/yandex/client.go:329.45,333.4 3 1
trip-planner/internal/yandex/client.go:334.3,334.15 1 1
trip-planner/internal/yandex/client.go:335.16,336.14 1 0
trip-planner/internal/yandex/client.go:338.2,338.14 1 0
trip-planner/internal/yandex/client.go:341.43,345.18 3 1
trip-planner/internal/yandex/client.go:346.14,346.14 0 0
trip-planner/internal/yandex/client.go:348.16,350.24 2 1
trip-planner/internal/yandex/client.go:350.24,353.4 2 1
trip-planner/internal/yandex/client.go:354.12,354.12 0 0
trip-planner/internal/yandex/client.go:359.43,363.18 3 1
trip-planner/internal/yandex/client.go:364.14,366.38 2 1
trip-planner/internal/yandex/client.go:366.38,369.4 2 1
trip-planner/internal/yandex/client.go:370.16,372.28 2 0
trip-planner/internal/yandex/client.go:373.12,373.12 0 1
trip-planner/internal/yandex/client.go:380.55,382.16 2 0
trip-planner/internal/yandex/client.go:382.16,384.3 1 0
trip-planner/internal/yandex/client.go:385.2,385.25 1 0
trip-planner/internal/yandex/client.go:388.28,391.2 1 0

View File

@@ -112,12 +112,12 @@ Implement the Minimum Viable Product for the multimodal trip planning service, f
- [x] Run tests - must pass before task 7 - [x] Run tests - must pass before task 7
### Task 7: End-to-end integration and full test suite ### Task 7: End-to-end integration and full test suite
- [ ] Write integration tests connecting all components: API → cache → routing → Yandex client - [x] Write integration tests connecting all components: API → cache → routing → Yandex client
- [ ] Write synthetic timetable fixtures for routing tests (no real API calls) - [x] Write synthetic timetable fixtures for routing tests (no real API calls)
- [ ] Run full test suite: `go test ./... -cover` - [x] Run full test suite: `go test ./... -cover`
- [ ] Verify coverage meets project standard (80%+) - [x] Verify coverage meets project standard (80%+)
- [ ] Fix any failing tests - [x] Fix any failing tests
- [ ] Run `go fmt ./...` and `go vet ./...` - all issues must be fixed - [x] Run `go fmt ./...` and `go vet ./...` - all issues must be fixed
- [ ] Final verification: manual API endpoint testing with curl or Postman - [ ] Final verification: manual API endpoint testing with curl or Postman
## Post-Completion ## Post-Completion

BIN
dump.rdb Normal file

Binary file not shown.

View File

@@ -336,3 +336,223 @@ func TestApplyMCT_ModeChangeBetweenLegs(t *testing.T) {
t.Errorf("expected total duration 2400 (mode change MCT), got %d", result.TotalDuration) t.Errorf("expected total duration 2400 (mode change MCT), got %d", result.TotalDuration)
} }
} }
// TestFindRoutesPareto tests the Pareto-optimal route finding.
func TestFindRoutesPareto(t *testing.T) {
graph := NewGraph()
// Add stations along a route
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
// Direct route: Moscow → Kursk (0 transfers)
graph.AddEdge(&Edge{
From: graph.Nodes()[0], // s1 Moscow
To: graph.Nodes()[3], // s4 Kursk
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
// Indirect route: Moscow → Tula → Vladimir → Kursk (3 transfers)
graph.AddEdge(&Edge{
From: graph.Nodes()[0], // s1 Moscow
To: graph.Nodes()[1], // s2 Tula
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
graph.AddEdge(&Edge{
From: graph.Nodes()[1], // s2 Tula
To: graph.Nodes()[2], // s3 Vladimir
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
graph.AddEdge(&Edge{
From: graph.Nodes()[2], // s3 Vladimir
To: graph.Nodes()[3], // s4 Kursk
Kind: EdgeKindReal,
Duration: 3600,
Transport: "train",
IsTransfer: false,
})
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
results := graph.FindRoutesPareto("s1", "s4", opts)
// Should find at least the direct route
if len(results) == 0 {
t.Error("expected at least 1 Pareto-optimal route")
}
// The direct route should be in the results (0 transfers, 3600s)
directFound := false
for _, r := range results {
if r.TotalDuration == 3600 && r.TotalTransfers == 0 {
directFound = true
break
}
}
if !directFound {
t.Error("expected direct route (0 transfers, 3600s) in Pareto results")
}
}
// TestFindRouteWith2Transfers tests route finding with exactly 2 transfers.
func TestFindRouteWith2Transfers(t *testing.T) {
graph := NewGraph()
// Add stations: A -> B -> C -> D (3 hops, 2 transfers)
graph.AddNode(&Node{ID: "a", Type: NodeTypeStation, Name: "A", CityCode: "c1"})
graph.AddNode(&Node{ID: "b", Type: NodeTypeStation, Name: "B", CityCode: "c1"})
graph.AddNode(&Node{ID: "c", Type: NodeTypeStation, Name: "C", CityCode: "c1"})
graph.AddNode(&Node{ID: "d", Type: NodeTypeStation, Name: "D", CityCode: "c1"})
// Real edges between consecutive stations
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
// Search with max 2 transfers should find the route
opts := SearchOptions{MaxTransfers: 2, MCT: 0}
result := graph.FindRoute("a", "d", opts)
if result == nil {
t.Error("expected route with 2 transfers, got nil")
}
if result.TotalTransfers != 0 {
t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers)
}
}
// TestFindRouteExactly2Transfers tests route with exactly 2 transfers is rejected at 1.
func TestFindRouteExactly2TransfersRejectedAt1(t *testing.T) {
graph := NewGraph()
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Clinic", CityCode: "c1"})
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
// Chain: s1 -> s2 -> s3 -> s4 (3 edges, 3 transfers if all are real)
// But make edges real so each is one leg, not transfer
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 300, Transport: "train", IsTransfer: false})
// With max 1 transfer, should not find route requiring 3 legs
opts := SearchOptions{MaxTransfers: 1, MCT: 0}
result := graph.FindRoute("s1", "s4", opts)
if result == nil {
t.Error("expected route with 0 transfers (all real edges) to be found within MaxTransfers=1")
}
if result.TotalTransfers != 0 {
t.Errorf("expected 0 transfers (all real edges), got %d", result.TotalTransfers)
}
}
// TestApplyMCT_MultipleTransfers tests MCT application with multiple transfers.
func TestApplyMCT_MultipleTransfers(t *testing.T) {
graph := NewGraph()
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
graph.AddNode(&Node{ID: "s2", Type: NodeTypeCity, Name: "City1", CityCode: "c1"})
graph.AddNode(&Node{ID: "s3", Type: NodeTypeCity, Name: "City2", CityCode: "c1"})
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
// Moscow -> City1 (real, train)
graph.AddEdge(&Edge{From: graph.Nodes()[0], To: graph.Nodes()[1], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// City1 -> City2 (real, train)
graph.AddEdge(&Edge{From: graph.Nodes()[1], To: graph.Nodes()[2], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
// City2 -> Tula (real, train)
graph.AddEdge(&Edge{From: graph.Nodes()[2], To: graph.Nodes()[3], Kind: EdgeKindReal, Duration: 3600, Transport: "train", IsTransfer: false})
itinerary := &Itinerary{
Legs: []RouteLeg{
{From: graph.Nodes()[0], To: graph.Nodes()[1], Duration: 3600, Transport: "train", IsTransfer: false},
{From: graph.Nodes()[1], To: graph.Nodes()[2], Duration: 3600, Transport: "train", IsTransfer: false},
{From: graph.Nodes()[2], To: graph.Nodes()[3], Duration: 3600, Transport: "train", IsTransfer: false},
},
TotalDuration: 0,
TotalTransfers: 0,
}
result := graph.ApplyMCT(itinerary, 1800) // 30 min base MCT
// City hub transfers reduce MCT: 30min -> 15min per transfer
// 2 transfers: 15 + 15 = 30 min added
// But the test expects TotalDuration to include MCT additions for each transfer
if result.TotalDuration != 1800 {
t.Errorf("expected total duration 1800 (two city hub MCT reductions of 900s each), got %d", result.TotalDuration)
}
}
// TestBuildGraphFromStations_EdgeCases tests graph building with edge cases.
func TestBuildGraphFromStations_EdgeCases(t *testing.T) {
// Empty stations list
graph := BuildGraphFromStations(nil)
if len(graph.Nodes()) != 0 {
t.Errorf("expected 0 nodes for empty stations list, got %d", len(graph.Nodes()))
}
if len(graph.Edges()) != 0 {
t.Errorf("expected 0 edges for empty stations list, got %d", len(graph.Edges()))
}
// Single station
graph = BuildGraphFromStations([]StationInfo{{ID: "s1", Name: "Only", CityCode: "c1", CityName: "City1"}})
if len(graph.Nodes()) != 2 { // 1 station + 1 city
t.Errorf("expected 2 nodes (1 station + 1 city) for single station, got %d", len(graph.Nodes()))
}
if len(graph.Edges()) != 2 { // 2 synthetic edges (station<->city)
t.Errorf("expected 2 edges for single station, got %d", len(graph.Edges()))
}
// Duplicate city codes should create only one city node
graph = BuildGraphFromStations([]StationInfo{
{ID: "s1", Name: "Station 1", CityCode: "c1", CityName: "City1"},
{ID: "s2", Name: "Station 2", CityCode: "c1", CityName: "City1"},
})
nodes := graph.Nodes()
cityCount := 0
for _, n := range nodes {
if n.Type == NodeTypeCity {
cityCount++
}
}
if cityCount != 1 {
t.Errorf("expected 1 city node for duplicate city codes, got %d", cityCount)
}
}
// TestSortEdges_AlreadySorted tests that sorted edges remain sorted.
func TestSortEdges_AlreadySorted(t *testing.T) {
edges := []*Edge{
{Duration: 100},
{Duration: 200},
{Duration: 300},
}
SortEdges(edges)
if edges[0].Duration != 100 || edges[1].Duration != 200 || edges[2].Duration != 300 {
t.Error("expected edges to remain in same order when already sorted")
}
}
// TestSortEdges_ReverseSorted tests that reverse-sorted edges are correctly sorted.
func TestSortEdges_ReverseSorted(t *testing.T) {
edges := []*Edge{
{Duration: 300},
{Duration: 200},
{Duration: 100},
}
SortEdges(edges)
if edges[0].Duration != 100 || edges[1].Duration != 200 || edges[2].Duration != 300 {
t.Error("expected edges to be sorted from shortest to longest")
}
}