9794d8076c
- DELETE /api/assets/{id}/gps: clears lat/lng to NULL
- GET /api/assets/{id}: asset lookup by ID
- GET /api/assets/search?machine_id=: asset lookup by machine ID
- Export & Admin page: Clear Asset GPS card with lookup + confirmation
- Tests: 204 success + 404 not found
320 lines
14 KiB
Python
320 lines
14 KiB
Python
"""
|
|
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
|