""" Tests for Canteen Admin Server CRUD endpoints. Covers: customers, locations, rooms, geofences, users, settings. Started here after these endpoints were moved from the main server (test_server.py). Run: python3 -m pytest test_admin_crud.py -v """ import json import os import threading import time from pathlib import Path import httpx import pytest DB_PATH = str(Path(__file__).parent / "assets.db") BASE_URL = None # ── Server Fixture ────────────────────────────────────────────────────────── @pytest.fixture(scope="session") def admin_server(): """Start the admin server on a random port.""" import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) port = s.getsockname()[1] import subprocess, sys 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__) or ".", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) url = f"http://127.0.0.1:{port}" for _ in range(30): time.sleep(0.2) try: resp = httpx.get(f"{url}/health", timeout=2) if resp.status_code == 200: break except Exception: continue else: proc.kill() raise RuntimeError("Admin server did not start") yield url proc.kill() proc.wait() @pytest.fixture def client(admin_server): """Return an httpx client pointed at the admin server.""" with httpx.Client(base_url=admin_server) as c: yield c # ── Helpers ───────────────────────────────────────────────────────────────── def _assert_created(resp, entity: str): """Assert 201 with an id.""" assert resp.status_code == 201, f"Create {entity}: expected 201, got {resp.status_code}: {resp.text[:200]}" data = resp.json() assert "id" in data, f"Create {entity} response missing 'id': {data}" return data["id"] def _assert_ok(resp, entity: str): """Assert 200.""" assert resp.status_code == 200, f"GET {entity}: expected 200, got {resp.status_code}: {resp.text[:200]}" return resp.json() # ─── Customers ────────────────────────────────────────────────────────────── class TestCustomers: def test_create_customer(self, client): import uuid name = f"Test Corp {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/customers", json={"name": name}), "customer") assert isinstance(cid, int) def test_create_customer_with_contacts(self, client): import uuid name = f"Test Co {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/customers", json={ "name": name, "contacts": [{"name": "Bob", "email": "bob@test.co"}] }), "customer") assert isinstance(cid, int) def test_create_customer_requires_name(self, client): resp = client.post("/api/customers", json={}) assert resp.status_code == 422 def test_list_customers(self, client): import uuid name = f"List Test {uuid.uuid4().hex[:8]}" client.post("/api/customers", json={"name": name}) data = _assert_ok(client.get("/api/customers"), "customers") assert isinstance(data, list) assert any(c["name"] == name for c in data) def test_get_customer(self, client): import uuid name = f"Get Test {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/customers", json={"name": name}), "customer") data = _assert_ok(client.get(f"/api/customers/{cid}"), "customer") assert data["name"] == name def test_get_customer_not_found(self, client): resp = client.get("/api/customers/99999") assert resp.status_code == 404 def test_update_customer(self, client): import uuid old_name = f"Old Name {uuid.uuid4().hex[:8]}" new_name = f"New Name {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/customers", json={"name": old_name}), "customer") resp = client.put(f"/api/customers/{cid}", json={"name": new_name}) assert resp.status_code == 200 data = _assert_ok(client.get(f"/api/customers/{cid}"), "customer") assert data["name"] == new_name def test_delete_customer(self, client): import uuid name = f"Delete Me {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/customers", json={"name": name}), "customer") resp = client.delete(f"/api/customers/{cid}") assert resp.status_code == 204 assert client.get(f"/api/customers/{cid}").status_code == 404 # ─── Locations ────────────────────────────────────────────────────────────── class TestLocations: def test_create_location(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"Loc Test Co {uuid.uuid4().hex[:8]}"}), "customer") lid = _assert_created(client.post("/api/locations", json={ "name": f"Warehouse A {uuid.uuid4().hex[:8]}", "customer_id": cid }), "location") assert isinstance(lid, int) def test_create_location_with_latlng(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"GPS Test Co {uuid.uuid4().hex[:8]}"}), "customer") lid = _assert_created(client.post("/api/locations", json={ "name": f"GPS Point {uuid.uuid4().hex[:8]}", "customer_id": cid, "latitude": 28.3852, "longitude": -81.5639, }), "location") data = _assert_ok(client.get(f"/api/locations/{lid}"), "location") assert data["latitude"] == 28.3852 def test_list_locations(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"List Loc Co {uuid.uuid4().hex[:8]}"}), "customer") loc_name = f"Site 1 {uuid.uuid4().hex[:8]}" client.post("/api/locations", json={"name": loc_name, "customer_id": cid}) data = _assert_ok(client.get("/api/locations"), "locations") assert isinstance(data, list) assert any(l["name"] == loc_name for l in data) def test_update_location(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"Upd Loc Co {uuid.uuid4().hex[:8]}"}), "customer") old_name = f"Old Site {uuid.uuid4().hex[:8]}" new_name = f"New Site {uuid.uuid4().hex[:8]}" lid = _assert_created(client.post("/api/locations", json={"name": old_name, "customer_id": cid}), "location") resp = client.put(f"/api/locations/{lid}", json={"name": new_name}) assert resp.status_code == 200 data = _assert_ok(client.get(f"/api/locations/{lid}"), "location") assert data["name"] == new_name def test_delete_location(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"Del Loc Co {uuid.uuid4().hex[:8]}"}), "customer") loc_name = f"Temp Site {uuid.uuid4().hex[:8]}" lid = _assert_created(client.post("/api/locations", json={"name": loc_name, "customer_id": cid}), "location") resp = client.delete(f"/api/locations/{lid}") assert resp.status_code == 204 # ─── Rooms ────────────────────────────────────────────────────────────────── class TestRooms: def test_create_room(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"Room Co {uuid.uuid4().hex[:8]}"}), "customer") lid = _assert_created(client.post("/api/locations", json={"name": f"Building 1 {uuid.uuid4().hex[:8]}", "customer_id": cid}), "location") rid = _assert_created(client.post("/api/rooms", json={ "name": f"Room 101 {uuid.uuid4().hex[:8]}", "location_id": lid }), "room") assert isinstance(rid, int) def test_list_rooms(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"List Room Co {uuid.uuid4().hex[:8]}"}), "customer") lid = _assert_created(client.post("/api/locations", json={"name": f"Bldg 2 {uuid.uuid4().hex[:8]}", "customer_id": cid}), "location") room_name = f"Rm 201 {uuid.uuid4().hex[:8]}" client.post("/api/rooms", json={"name": room_name, "location_id": lid}) data = _assert_ok(client.get("/api/rooms"), "rooms") assert isinstance(data, list) def test_list_rooms_filter_by_location(self, client): import uuid cid = _assert_created(client.post("/api/customers", json={"name": f"Filter Room Co {uuid.uuid4().hex[:8]}"}), "customer") lid1 = _assert_created(client.post("/api/locations", json={"name": f"Bldg A {uuid.uuid4().hex[:8]}", "customer_id": cid}), "location") lid2 = _assert_created(client.post("/api/locations", json={"name": f"Bldg B {uuid.uuid4().hex[:8]}", "customer_id": cid}), "location") client.post("/api/rooms", json={"name": f"Room A1 {uuid.uuid4().hex[:8]}", "location_id": lid1}) client.post("/api/rooms", json={"name": f"Room B1 {uuid.uuid4().hex[:8]}", "location_id": lid2}) data = _assert_ok(client.get(f"/api/rooms?location_id={lid1}"), "rooms by location") assert all(r["location_id"] == lid1 for r in data) # ─── Geofences ────────────────────────────────────────────────────────────── class TestGeofences: def test_create_geofence(self, client): import uuid name = f"Test Zone {uuid.uuid4().hex[:8]}" points = [[28.3852, -81.5639], [28.3852, -81.5539], [28.3752, -81.5639]] gid = _assert_created(client.post("/api/geofences", json={ "name": name, "points": points }), "geofence") assert isinstance(gid, int) def test_list_geofences(self, client): import uuid name = f"List Zone {uuid.uuid4().hex[:8]}" client.post("/api/geofences", json={"name": name, "points": [[28.38, -81.56], [28.38, -81.55], [28.37, -81.56]]}) data = _assert_ok(client.get("/api/geofences"), "geofences") assert isinstance(data, list) assert any(g["name"] == name for g in data) def test_update_geofence(self, client): import uuid old_name = f"Old Zone {uuid.uuid4().hex[:8]}" new_name = f"New Zone {uuid.uuid4().hex[:8]}" gid = _assert_created(client.post("/api/geofences", json={ "name": old_name, "points": [[28.38, -81.56], [28.38, -81.55], [28.37, -81.56]] }), "geofence") resp = client.put(f"/api/geofences/{gid}", json={"name": new_name}) assert resp.status_code == 200 def test_delete_geofence(self, client): import uuid name = f"Delete Zone {uuid.uuid4().hex[:8]}" gid = _assert_created(client.post("/api/geofences", json={ "name": name, "points": [[28.38, -81.56], [28.38, -81.55], [28.37, -81.56]] }), "geofence") resp = client.delete(f"/api/geofences/{gid}") assert resp.status_code == 204 # Verify via list (no single get endpoint — list is authoritative) data = _assert_ok(client.get("/api/geofences"), "geofences") assert not any(g["name"] == name for g in data), f"Deleted geofence {name} still in list" # ─── Settings ─────────────────────────────────────────────────────────────── class TestSettings: def test_create_category(self, client): import uuid name = f"Test Cat {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/settings/categories", json={"name": name}), "category") data = _assert_ok(client.get(f"/api/settings/categories/{cid}"), "category") assert data["name"] == name def test_list_categories(self, client): data = _assert_ok(client.get("/api/settings/categories"), "categories") assert isinstance(data, list) def test_delete_category(self, client): import uuid name = f"Delete Cat {uuid.uuid4().hex[:8]}" cid = _assert_created(client.post("/api/settings/categories", json={"name": name}), "category") resp = client.delete(f"/api/settings/categories/{cid}") assert resp.status_code == 204 # ─── Asset GPS ────────────────────────────────────────────────────────────── class TestClearAssetGps: def test_clear_gps_returns_204(self, client): """Clear GPS returns 204 and sets lat/lng to null.""" # Create an asset with GPS coords via direct DB insert import sqlite3 conn = sqlite3.connect(DB_PATH) conn.execute( "INSERT INTO assets (machine_id, name, latitude, longitude) VALUES (?, ?, ?, ?)", ("GPS-ADMIN-TEST", "Test GPS Asset", 28.5, -81.3), ) asset_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] conn.commit() conn.close() resp = client.delete(f"/api/assets/{asset_id}/gps") assert resp.status_code == 204 # Verify data = _assert_ok(client.get(f"/api/assets/{asset_id}"), "asset") assert data["latitude"] is None assert data["longitude"] is None def test_clear_gps_not_found_returns_404(self, client): resp = client.delete("/api/assets/99999/gps") assert resp.status_code == 404