package main import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-redis/redis/v8" "trip-planner/internal/routing" "trip-planner/internal/yandex" ) func newMockHandlerContext() *HandlerContext { redisClient := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) // Create an empty routing graph router := routing.NewGraph() // Create Yandex client yandexClient := yandex.NewClient("test-key") return NewHandlerContext(redisClient, router, yandexClient) } func TestHandlerCityAutocomplete(t *testing.T) { h := newMockHandlerContext() req := httptest.NewRequest("GET", "/v1/cities?query=mos", nil) rr := httptest.NewRecorder() CityAutocomplete(h, rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) } var resp []cityResponse if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } t.Logf("city autocomplete response: %d cities", len(resp)) } func TestHandlerCityStations(t *testing.T) { h := newMockHandlerContext() req := httptest.NewRequest("GET", "/v1/cities/1/stations", nil) rr := httptest.NewRecorder() CityStations(h, rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) } } func TestHandlerRouteSearch(t *testing.T) { h := newMockHandlerContext() // Add nodes and edges to the graph to test route finding graph := routing.NewGraph() graph.AddNode(&routing.Node{ID: "c146", Type: routing.NodeTypeCity, Name: "Simferopol"}) graph.AddNode(&routing.Node{ID: "c213", Type: routing.NodeTypeCity, Name: "Moscow"}) graph.AddNode(&routing.Node{ID: "s9600213", Type: routing.NodeTypeStation, Name: "Шереметьево", CityCode: "c146"}) graph.AddNode(&routing.Node{ID: "s9600396", Type: routing.NodeTypeStation, Name: "Симферополь", CityCode: "c146"}) // Add synthetic edges: station <-> city graph.AddEdge(&routing.Edge{ From: graph.Nodes()[2], // s9600213 To: graph.Nodes()[0], // c146 Kind: routing.EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true, }) graph.AddEdge(&routing.Edge{ From: graph.Nodes()[0], // c146 To: graph.Nodes()[2], // s9600213 Kind: routing.EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true, }) graph.AddEdge(&routing.Edge{ From: graph.Nodes()[3], // s9600396 To: graph.Nodes()[0], // c146 Kind: routing.EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true, }) graph.AddEdge(&routing.Edge{ From: graph.Nodes()[0], // c146 To: graph.Nodes()[3], // s9600396 Kind: routing.EdgeKindSynthetic, Duration: 300, Transport: "train", IsTransfer: true, }) // Replace the router with our test graph h.Router = graph // Create request with JSON body req := httptest.NewRequest("POST", "/v1/routes/search", strings.NewReader(`{"from_city_id": "c146", "to_city_id": "c213", "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) } func TestHandlerRouteGeoJSON(t *testing.T) { h := newMockHandlerContext() req := httptest.NewRequest("GET", "/v1/routes/search-123/route-456/geojson", nil) rr := httptest.NewRecorder() RouteGeoJSON(h, rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) } var resp routeGeoJSONResponse if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } t.Logf("route geojson response: %+v", resp) } func TestHandlerStationStatus(t *testing.T) { h := newMockHandlerContext() req := httptest.NewRequest("GET", "/v1/stations/s9600213/status", nil) rr := httptest.NewRecorder() StationStatus(h, rr, req) if rr.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rr.Code) } var resp stationStatusResponse if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { 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) } }