""" 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/ returns 200", resp.status_code == 200, str(resp.status_code)) detail = resp.json() check("GET /batches/ has raw_data", "raw_data" in detail, str(detail.keys())) check("GET /batches/ 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()