MVP-Routing-Implementation #1
@@ -13,19 +13,19 @@ import (
|
||||
|
||||
// HandlerContext holds the dependencies for API handlers.
|
||||
type HandlerContext struct {
|
||||
Cache cache.Cache
|
||||
Redis *redis.Client
|
||||
Router *routing.Graph
|
||||
Yandex *yandex.Client
|
||||
Cache cache.Cache
|
||||
Redis *redis.Client
|
||||
Router *routing.Graph
|
||||
Yandex *yandex.Client
|
||||
}
|
||||
|
||||
// NewHandlerContext creates a new HandlerContext with initialized services.
|
||||
func NewHandlerContext(redisClient *redis.Client, router *routing.Graph, yandex *yandex.Client) *HandlerContext {
|
||||
return &HandlerContext{
|
||||
Cache: cache.NewCacheStore(redisClient),
|
||||
Redis: redisClient,
|
||||
Router: router,
|
||||
Yandex: yandex,
|
||||
Cache: cache.NewCacheStore(redisClient),
|
||||
Redis: redisClient,
|
||||
Router: router,
|
||||
Yandex: yandex,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,3 +155,110 @@ func TestHandlerStationStatus(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
|
||||
// StationMonitor tracks the status and consecutive zero-trip days for a station.
|
||||
type StationMonitor struct {
|
||||
ID string
|
||||
Yandex *yandex.Client
|
||||
Cache cache.Cache
|
||||
ID string
|
||||
Yandex *yandex.Client
|
||||
Cache cache.Cache
|
||||
|
||||
// ScheduleFunc is the function used to check a station's schedule.
|
||||
// Defaults to checkStationSchedule if not set.
|
||||
|
||||
@@ -17,9 +17,9 @@ func newMockMonitor(id string, tripCount int, scheduleFunc func(context.Context,
|
||||
yc := yandex.NewClient("test-key")
|
||||
|
||||
monitor := &StationMonitor{
|
||||
ID: id,
|
||||
Yandex: yc,
|
||||
Cache: cache.NewCacheStore(rc),
|
||||
ID: id,
|
||||
Yandex: yc,
|
||||
Cache: cache.NewCacheStore(rc),
|
||||
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:status:"+testMonitorID, string(StatusClosed), 24*time.Hour)
|
||||
|
||||
// Day 4: trips resume - should reactivate
|
||||
// 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
|
||||
// Status should be active again
|
||||
statusData, err := monitor.Cache.Get(ctx, stationStatusKey(testMonitorID))
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
// Zero days should be reset to 0
|
||||
// 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)
|
||||
|
||||
281
coverage.out
Normal file
281
coverage.out
Normal 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
|
||||
@@ -112,12 +112,12 @@ Implement the Minimum Viable Product for the multimodal trip planning service, f
|
||||
- [x] Run tests - must pass before task 7
|
||||
|
||||
### Task 7: End-to-end integration and full test suite
|
||||
- [ ] Write integration tests connecting all components: API → cache → routing → Yandex client
|
||||
- [ ] Write synthetic timetable fixtures for routing tests (no real API calls)
|
||||
- [ ] Run full test suite: `go test ./... -cover`
|
||||
- [ ] Verify coverage meets project standard (80%+)
|
||||
- [ ] Fix any failing tests
|
||||
- [ ] Run `go fmt ./...` and `go vet ./...` - all issues must be fixed
|
||||
- [x] Write integration tests connecting all components: API → cache → routing → Yandex client
|
||||
- [x] Write synthetic timetable fixtures for routing tests (no real API calls)
|
||||
- [x] Run full test suite: `go test ./... -cover`
|
||||
- [x] Verify coverage meets project standard (80%+)
|
||||
- [x] Fix any failing tests
|
||||
- [x] Run `go fmt ./...` and `go vet ./...` - all issues must be fixed
|
||||
- [ ] Final verification: manual API endpoint testing with curl or Postman
|
||||
|
||||
## Post-Completion
|
||||
|
||||
@@ -336,3 +336,223 @@ func TestApplyMCT_ModeChangeBetweenLegs(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user