birdie / tests /test_api.py
Affaad's picture
Deploy Birdie b55b931
1f0fbfd verified
Raw History Blame Contribute Delete
5.15 kB
from datetime import datetime
from fastapi.testclient import TestClient
from birdie.api import _counts, _status, create_app, mount_mobile_web
from birdie.models import Seat, SeatSnapshot, SeatState, Venue, VenueConfig, VenueSource, VenueType
from birdie.service import BirdieService
from birdie.storage import Repository
def test_public_api_reports_unknown_seats_as_not_free_and_hides_camera_polygons(clock):
venue = Venue(
venue_id="v", name="Venue", area="Doha", type=VenueType.cowork,
lat=0, lon=0, zones=["open"], source=VenueSource.recorded,
)
seat = Seat(
seat_id="A", venue_id="v", zone="open",
polygon=[(0, 0), (1, 0), (1, 1)],
)
svc = BirdieService(
repo=Repository(":memory:"), clock=clock,
venues={"v": VenueConfig(venue=venue, seats=[seat])},
detector=lambda image, threshold: [], persist_venues=False,
)
client = TestClient(create_app(svc))
listed = client.get("/api/venues").json()[0]
detail = client.get("/api/venues/v").json()
assert listed["counts"]["free"] == 0
assert listed["counts"]["unknown"] == 1
assert listed["counts"]["taken"] == 0
assert detail["seats"][0]["state"] == SeatState.UNKNOWN.value
assert detail["seats"][0]["status"] == "checking"
assert "polygon" not in detail["seats"][0]
assert client.get("/api/venues/missing").status_code == 404
def test_public_availability_has_three_groups_and_a_checking_indicator():
assert {state: _status(state) for state in SeatState} == {
SeatState.UNKNOWN: "checking", SeatState.FREE: "free", SeatState.OCCUPIED: "taken",
SeatState.HELD: "held", SeatState.REVIEW_REQUIRED: "held", SeatState.UNAVAILABLE: "taken",
}
now = datetime.fromisoformat("2026-11-07T12:00:00+00:00")
states = [SeatState.FREE, SeatState.OCCUPIED, SeatState.UNAVAILABLE, SeatState.HELD,
SeatState.REVIEW_REQUIRED, SeatState.UNKNOWN]
snapshots = [SeatSnapshot(venue_id="v", seat_id=str(i), zone="open", has_outlet=False,
state=state, since=now, updated_at=now) for i, state in enumerate(states)]
counts = _counts(snapshots)
assert (counts["free"], counts["taken"], counts["held_or_review"], counts["checking"]) == (1, 2, 2, 1)
assert counts["free"] + counts["taken"] + counts["held_or_review"] + counts["checking"] == counts["total"]
def test_mobile_web_is_served_after_api_routes(clock):
venue = Venue(
venue_id="v", name="Venue", area="Doha", type=VenueType.cowork,
lat=0, lon=0, zones=["open"], source=VenueSource.recorded,
)
svc = BirdieService(
repo=Repository(":memory:"), clock=clock,
venues={"v": VenueConfig(venue=venue, seats=[])}, persist_venues=False,
detector=lambda image, threshold: [],
)
app = create_app(svc)
mount_mobile_web(app)
client = TestClient(app)
assert "Birdie" in client.get("/").text
assert client.get("/api/venues").status_code == 200
def test_staff_override_requires_token_and_changes_availability(clock, monkeypatch):
venue = Venue(
venue_id="v", name="Venue", area="Doha", type=VenueType.cowork,
lat=0, lon=0, zones=["open"], source=VenueSource.recorded,
)
seat = Seat(seat_id="A", venue_id="v", zone="open")
svc = BirdieService(
repo=Repository(":memory:"), clock=clock,
venues={"v": VenueConfig(venue=venue, seats=[seat])},
detector=lambda image, threshold: [], persist_venues=False,
)
client = TestClient(create_app(svc))
monkeypatch.setenv("BIRDIE_ADMIN_TOKEN", "test-token")
response = client.put("/api/venues/v/seats/A/unavailable", json={"unavailable": True})
assert response.status_code == 401
response = client.put(
"/api/venues/v/seats/A/unavailable", json={"unavailable": True},
headers={"Authorization": "Bearer test-token"},
)
assert response.status_code == 200
assert response.json()["state"] == SeatState.UNAVAILABLE.value
assert svc.counts("v")[SeatState.FREE] == 0
def test_forecast_starts_from_the_live_count_and_covers_three_hours(clock):
venue = Venue(
venue_id="v", name="Venue", area="Doha", type=VenueType.library,
lat=0, lon=0, zones=["open"], source=VenueSource.simulated, drive_minutes=15,
)
seats = [Seat(seat_id=f"A{i}", venue_id="v", zone="open") for i in range(1, 9)]
svc = BirdieService(
repo=Repository(":memory:"), clock=clock,
venues={"v": VenueConfig(venue=venue, seats=seats)},
detector=lambda image, threshold: [], persist_venues=False,
)
client = TestClient(create_app(svc))
listed = client.get("/api/venues").json()[0]["forecast"]
detail = client.get("/api/venues/v").json()
timeline = detail["forecast"]["timeline"]
assert listed["arrival"]["minutes"] == 15
assert "timeline" not in listed
assert [p["minutes"] for p in timeline] == [0, 30, 60, 90, 120, 150, 180]
assert timeline[0]["expected_free"] == detail["counts"]["usable_study_spaces"]
assert all(0 <= p["low"] <= p["expected_free"] <= p["high"] <= 8 for p in timeline)