From 17081e13e4b6c503ff13eb26f94fa743447c4058 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Mon, 26 Jan 2026 16:28:39 +0700 Subject: [PATCH] Fix: resolve_offer_to_hub call in resolve_route_to_coordinate (same graphene self=None bug) --- geo_app/schema.py | 3 +- ...hql_endpoints.cpython-313-pytest-9.0.2.pyc | Bin 75622 -> 90148 bytes tests/test_graphql_endpoints.py | 170 ++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) diff --git a/geo_app/schema.py b/geo_app/schema.py index 5ed64ad..b8d08e3 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -1677,7 +1677,8 @@ class Query(graphene.ObjectType): logger.info("Found nearest hub %s to coordinates (%.3f, %.3f)", hub_uuid, lat, lon) # Use existing offer_to_hub logic - return self.resolve_offer_to_hub(info, offer_uuid, hub_uuid) + # Note: in graphene, self is None (root value), so we call as class method + return Query.resolve_offer_to_hub(Query, info, offer_uuid, hub_uuid) except Exception as e: logger.error("Error finding route to coordinates: %s", e) return None diff --git a/tests/__pycache__/test_graphql_endpoints.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_graphql_endpoints.cpython-313-pytest-9.0.2.pyc index c1df1bc2125f65478e30278f8f218cfeb0516df6..05afb75613255d8271f1db8855dc2567c739ddae 100644 GIT binary patch delta 11722 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 Md&-zw)L8%j09o^6`2YX_ delta 4178 zcmbtXX>e2573MrkvSnesVQeb~WOf-_-Y^u$EXG?r0ldUO41U@oKBd)rupU-RL)BE?mzf@C zV@<*0s$~kb1h>~5N{pD7t1y)h9VlK9a=8oZJb-Ti?iXNJSUco)?rN+y1t1pk6OFkk zZitvQ=fF(E&vW2ba_KL@e%1Ax5%P{X`n!_*{1J%?8` z=LDBFXBepE`{miety@p0-X^V;?%fA<_^2EDeZFqV*F-={_`O{zi*`Z*S*0hmdjqQ9 z%~YAeGE13)StmcywaO$@t%LukE6)@P3(xY{ERkro@hZ>WNN}pB3eKbyj%088GB z6piAUzf5zeLMy5#J5N?-&a{dgzRS!kdlK!I;I7`cQs?&xtx*FT#A2|co0zM^6HwVi zS+pJKvqfTS=nCxqTwDwJI|s6|i?KMd!=w6LJ?-v0yV-6rFiLEI+YaW_KECN-Bdy?% z9NZF)?bJO%2J~kz2_#$r5Xv$|9}$vn>1Do-K)ZiSz|$cT?s~&qLIGy;p|RQYI6pR2pNsa!RsvQ5 z&>wVD?|#2q_3H+8ucwy}U0#$MNugN1BaPMars3Ny(cR8Em1k-i8im1zPVvv-`Jz+s z;Vap}>?1X))>AO!v;fb4Nl07q_)`EHTy8vj6!K&Iv8QSWBCMKxot-i21rcHk0E-0Z zMybUXV_*#MBmgY4rvS?Ud4T1BrvW1XN#qJ4^)GCY8`;z@Ux2%x-k38gM!!707w}d| z@AHsC^!mc|I!2CIsGnavvohl(<`sZ+207{~6`W`Dn@_9|ykFywHpI^o(XxrNgax4( zMYQQW2FLjEF)KaCe?L}V*q>wz#mgoR728ssma<<8om(f@ub}@dUO#s0 z7X&*4%~%xEpZKxYYRb-HUNR{v5^^+?7?XQ`p-P1tp-MHQQpJ;=$4|VT7d-O%O-hv= zdPodzw$Eh4;#YH~QAXtnA+cWrE&?tA{tWmF;3xo#CVD-jAKn7!st)aUX_S}xehV!$ z`Y5CZ4V(j7VLcUVTheKca+$`v(`nuuI`pX2h{&1VDc?-7<@d*i~&)A>~q&<}0L)%8NAqn?kahLiId;ql}!UlsE9QD}WyZexmKFpd}>R zv?y-vcm>^Kmq7@JN^Z^OLR*GB9g-kT+-$TpYp8;*Y5i-cHd3w2qpDR#AZ3~>%6#cH zJdD0&%X)0&-pnk8HEBI-shsAGkFKTTG~rbDdb-PKjdluedJhox>ysL~BPH4|ZA&dJ zHC=<^+uDIzTAcHdknOXkF*PHmsoM2g$`oNws1pXRAFryT*3Z)7sNz^8y% z#q}B{0GCO-eh1k?7b^_hDya2}OBwI0CpCWYDJY*7u;=FJjaN49Zd_-NV}lrc0Dvn+ zI1J(%u~2JhqD1TC7!G%Eb_|aN0EebF(TWI}(SJiC4HvYAuII_q1T5zDTy6($)noy- z&wd2aZOzo6bSUE|o9W^$rVqE!J4zZH75_H|#m_{O(7wA(L|fIaZlj`z*-Iih2s=m3 zQ=6EXqN1CJjS%}Ey_;&3{fhSYyXn+KqK&lD%DAtEgUxhU&CL*@8%^Ni{D?6W^{yBKu=F_I=fw-9;dTkDcC2zs<==5K&;af zb_YCOzo$oKx8pI}seRB!1zoH0p!n3#e^S{9(e!lF9=G4!voCZXHHwkY34xpqS%t$! zH()R9+3E4Roz5up^@NIUar5=M*Rm|_u#+|>-ycUlx-moke6S%wyXvIX2Dtlwn48Rd diff --git a/tests/test_graphql_endpoints.py b/tests/test_graphql_endpoints.py index f4a991f..4c12ecd 100644 --- a/tests/test_graphql_endpoints.py +++ b/tests/test_graphql_endpoints.py @@ -454,6 +454,176 @@ class TestNearestEndpoints: suppliers = data['data']['nearestSuppliers'] print(f"✓ nearestSuppliers with product: {len(suppliers)} suppliers for product {product_uuid[:8]}") + def test_nearest_offers_with_hub_uuid(self): + """Test nearestOffers with hubUuid - should return offers with calculated routes. + + This tests the fix for the bug where resolve_offer_to_hub was called incorrectly + (self was None in graphene resolvers). + """ + # First, get a hub UUID from the database + hubs_query = """ + query { + nearestHubs(lat: 0, lon: 0, radius: 20000, limit: 5) { + uuid + name + latitude + longitude + } + } + """ + hubs_response = requests.post(GEO_URL, json={'query': hubs_query}) + hubs_data = hubs_response.json() + + if not hubs_data.get('data', {}).get('nearestHubs'): + pytest.skip("No hubs found in database") + + hub = hubs_data['data']['nearestHubs'][0] + hub_uuid = hub['uuid'] + hub_lat = hub['latitude'] + hub_lon = hub['longitude'] + + # Now test nearestOffers with this hub UUID + query = """ + query NearestOffers($lat: Float!, $lon: Float!, $radius: Float, $hubUuid: String, $limit: Int) { + nearestOffers(lat: $lat, lon: $lon, radius: $radius, hubUuid: $hubUuid, limit: $limit) { + uuid + productUuid + productName + supplierUuid + supplierName + latitude + longitude + pricePerUnit + currency + distanceKm + routes { + totalDistanceKm + totalTimeSeconds + stages { + fromUuid + fromName + fromLat + fromLon + toUuid + toName + toLat + toLon + distanceKm + travelTimeSeconds + transportType + } + } + } + } + """ + # Search around the hub location with large radius + variables = { + 'lat': float(hub_lat) if hub_lat else 0.0, + 'lon': float(hub_lon) if hub_lon else 0.0, + 'radius': 5000, # 5000km radius to find offers + 'hubUuid': hub_uuid, + '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), "nearestOffers should return a list" + + # The key assertion: with hubUuid, we should get offers with routes calculated + # (This was the bug - resolve_offer_to_hub was failing silently) + print(f"✓ nearestOffers with hubUuid: {len(offers)} offers for hub '{hub['name']}'") + + if len(offers) > 0: + # Check first offer structure + offer = offers[0] + assert 'uuid' in offer + assert 'productUuid' in offer + assert 'routes' in offer, "Offer should have routes field when hubUuid is provided" + + # If routes exist, verify structure + if offer['routes'] and len(offer['routes']) > 0: + route = offer['routes'][0] + assert 'totalDistanceKm' in route + assert 'totalTimeSeconds' in route + assert 'stages' in route + + if route['stages'] and len(route['stages']) > 0: + stage = route['stages'][0] + assert 'fromUuid' in stage + assert 'toUuid' in stage + assert 'transportType' in stage + assert 'distanceKm' in stage + print(f" Route has {len(route['stages'])} stages, total {route['totalDistanceKm']:.1f}km") + + def test_nearest_offers_with_hub_and_product(self): + """Test nearestOffers with both hubUuid and productUuid filters.""" + # Get a product and hub + products_query = "query { products { uuid name } }" + prod_response = requests.post(GEO_URL, json={'query': products_query}) + products = prod_response.json().get('data', {}).get('products', []) + + hubs_query = """ + query { + nearestHubs(lat: 0, lon: 0, radius: 20000, limit: 1) { + uuid + name + latitude + longitude + } + } + """ + hubs_response = requests.post(GEO_URL, json={'query': hubs_query}) + hubs = hubs_response.json().get('data', {}).get('nearestHubs', []) + + if not products or not hubs: + pytest.skip("No products or hubs in database") + + product = products[0] + hub = hubs[0] + + query = """ + query NearestOffers($lat: Float!, $lon: Float!, $radius: Float, $productUuid: String, $hubUuid: String) { + nearestOffers(lat: $lat, lon: $lon, radius: $radius, productUuid: $productUuid, hubUuid: $hubUuid) { + uuid + productUuid + productName + routes { + totalDistanceKm + stages { + transportType + } + } + } + } + """ + variables = { + 'lat': float(hub['latitude']) if hub['latitude'] else 0.0, + 'lon': float(hub['longitude']) if hub['longitude'] else 0.0, + 'radius': 10000, + 'productUuid': product['uuid'], + 'hubUuid': hub['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 has wrong productUuid: {offer['productUuid']} != {product['uuid']}" + + print(f"✓ nearestOffers with hub+product: {len(offers)} offers for '{product['name']}' via hub '{hub['name']}'") + class TestRoutingEndpoints: """Test routing and pathfinding endpoints."""