f7ffa0e6cc
- Fix greedy matching where 'name' keyword matched 'Location Name' header - Use word-subset matching instead of substring matching - Normalize keywords (replace _ and # with spaces) to match header normalization - Reorder heuristics: multi-word specific patterns before single-word generics - Add 10 unit tests for mapping, diff, and Excel parsing - Add 23 integration tests for all sync API endpoints - All 33 tests pass
213 lines
7.7 KiB
Python
213 lines
7.7 KiB
Python
"""
|
|
Test script for Cantaloupe sync API.
|
|
Runs the admin server on a random port, creates test Excel data,
|
|
and tests all endpoints.
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import openpyxl
|
|
import httpx
|
|
|
|
DB_PATH = str(Path(__file__).parent / "assets.db")
|
|
|
|
|
|
def create_test_excel(path: str):
|
|
"""Create a test Cantaloupe Excel export file."""
|
|
wb = openpyxl.Workbook()
|
|
ws = wb.active
|
|
ws.append([
|
|
"Asset ID", "Name", "Serial Number", "Customer", "Location Name",
|
|
"Make", "Model", "Status", "Category", "Address"
|
|
])
|
|
ws.append([
|
|
"MACH001", "Coffee Machine 1", "SN001", "Acme Corp", "Building A",
|
|
"Canteen", "CM2000", "active", "Appliances", "123 Main St"
|
|
])
|
|
ws.append([
|
|
"MACH002", "Fridge Unit 2", "SN002", "Acme Corp", "Building B",
|
|
"Hobart", "HF500", "active", "Appliances", "456 Oak Ave"
|
|
])
|
|
wb.save(path)
|
|
print(f" Created test Excel with {ws.max_row - 1} data rows")
|
|
|
|
|
|
def run_server(port: int, ready_event: threading.Event):
|
|
"""Run the admin server in a subprocess."""
|
|
env = os.environ.copy()
|
|
env["CANTEEN_SKIP_AUTH"] = "1"
|
|
env["CANTEEN_DB_PATH"] = DB_PATH
|
|
|
|
proc = subprocess.Popen(
|
|
[sys.executable, "-m", "uvicorn", "admin_server:app",
|
|
"--host", "127.0.0.1", "--port", str(port)],
|
|
env=env,
|
|
cwd=os.path.dirname(__file__),
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
# Wait for server to be ready
|
|
for _ in range(30):
|
|
time.sleep(0.2)
|
|
try:
|
|
resp = httpx.get(f"http://127.0.0.1:{port}/health", timeout=2)
|
|
if resp.status_code == 200:
|
|
ready_event.set()
|
|
return proc, port
|
|
except Exception:
|
|
pass
|
|
proc.kill()
|
|
raise RuntimeError("Server did not start")
|
|
|
|
|
|
def run_tests(base_url: str, excel_path: str):
|
|
"""Test all Cantaloupe sync API endpoints."""
|
|
passed = 0
|
|
failed = 0
|
|
|
|
def check(name: str, condition: bool, detail: str = ""):
|
|
nonlocal passed, failed
|
|
if condition:
|
|
print(f" ✓ {name}")
|
|
passed += 1
|
|
else:
|
|
print(f" ✗ {name} — {detail}")
|
|
failed += 1
|
|
|
|
# Clean up any leftover test data from previous runs
|
|
import sqlite3
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.execute("DELETE FROM cantaloupe_sync_batches")
|
|
conn.execute("DELETE FROM assets WHERE machine_id IN ('MACH001', 'MACH002')")
|
|
conn.execute("DELETE FROM customers WHERE name = 'Acme Corp'")
|
|
conn.execute("DELETE FROM locations")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# 1. List batches (should be empty now)
|
|
resp = httpx.get(f"{base_url}/api/admin/cantaloupe/batches")
|
|
check("GET /batches returns 200", resp.status_code == 200, str(resp.status_code))
|
|
batches = resp.json()
|
|
check("GET /batches returns list", isinstance(batches, list), str(type(batches)))
|
|
check("GET /batches is empty initially", len(batches) == 0, f"count={len(batches)}")
|
|
|
|
# 2. Run sync
|
|
resp = httpx.post(
|
|
f"{base_url}/api/admin/cantaloupe/sync",
|
|
params={"file_path": excel_path},
|
|
)
|
|
check("POST /sync returns 200", resp.status_code == 200, f"{resp.status_code}: {resp.text[:200]}")
|
|
sync_result = resp.json()
|
|
check("POST /sync has id", "id" in sync_result, str(sync_result.keys()))
|
|
check("POST /sync status=pending", sync_result.get("status") == "pending", sync_result.get("status"))
|
|
check("POST /sync has diff_summary", "diff_summary" in sync_result, str(sync_result.keys()))
|
|
|
|
diff = sync_result.get("diff_summary", {})
|
|
print(f" Diff summary: {json.dumps({k: v for k, v in diff.items() if k != 'raw_data'}, default=str)[:200]}")
|
|
|
|
batch_id = sync_result.get("id")
|
|
assert batch_id, "No batch id in sync result"
|
|
|
|
# 3. Get batch detail
|
|
resp = httpx.get(f"{base_url}/api/admin/cantaloupe/batches/{batch_id}")
|
|
check("GET /batches/<id> returns 200", resp.status_code == 200, str(resp.status_code))
|
|
detail = resp.json()
|
|
check("GET /batches/<id> has raw_data", "raw_data" in detail, str(detail.keys()))
|
|
check("GET /batches/<id> has diff_rows", "diff_rows" in detail, str(detail.keys()))
|
|
|
|
# 4. List batches again (should have 1)
|
|
resp = httpx.get(f"{base_url}/api/admin/cantaloupe/batches")
|
|
check("GET /batches has 1 item after sync", len(resp.json()) == 1, f"count={len(resp.json())}")
|
|
|
|
# 5. Approve batch
|
|
resp = httpx.post(f"{base_url}/api/admin/cantaloupe/batches/{batch_id}/approve")
|
|
check("POST /approve returns 200", resp.status_code == 200, f"{resp.status_code}: {resp.text[:200]}")
|
|
approve_result = resp.json()
|
|
check("POST /approve status=approved", approve_result.get("status") == "approved", approve_result.get("status"))
|
|
check("POST /approve has approved_at", bool(approve_result.get("approved_at")), "missing approved_at")
|
|
check("POST /approve has applied_stats", "applied_stats" in approve_result, str(approve_result.keys()))
|
|
if "applied_stats" in approve_result:
|
|
stats = approve_result["applied_stats"]
|
|
print(f" Applied: {stats}")
|
|
|
|
# 6. Verify customers were created (via API)
|
|
resp = httpx.get(f"{base_url}/api/customers")
|
|
if resp.status_code == 200:
|
|
customers = resp.json()
|
|
names = [c.get("name") for c in customers]
|
|
check("Customer 'Acme Corp' created", any("Acme Corp" in str(n) for n in names), str(names))
|
|
|
|
# 7. Verify locations were created (via API)
|
|
resp = httpx.get(f"{base_url}/api/locations")
|
|
if resp.status_code == 200:
|
|
locations = resp.json()
|
|
loc_names = [l.get("name") for l in locations]
|
|
check("Location created from sync", len(locations) >= 2, f"count={len(locations)}")
|
|
|
|
# 9. Re-approve should fail (already approved)
|
|
resp = httpx.post(f"{base_url}/api/admin/cantaloupe/batches/{batch_id}/approve")
|
|
check("Re-approve rejected (status != pending)", resp.status_code == 400, f"got {resp.status_code}")
|
|
|
|
# 10. Create another batch and reject it
|
|
resp = httpx.post(
|
|
f"{base_url}/api/admin/cantaloupe/sync",
|
|
params={"file_path": excel_path},
|
|
)
|
|
batch2_id = resp.json().get("id")
|
|
check("Second sync batch created", batch2_id is not None, str(resp.json())[:100])
|
|
|
|
if batch2_id:
|
|
resp = httpx.post(f"{base_url}/api/admin/cantaloupe/batches/{batch2_id}/reject")
|
|
check("POST /reject returns 200", resp.status_code == 200, f"{resp.status_code}: {resp.text[:200]}")
|
|
reject_result = resp.json()
|
|
check("Rejected status=rejected", reject_result.get("status") == "rejected", reject_result.get("status"))
|
|
check("Rejected raw_data cleared", reject_result.get("raw_data") is None, str(reject_result.get("raw_data"))[:50])
|
|
|
|
# 11. Test 404 for non-existent batch
|
|
resp = httpx.get(f"{base_url}/api/admin/cantaloupe/batches/99999")
|
|
check("GET /batches/99999 returns 404", resp.status_code == 404, str(resp.status_code))
|
|
|
|
print(f"\nResults: {passed} passed, {failed} failed")
|
|
return failed == 0
|
|
|
|
|
|
def main():
|
|
print("=== Cantaloupe Sync API Tests ===\n")
|
|
|
|
excel_path = "/tmp/test_cantaloupe_api.xlsx"
|
|
create_test_excel(excel_path)
|
|
|
|
# Find a free port
|
|
import socket
|
|
sock = socket.socket()
|
|
sock.bind(('', 0))
|
|
port = sock.getsockname()[1]
|
|
sock.close()
|
|
|
|
base_url = f"http://127.0.0.1:{port}"
|
|
print(f"Starting server on port {port}...")
|
|
|
|
ready = threading.Event()
|
|
proc, _ = run_server(port, ready)
|
|
|
|
try:
|
|
ready.wait(timeout=10)
|
|
print(f"Server ready at {base_url}\n")
|
|
success = run_tests(base_url, excel_path)
|
|
finally:
|
|
proc.terminate()
|
|
proc.wait(timeout=5)
|
|
print("\nServer stopped.")
|
|
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|