"""Mobile / Seaca API.""" from datetime import datetime, timedelta, timezone from app.models.mobile_device import MobileDevice from app.models.mobile_enrollment_code import MobileEnrollmentCode from app.models.mobile_settings import MOBILE_SETTINGS_ROW_ID, MobileSettings from app.services.mobile_tokens import hash_mobile_secret def _enable_mobile(db_session): row = db_session.get(MobileSettings, MOBILE_SETTINGS_ROW_ID) if row is None: row = MobileSettings(id=MOBILE_SETTINGS_ROW_ID, devices_allowed=True, max_devices_per_user=3) db_session.add(row) else: row.devices_allowed = True db_session.commit() def test_mobile_settings_admin(jwt_headers, client, db_session): _enable_mobile(db_session) response = client.get("/api/v1/mobile/settings", headers=jwt_headers) assert response.status_code == 200 body = response.json() assert body["devices_allowed"] is True assert body["max_devices_per_user"] == 3 def test_create_enrollment_code_and_enroll(jwt_headers, client, db_session): _enable_mobile(db_session) create = client.post( "/api/v1/mobile/enrollment-codes", headers=jwt_headers, json={"label": "test phone", "login_mode": "password", "expires_in_hours": 1}, ) assert create.status_code == 200 created = create.json() assert created["enrollment_code"].startswith("sacmob_") assert created["code_prefix"] enroll = client.post( "/api/v1/mobile/enroll", json={ "enrollment_code": created["enrollment_code"], "username": "test-monitor", "password": "test-monitor-password", "device_uuid": "uuid-test-device-001", "display_name": "Pixel Test", "fcm_token": "fcm-token-test", }, ) assert enroll.status_code == 200 body = enroll.json() assert body["username"] == "test-monitor" assert body["role"] == "monitor" assert body["device_id"] > 0 assert body["access_token"] assert body["refresh_token"] devices = client.get("/api/v1/mobile/devices", headers=jwt_headers) assert devices.status_code == 200 assert len(devices.json()) == 1 assert devices.json()[0]["display_name"] == "Pixel Test" def test_enroll_blocked_when_disabled(client, db_session, jwt_headers): code_plain = "sacmob_testcode123456" db_session.add( MobileEnrollmentCode( label="x", code_hash=hash_mobile_secret(code_plain), code_prefix=code_plain[:12], login_mode="password", max_uses=1, use_count=0, expires_at=datetime.now(timezone.utc) + timedelta(hours=1), ) ) db_session.commit() response = client.post( "/api/v1/mobile/enroll", json={ "enrollment_code": code_plain, "username": "test-monitor", "password": "test-monitor-password", "device_uuid": "uuid-disabled-001", }, ) assert response.status_code == 403 def test_refresh_token_rotation(client, db_session, jwt_headers): _enable_mobile(db_session) create = client.post( "/api/v1/mobile/enrollment-codes", headers=jwt_headers, json={"login_mode": "password"}, ) code = create.json()["enrollment_code"] enroll = client.post( "/api/v1/mobile/enroll", json={ "enrollment_code": code, "username": "test-admin", "password": "test-admin-password", "device_uuid": "uuid-refresh-001", }, ) data = enroll.json() refresh = client.post( "/api/v1/mobile/auth/refresh", json={"refresh_token": data["refresh_token"], "device_uuid": "uuid-refresh-001"}, ) assert refresh.status_code == 200 refreshed = refresh.json() assert refreshed["refresh_token"] != data["refresh_token"] assert refreshed["device_id"] == data["device_id"] def test_revoke_device_blocks_token(client, db_session, jwt_headers): _enable_mobile(db_session) create = client.post( "/api/v1/mobile/enrollment-codes", headers=jwt_headers, json={"login_mode": "password"}, ) code = create.json()["enrollment_code"] enroll = client.post( "/api/v1/mobile/enroll", json={ "enrollment_code": code, "username": "test-monitor", "password": "test-monitor-password", "device_uuid": "uuid-revoke-001", }, ) data = enroll.json() headers = {"Authorization": f"Bearer {data['access_token']}"} revoke = client.delete(f"/api/v1/mobile/devices/{data['device_id']}", headers=jwt_headers) assert revoke.status_code == 200 me = client.put( "/api/v1/mobile/devices/me/fcm", headers=headers, json={"fcm_token": "new-token"}, ) assert me.status_code == 401