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

View File

@@ -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,
}
}
@@ -345,4 +345,4 @@ func splitBySlash(s string) []string {
parts = append(parts, s[start:])
}
return parts
}
}

View File

@@ -154,4 +154,111 @@ func TestHandlerStationStatus(t *testing.T) {
t.Fatalf("failed to unmarshal response: %v", err)
}
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.
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.
@@ -166,4 +166,4 @@ func ProcessAllStations(ctx context.Context, monitors []*StationMonitor) error {
}
}
return nil
}
}

View File

@@ -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)
@@ -232,4 +232,4 @@ func TestProcessStation_Reactivation_AfterClosure(t *testing.T) {
if zeroDays != 0 {
t.Errorf("expected zero days 0 after reactivation, got %d", zeroDays)
}
}
}