diff --git a/pyproject.toml b/pyproject.toml index bf20835..e0d06a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,10 @@ dependencies = [ [tool.poetry] package-mode = false +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" +requests = "^2.32.0" + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e10dbdf --- /dev/null +++ b/pytest.ini @@ -0,0 +1,12 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..676eb60 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Run geo service GraphQL endpoint tests + +set -e + +cd "$(dirname "$0")" + +echo "๐Ÿงช Running Geo Service GraphQL Tests" +echo "====================================" +echo "" + +# Check if TEST_GEO_URL is set, otherwise use production +if [ -z "$TEST_GEO_URL" ]; then + export TEST_GEO_URL="https://geo.optovia.ru/graphql/public/" + echo "๐Ÿ“ Testing against: $TEST_GEO_URL (production)" +else + echo "๐Ÿ“ Testing against: $TEST_GEO_URL" +fi + +echo "" + +# Install dependencies if needed +if ! poetry run python -c "import pytest" 2>/dev/null; then + echo "๐Ÿ“ฆ Installing dependencies..." + poetry install --with dev + echo "" +fi + +# Run tests +echo "๐Ÿš€ Running tests..." +echo "" + +poetry run pytest tests/test_graphql_endpoints.py -v -s "$@" + +echo "" +echo "โœ… Test run complete" diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b1183a4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,130 @@ +# Geo Service Tests + +Comprehensive test suite for all GraphQL endpoints in the geo service. + +## Test Coverage + +### Basic Endpoints (4 tests) +- `test_products_query` - List all unique products +- `test_nodes_query_basic` - List hubs/nodes without filters +- `test_nodes_query_with_filters` - Filter nodes by transport type and country +- `test_nodes_query_with_bounds` - Filter nodes by geographic bounds +- `test_clustered_nodes_query` - Map clustering for visualization + +### Nearest Endpoints (6 tests) +- `test_nearest_hubs` - Find hubs near coordinates +- `test_nearest_hubs_with_product_filter` - Find hubs with specific product +- `test_nearest_offers` - Find offers near coordinates +- `test_nearest_offers_with_product_filter` - Find offers for specific product +- `test_nearest_suppliers` - Find suppliers near coordinates +- `test_nearest_suppliers_with_product_filter` - Find suppliers with product + +### Routing Endpoints (3 tests) +- `test_route_to_coordinate` - Multi-hop route from offer to destination +- `test_auto_route` - Road route between coordinates (requires OSRM) +- `test_rail_route` - Rail route between coordinates + +### Edge Cases (3 tests) +- `test_nearest_with_zero_radius` - Very small search radius +- `test_invalid_coordinates` - Invalid lat/lon values +- `test_nonexistent_uuid` - Non-existent offer UUID + +**Total: 16 tests covering 8 main endpoints** + +## Running Tests + +### Local Testing (against production) + +```bash +cd backends/geo +poetry install +poetry run pytest tests/test_graphql_endpoints.py -v +``` + +### Testing against different endpoint + +```bash +export TEST_GEO_URL=https://geo-staging.example.com/graphql/public/ +poetry run pytest tests/test_graphql_endpoints.py -v +``` + +### Run specific test class + +```bash +poetry run pytest tests/test_graphql_endpoints.py::TestNearestEndpoints -v +``` + +### Run single test + +```bash +poetry run pytest tests/test_graphql_endpoints.py::TestNearestEndpoints::test_nearest_offers -v +``` + +### Show print output + +```bash +poetry run pytest tests/test_graphql_endpoints.py -v -s +``` + +## CI Integration + +Tests should be run on each deployment: + +```yaml +# .gitea/workflows/test.yml +- name: Run geo endpoint tests + run: | + cd backends/geo + poetry install + export TEST_GEO_URL=https://geo.optovia.ru/graphql/public/ + poetry run pytest tests/test_graphql_endpoints.py -v +``` + +## Test Data Requirements + +Tests use real data from the production/staging database. Required data: +- At least one product in `products` collection +- At least one hub node with coordinates +- At least one offer with coordinates +- Graph edges for routing tests + +## Expected Test Results + +All tests should pass on production environment. Some tests may be skipped if: +- No products exist: `test_nearest_hubs_with_product_filter`, `test_nearest_offers_with_product_filter` +- No offers exist: `test_route_to_coordinate` +- OSRM not configured: `test_auto_route`, `test_rail_route` (warnings, not failures) + +## Troubleshooting + +### All nearest* tests return 0 results + +Check that nodes collection has documents with: +- Valid `latitude` and `longitude` fields (not null) +- Correct `node_type` field (`'hub'`, `'offer'`, `'supplier'`) + +Query ArangoDB directly: + +```javascript +// Count offers with coordinates +db._query(` + FOR node IN nodes + FILTER node.node_type == 'offer' + FILTER node.latitude != null AND node.longitude != null + RETURN node +`).toArray().length +``` + +### Test failures with 400 errors + +Check GraphQL schema matches test queries. GraphQL validation errors indicate: +- Missing required arguments +- Wrong argument types +- Invalid field names + +### Connection errors + +Verify: +- TEST_GEO_URL points to correct endpoint +- Endpoint is accessible (not behind VPN/firewall) +- GraphQL endpoint is `/graphql/public/` not `/graphql/` diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..0782730 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Geo service tests diff --git a/tests/test_graphql_endpoints.py b/tests/test_graphql_endpoints.py new file mode 100644 index 0000000..f4a991f --- /dev/null +++ b/tests/test_graphql_endpoints.py @@ -0,0 +1,672 @@ +""" +Comprehensive tests for all Geo GraphQL endpoints. +Tests use real API calls to production/staging GraphQL endpoint. +""" + +import os +import json +import requests +import pytest + +# GraphQL endpoint - override with TEST_GEO_URL env var +GEO_URL = os.getenv('TEST_GEO_URL', 'https://geo.optovia.ru/graphql/public/') + + +class TestBasicEndpoints: + """Test basic list/query endpoints.""" + + def test_products_query(self): + """Test products query - should return list of unique products.""" + query = """ + query GetProducts { + products { + uuid + name + offersCount + } + } + """ + response = requests.post(GEO_URL, json={'query': query}) + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + assert 'data' in data + assert 'products' in data['data'] + + products = data['data']['products'] + assert isinstance(products, list), "products should be a list" + if len(products) > 0: + product = products[0] + assert 'uuid' in product + assert 'name' in product + assert 'offersCount' in product + assert isinstance(product['offersCount'], int) + + print(f"โœ“ products query: {len(products)} products found") + + def test_nodes_query_basic(self): + """Test nodes query without filters.""" + query = """ + query GetNodes($limit: Int, $offset: Int) { + nodes(limit: $limit, offset: $offset) { + uuid + name + latitude + longitude + country + transportTypes + } + nodesCount + } + """ + variables = {'limit': 10, 'offset': 0} + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + nodes = data['data']['nodes'] + count = data['data']['nodesCount'] + + assert isinstance(nodes, list) + assert isinstance(count, int) + assert count > 0, "Should have at least some nodes in database" + assert len(nodes) <= 10, "Should respect limit" + + if len(nodes) > 0: + node = nodes[0] + assert 'uuid' in node + assert 'name' in node + # Coordinates might be null for some nodes + assert 'latitude' in node + assert 'longitude' in node + + print(f"โœ“ nodes query: {len(nodes)}/{count} nodes found") + + def test_nodes_query_with_filters(self): + """Test nodes query with transport type and country filters.""" + query = """ + query GetNodes($transportType: String, $country: String, $limit: Int) { + nodes(transportType: $transportType, country: $country, limit: $limit) { + uuid + name + country + transportTypes + } + nodesCount(transportType: $transportType, country: $country) + } + """ + variables = {'transportType': 'sea', 'limit': 5} + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + nodes = data['data']['nodes'] + # All nodes should have 'sea' in their transportTypes + for node in nodes: + if node.get('transportTypes'): + assert 'sea' in node['transportTypes'], f"Node {node['uuid']} missing 'sea' transport type" + + print(f"โœ“ nodes query with filters: {len(nodes)} sea nodes found") + + def test_nodes_query_with_bounds(self): + """Test nodes query with geographic bounds.""" + query = """ + query GetNodes($west: Float, $south: Float, $east: Float, $north: Float, $limit: Int) { + nodes(west: $west, south: $south, east: $east, north: $north, limit: $limit) { + uuid + name + latitude + longitude + } + } + """ + # Bounds for central Europe + variables = { + 'west': 5.0, + 'south': 45.0, + 'east': 15.0, + 'north': 55.0, + 'limit': 20 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + nodes = data['data']['nodes'] + # Verify all nodes are within bounds + for node in nodes: + if node.get('latitude') and node.get('longitude'): + lat = float(node['latitude']) + lon = float(node['longitude']) + assert variables['south'] <= lat <= variables['north'], \ + f"Node {node['uuid']} latitude {lat} outside bounds" + assert variables['west'] <= lon <= variables['east'], \ + f"Node {node['uuid']} longitude {lon} outside bounds" + + print(f"โœ“ nodes with bounds: {len(nodes)} nodes in central Europe") + + def test_clustered_nodes_query(self): + """Test clusteredNodes query for map clustering.""" + query = """ + query GetClusteredNodes($west: Float!, $south: Float!, $east: Float!, $north: Float!, $zoom: Int!) { + clusteredNodes(west: $west, south: $south, east: $east, north: $north, zoom: $zoom) { + id + latitude + longitude + count + expansionZoom + name + } + } + """ + # World view + variables = { + 'west': -180.0, + 'south': -90.0, + 'east': 180.0, + 'north': 90.0, + 'zoom': 2 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + clusters = data['data']['clusteredNodes'] + assert isinstance(clusters, list) + assert len(clusters) > 0, "Should have clusters at zoom level 2" + + for cluster in clusters: + assert 'id' in cluster + assert 'latitude' in cluster + assert 'longitude' in cluster + assert 'count' in cluster + assert cluster['count'] >= 1 + + print(f"โœ“ clusteredNodes: {len(clusters)} clusters/points at zoom 2") + + +class TestNearestEndpoints: + """Test new coordinate-based 'nearest' endpoints.""" + + def test_nearest_hubs(self): + """Test nearestHubs query - find hubs near coordinates.""" + query = """ + query NearestHubs($lat: Float!, $lon: Float!, $radius: Float, $limit: Int) { + nearestHubs(lat: $lat, lon: $lon, radius: $radius, limit: $limit) { + uuid + name + latitude + longitude + country + transportTypes + distanceKm + } + } + """ + # Rotterdam coordinates (major European port) + variables = { + 'lat': 51.9244, + 'lon': 4.4777, + 'radius': 200, + 'limit': 5 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200, f"Status: {response.status_code}, Body: {response.text}" + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + hubs = data['data']['nearestHubs'] + assert isinstance(hubs, list) + + # Should find some hubs in 200km radius of Rotterdam + if len(hubs) > 0: + hub = hubs[0] + assert 'uuid' in hub + assert 'name' in hub + assert 'distanceKm' in hub + assert hub['distanceKm'] <= 200, f"Hub {hub['uuid']} distance {hub['distanceKm']} exceeds radius" + + # Verify hubs are sorted by distance + distances = [h['distanceKm'] for h in hubs] + assert distances == sorted(distances), "Hubs should be sorted by distance" + + print(f"โœ“ nearestHubs: {len(hubs)} hubs near Rotterdam") + + def test_nearest_hubs_with_product_filter(self): + """Test nearestHubs with product filter.""" + # First get a product UUID + products_query = "query { products { uuid } }" + prod_response = requests.post(GEO_URL, json={'query': products_query}) + products = prod_response.json()['data']['products'] + + if not products: + pytest.skip("No products in database") + + product_uuid = products[0]['uuid'] + + query = """ + query NearestHubs($lat: Float!, $lon: Float!, $radius: Float, $productUuid: String) { + nearestHubs(lat: $lat, lon: $lon, radius: $radius, productUuid: $productUuid) { + uuid + name + distanceKm + } + } + """ + variables = { + 'lat': 50.0, + 'lon': 10.0, + 'radius': 1000, + 'productUuid': product_uuid + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + hubs = data['data']['nearestHubs'] + print(f"โœ“ nearestHubs with product filter: {len(hubs)} hubs for product {product_uuid[:8]}") + + def test_nearest_offers(self): + """Test nearestOffers query - find offers near coordinates.""" + query = """ + query NearestOffers($lat: Float!, $lon: Float!, $radius: Float, $limit: Int) { + nearestOffers(lat: $lat, lon: $lon, radius: $radius, limit: $limit) { + uuid + productUuid + productName + latitude + longitude + pricePerUnit + currency + distanceKm + } + } + """ + # Central Europe + variables = { + 'lat': 50.0, + 'lon': 10.0, + 'radius': 500, + 'limit': 10 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200, f"Status: {response.status_code}, Body: {response.text}" + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + offers = data['data']['nearestOffers'] + assert isinstance(offers, list) + + if len(offers) > 0: + offer = offers[0] + assert 'uuid' in offer + assert 'productUuid' in offer + assert 'distanceKm' in offer + assert offer['distanceKm'] <= 500 + + # Verify offers are sorted by distance + distances = [o['distanceKm'] for o in offers] + assert distances == sorted(distances), "Offers should be sorted by distance" + + print(f"โœ“ nearestOffers: {len(offers)} offers in Central Europe") + + def test_nearest_offers_with_product_filter(self): + """Test nearestOffers with product UUID filter.""" + # First get a product UUID + products_query = "query { products { uuid name } }" + prod_response = requests.post(GEO_URL, json={'query': products_query}) + products = prod_response.json()['data']['products'] + + if not products: + pytest.skip("No products in database") + + product_uuid = products[0]['uuid'] + product_name = products[0]['name'] + + query = """ + query NearestOffers($lat: Float!, $lon: Float!, $radius: Float, $productUuid: String) { + nearestOffers(lat: $lat, lon: $lon, radius: $radius, productUuid: $productUuid) { + uuid + productUuid + productName + distanceKm + } + } + """ + # Global search with large radius + variables = { + 'lat': 0.0, + 'lon': 0.0, + 'radius': 20000, + 'productUuid': product_uuid + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + offers = data['data']['nearestOffers'] + # All offers should be for the requested product + for offer in offers: + assert offer['productUuid'] == product_uuid, \ + f"Offer {offer['uuid']} has wrong product UUID" + + print(f"โœ“ nearestOffers with product: {len(offers)} offers for '{product_name}'") + + def test_nearest_suppliers(self): + """Test nearestSuppliers query - find suppliers near coordinates.""" + query = """ + query NearestSuppliers($lat: Float!, $lon: Float!, $radius: Float, $limit: Int) { + nearestSuppliers(lat: $lat, lon: $lon, radius: $radius, limit: $limit) { + uuid + name + latitude + longitude + distanceKm + } + } + """ + variables = { + 'lat': 52.52, # Berlin + 'lon': 13.405, + 'radius': 300, + 'limit': 10 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + suppliers = data['data']['nearestSuppliers'] + assert isinstance(suppliers, list) + + if len(suppliers) > 0: + supplier = suppliers[0] + assert 'uuid' in supplier + assert 'name' in supplier + assert 'distanceKm' in supplier + assert supplier['distanceKm'] <= 300 + + # Verify sorted by distance + distances = [s['distanceKm'] for s in suppliers] + assert distances == sorted(distances) + + print(f"โœ“ nearestSuppliers: {len(suppliers)} suppliers near Berlin") + + def test_nearest_suppliers_with_product_filter(self): + """Test nearestSuppliers with product filter.""" + # Get a product UUID + products_query = "query { products { uuid } }" + prod_response = requests.post(GEO_URL, json={'query': products_query}) + products = prod_response.json()['data']['products'] + + if not products: + pytest.skip("No products in database") + + product_uuid = products[0]['uuid'] + + query = """ + query NearestSuppliers($lat: Float!, $lon: Float!, $radius: Float, $productUuid: String) { + nearestSuppliers(lat: $lat, lon: $lon, radius: $radius, productUuid: $productUuid) { + uuid + name + distanceKm + } + } + """ + variables = { + 'lat': 50.0, + 'lon': 10.0, + 'radius': 1000, + 'productUuid': product_uuid + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + suppliers = data['data']['nearestSuppliers'] + print(f"โœ“ nearestSuppliers with product: {len(suppliers)} suppliers for product {product_uuid[:8]}") + + +class TestRoutingEndpoints: + """Test routing and pathfinding endpoints.""" + + def test_route_to_coordinate(self): + """Test routeToCoordinate query - find route from offer to coordinates.""" + # First, get an offer UUID with coordinates + offers_query = """ + query { + nearestOffers(lat: 50.0, lon: 10.0, radius: 1000, limit: 1) { + uuid + latitude + longitude + } + } + """ + offers_response = requests.post(GEO_URL, json={'query': offers_query}) + offers_data = offers_response.json() + + if not offers_data.get('data', {}).get('nearestOffers'): + pytest.skip("No offers found for routing test") + + offer = offers_data['data']['nearestOffers'][0] + offer_uuid = offer['uuid'] + + query = """ + query RouteToCoordinate($offerUuid: String!, $lat: Float!, $lon: Float!) { + routeToCoordinate(offerUuid: $offerUuid, lat: $lat, lon: $lon) { + offerUuid + distanceKm + routes { + totalDistanceKm + totalTimeSeconds + stages { + fromUuid + fromName + fromLat + fromLon + toUuid + toName + toLat + toLon + distanceKm + travelTimeSeconds + transportType + } + } + } + } + """ + # Route to Amsterdam + variables = { + 'offerUuid': offer_uuid, + 'lat': 52.3676, + 'lon': 4.9041 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200, f"Status: {response.status_code}, Body: {response.text}" + + data = response.json() + assert 'errors' not in data, f"GraphQL errors: {data.get('errors')}" + + route_data = data['data']['routeToCoordinate'] + assert route_data is not None + assert route_data['offerUuid'] == offer_uuid + + if route_data.get('routes'): + route = route_data['routes'][0] + assert 'totalDistanceKm' in route + assert 'totalTimeSeconds' in route + assert 'stages' in route + assert len(route['stages']) > 0 + + # Verify each stage has required fields + for stage in route['stages']: + assert 'fromUuid' in stage + assert 'toUuid' in stage + assert 'distanceKm' in stage + assert 'transportType' in stage + + print(f"โœ“ routeToCoordinate: {len(route['stages'])} stages, {route['totalDistanceKm']:.1f}km") + else: + print(f"โœ“ routeToCoordinate: no routes found (offer may be isolated)") + + def test_auto_route(self): + """Test autoRoute query - calculate road route between coordinates.""" + query = """ + query AutoRoute($fromLat: Float!, $fromLon: Float!, $toLat: Float!, $toLon: Float!) { + autoRoute(fromLat: $fromLat, fromLon: $fromLon, toLat: $toLat, toLon: $toLon) { + distanceKm + geometry + } + } + """ + # Route from Amsterdam to Rotterdam + variables = { + 'fromLat': 52.3676, + 'fromLon': 4.9041, + 'toLat': 51.9244, + 'toLon': 4.4777 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + # Auto route might not be available (requires OSRM service) + if 'errors' in data: + print("โš  autoRoute: service not available (expected if OSRM not configured)") + else: + route = data['data']['autoRoute'] + if route: + assert 'distanceKm' in route + assert route['distanceKm'] > 0 + print(f"โœ“ autoRoute: {route['distanceKm']:.1f}km Amsterdam โ†’ Rotterdam") + else: + print("โš  autoRoute: returned null") + + def test_rail_route(self): + """Test railRoute query - calculate rail route between coordinates.""" + query = """ + query RailRoute($fromLat: Float!, $fromLon: Float!, $toLat: Float!, $toLon: Float!) { + railRoute(fromLat: $fromLat, fromLon: $fromLon, toLat: $toLat, toLon: $toLon) { + distanceKm + geometry + } + } + """ + variables = { + 'fromLat': 52.3676, + 'fromLon': 4.9041, + 'toLat': 51.9244, + 'toLon': 4.4777 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + # Rail route might not be available + if 'errors' in data: + print("โš  railRoute: service not available") + else: + route = data['data']['railRoute'] + if route: + assert 'distanceKm' in route + print(f"โœ“ railRoute: {route['distanceKm']:.1f}km") + else: + print("โš  railRoute: returned null") + + +class TestEdgeCases: + """Test edge cases and error handling.""" + + def test_nearest_with_zero_radius(self): + """Test nearest queries with very small radius.""" + query = """ + query NearestHubs($lat: Float!, $lon: Float!, $radius: Float) { + nearestHubs(lat: $lat, lon: $lon, radius: $radius) { + uuid + } + } + """ + variables = {'lat': 50.0, 'lon': 10.0, 'radius': 0.001} + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + assert response.status_code == 200 + + data = response.json() + assert 'errors' not in data + # Should return empty list or very few results + hubs = data['data']['nearestHubs'] + assert isinstance(hubs, list) + print(f"โœ“ nearestHubs with tiny radius: {len(hubs)} hubs") + + def test_invalid_coordinates(self): + """Test behavior with invalid latitude/longitude values.""" + query = """ + query NearestHubs($lat: Float!, $lon: Float!) { + nearestHubs(lat: $lat, lon: $lon) { + uuid + } + } + """ + # Latitude > 90 is invalid + variables = {'lat': 100.0, 'lon': 10.0} + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + # Should either return error or empty results + assert response.status_code in [200, 400] + print("โœ“ invalid coordinates handled") + + def test_nonexistent_uuid(self): + """Test route query with non-existent offer UUID.""" + query = """ + query RouteToCoordinate($offerUuid: String!, $lat: Float!, $lon: Float!) { + routeToCoordinate(offerUuid: $offerUuid, lat: $lat, lon: $lon) { + offerUuid + } + } + """ + variables = { + 'offerUuid': 'nonexistent-uuid-12345', + 'lat': 50.0, + 'lon': 10.0 + } + + response = requests.post(GEO_URL, json={'query': query, 'variables': variables}) + # Should handle gracefully (null result or error) + assert response.status_code in [200, 400] + print("โœ“ nonexistent UUID handled") + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s'])