fix: read map POIs from authenticated FalkorDB
This commit is contained in:
1 parent
90f75d41ae
commit
712ca1418c
7 files changed
+646
-206
No files matched your search
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local-only map regression: real graphs + a fresh JSON-imported scratch graph.
|
||||
|
||||
Existing graphs are read-only. Only the UUID graph created by this invocation
|
||||
is written/deleted; PostgreSQL POI access is deliberately forbidden.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from app.api import plaza
|
||||
from app.auth import get_current_user
|
||||
from app.config import settings
|
||||
from app.project_context import ProjectContext
|
||||
from app.project_lifecycle import _falkor_client, import_falkor_graph, normalize_provision_payload
|
||||
|
||||
|
||||
def map_result(graph_name):
|
||||
with patch("app.api.plaza.get_conn", side_effect=AssertionError("Map reads must not require PostgreSQL POIs")):
|
||||
return asyncio.run(plaza.map_pois(context=ProjectContext("local_map_test", graph_name, graph_name)))
|
||||
|
||||
|
||||
def verify(map_json: Path | None = None):
|
||||
if settings.falkordb_host.lower() not in {"localhost", "127.0.0.1", "::1"}:
|
||||
raise RuntimeError("This test is restricted to the local FalkorDB service")
|
||||
report = {"test": "graph-backed-map", "read_only_existing_graphs": True}
|
||||
source_nodes = None
|
||||
if map_json is not None:
|
||||
with map_json.open(encoding="utf-8") as stream:
|
||||
source_nodes = json.load(stream)["nodes"]
|
||||
assert len(source_nodes) > 2000, "Provide a map JSON with more than 2,000 POIs"
|
||||
for graph_name in ("yunyou_libo", "guiyang_new2"):
|
||||
start = time.monotonic()
|
||||
payload = map_result(graph_name)
|
||||
ids = {item["id"] for item in payload["items"]}
|
||||
assert len(ids) == payload["total"], "Duplicate map POI ids"
|
||||
assert payload["source"] == "falkordb"
|
||||
city_count = sum(str(item["adcode"]).startswith("5201") for item in payload["items"])
|
||||
report[graph_name] = {
|
||||
"graph": payload["graph_name"], "points": payload["total"],
|
||||
"guiyang_city_points": city_count, "categories": payload["categories"],
|
||||
"elapsed_seconds": round(time.monotonic() - start, 3),
|
||||
}
|
||||
if graph_name == "yunyou_libo":
|
||||
assert payload["total"] == 2799, "Libo point count changed"
|
||||
else:
|
||||
assert city_count > 0, "Guiyang map has no points in its selected city"
|
||||
if source_nodes is not None:
|
||||
source_ids = {str(node["properties"].get("element_id") or node["id"]) for node in source_nodes}
|
||||
assert not source_ids - ids, "Exported POIs are missing from the local source graph"
|
||||
report["export_snapshot"] = {
|
||||
"points": len(source_ids), "missing_from_local_graph": 0,
|
||||
"extra_points_in_local_graph": len(ids - source_ids),
|
||||
}
|
||||
|
||||
client = _falkor_client()
|
||||
scratch_name = "codex_map_verify_" + uuid.uuid4().hex
|
||||
created = False
|
||||
try:
|
||||
existing = {name.decode() if isinstance(name, bytes) else str(name) for name in client.list_graphs()}
|
||||
assert scratch_name not in existing
|
||||
source_graph = client.select_graph("guiyang_spatial_v1")
|
||||
if source_nodes is None:
|
||||
rows = source_graph.ro_query(
|
||||
"MATCH (n) WHERE n.element_id IS NOT NULL "
|
||||
"AND (n.lng IS NOT NULL OR n.longitude IS NOT NULL) "
|
||||
"AND (n.lat IS NOT NULL OR n.latitude IS NOT NULL) "
|
||||
"AND n.adcode STARTS WITH '5201' RETURN labels(n),properties(n) LIMIT 2600",
|
||||
timeout=30_000,
|
||||
).result_set
|
||||
else:
|
||||
rows = [(node.get("labels") or [node["type"]], node["properties"]) for node in source_nodes[:2600]]
|
||||
nodes, seen, entity_types = [], set(), {}
|
||||
for labels, properties in rows:
|
||||
external_id = str(properties["element_id"])
|
||||
if external_id in seen:
|
||||
continue
|
||||
seen.add(external_id)
|
||||
for label in labels:
|
||||
entity_types[label] = {"fields": {"name": {"type": "string", "required": True}}}
|
||||
nodes.append({"id": external_id, "type": labels[0], "labels": labels, "properties": properties})
|
||||
assert len(nodes) > 2000, "Test must cross the map query page boundary"
|
||||
body = {
|
||||
"project_id": scratch_name, "display_name": "城市知识图谱-贵州*贵阳",
|
||||
"spatial_map": {"enabled": True, "scope": "guizhou", "region_name": "贵阳市",
|
||||
"region_adcode": "520100", "region_level": "city"},
|
||||
"schema": {"namespace": scratch_name, "version": "1.0.0",
|
||||
"entity_types": entity_types, "relation_types": {}},
|
||||
"graph_data": {"nodes": nodes, "relations": []},
|
||||
}
|
||||
normalized = normalize_provision_payload(body)
|
||||
expected = {item["id"]: item for item in plaza._graph_map_poi_items(
|
||||
[[node["id"], node["labels"], node["properties"]] for node in nodes])}
|
||||
created = True
|
||||
counts = import_falkor_graph(scratch_name, normalized["graph_data"])
|
||||
assert counts == {"nodes": len(nodes), "relations": 0}
|
||||
actual = map_result(scratch_name)
|
||||
indexed = {item["id"]: item for item in actual["items"]}
|
||||
assert indexed == expected, "Imported map fields do not round-trip intact"
|
||||
|
||||
# Exercise real HTTP routing/serialization as well as direct readers.
|
||||
app = FastAPI()
|
||||
app.include_router(plaza.router, prefix="/v1/admin")
|
||||
app.dependency_overrides[get_current_user] = lambda: {"username": "local_map_test", "roles": ["admin"]}
|
||||
headers = {"X-KG-Tenant-Id": scratch_name, "X-KG-Project-Id": scratch_name,
|
||||
"X-KG-Graph-Name": scratch_name}
|
||||
with TestClient(app) as http, patch(
|
||||
"app.api.plaza.get_conn", side_effect=AssertionError("No PostgreSQL POI rows in a new import")
|
||||
):
|
||||
response = http.get("/v1/admin/plaza/map-pois", headers=headers)
|
||||
assert response.status_code == 200, response.text[:300]
|
||||
assert response.json()["total"] == len(expected)
|
||||
sample = next(iter(expected.values()))
|
||||
detail = http.get("/v1/admin/plaza/map-pois/" + sample["id"], headers=headers)
|
||||
assert detail.status_code == 200, detail.text[:300]
|
||||
data = detail.json()
|
||||
assert (data["id"], data["name"], data["lng"], data["lat"]) == (
|
||||
sample["id"], sample["name"], sample["lng"], sample["lat"])
|
||||
report["fresh_json_import"] = {
|
||||
"display_name": body["display_name"], "region": body["spatial_map"],
|
||||
"nodes": counts["nodes"], "map_pois": len(expected),
|
||||
"http_map_status": 200, "http_detail_status": 200,
|
||||
"map_payload_matches_input": True, "postgres_pois_required": False,
|
||||
}
|
||||
report["status"] = "passed"
|
||||
finally:
|
||||
try:
|
||||
if created:
|
||||
remaining = {name.decode() if isinstance(name, bytes) else str(name) for name in client.list_graphs()}
|
||||
if scratch_name in remaining:
|
||||
client.select_graph(scratch_name).delete()
|
||||
report["scratch_graph_removed"] = True
|
||||
finally:
|
||||
client.close()
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--map-json", type=Path, help="Optional exported map POI JSON used for the import sample")
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(verify(args.map_json), ensure_ascii=False, indent=2))
|
||||
Reference in new issue
Block a user