554 lines
18 KiB
Python

from fastapi import status
from handler.database import db_device_handler
from models.device import Device
from models.user import User
class TestDeviceEndpoints:
def test_register_device(self, client, access_token: str):
response = client.post(
"/api/devices",
json={
"name": "Test Device",
"platform": "android",
"client": "argosy",
"client_version": "0.16.0",
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["name"] == "Test Device"
assert "device_id" in data
assert "created_at" in data
def test_register_device_minimal(self, client, access_token: str):
response = client.post(
"/api/devices",
json={},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["name"] is None
assert "device_id" in data
def test_list_devices(self, client, access_token: str, admin_user: User):
db_device_handler.add_device(
Device(
id="test-device-1",
user_id=admin_user.id,
name="Device 1",
)
)
db_device_handler.add_device(
Device(
id="test-device-2",
user_id=admin_user.id,
name="Device 2",
)
)
response = client.get(
"/api/devices",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) == 2
names = [d["name"] for d in data]
assert "Device 1" in names
assert "Device 2" in names
def test_get_device(self, client, access_token: str, admin_user: User):
device = db_device_handler.add_device(
Device(
id="test-device-get",
user_id=admin_user.id,
name="Get Test Device",
platform="linux",
)
)
response = client.get(
f"/api/devices/{device.id}",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == "test-device-get"
assert data["name"] == "Get Test Device"
assert data["platform"] == "linux"
def test_get_device_not_found(self, client, access_token: str):
response = client.get(
"/api/devices/nonexistent-device",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_update_device(self, client, access_token: str, admin_user: User):
device = db_device_handler.add_device(
Device(
id="test-device-update",
user_id=admin_user.id,
name="Original Name",
)
)
response = client.put(
f"/api/devices/{device.id}",
json={
"name": "Updated Name",
"platform": "android",
"client": "daijishou",
"client_version": "4.0.0",
"ip_address": "192.168.1.100",
"mac_address": "AA:BB:CC:DD:EE:FF",
"hostname": "my-odin3",
"sync_enabled": False,
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "Updated Name"
assert data["platform"] == "android"
assert data["client"] == "daijishou"
assert data["client_version"] == "4.0.0"
assert data["ip_address"] == "192.168.1.100"
assert data["mac_address"] == "AA:BB:CC:DD:EE:FF"
assert data["hostname"] == "my-odin3"
assert data["sync_enabled"] is False
def test_delete_device(self, client, access_token: str, admin_user: User):
device = db_device_handler.add_device(
Device(
id="test-device-delete",
user_id=admin_user.id,
name="To Delete",
)
)
response = client.delete(
f"/api/devices/{device.id}",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_204_NO_CONTENT
get_response = client.get(
f"/api/devices/{device.id}",
headers={"Authorization": f"Bearer {access_token}"},
)
assert get_response.status_code == status.HTTP_404_NOT_FOUND
class TestDeviceUserIsolation:
def test_list_devices_only_returns_own_devices(
self,
client,
access_token: str,
editor_access_token: str,
admin_user: User,
editor_user: User,
):
db_device_handler.add_device(
Device(id="admin-device", user_id=admin_user.id, name="Admin Device")
)
db_device_handler.add_device(
Device(id="editor-device", user_id=editor_user.id, name="Editor Device")
)
admin_response = client.get(
"/api/devices",
headers={"Authorization": f"Bearer {access_token}"},
)
assert admin_response.status_code == status.HTTP_200_OK
admin_devices = admin_response.json()
assert len(admin_devices) == 1
assert admin_devices[0]["name"] == "Admin Device"
editor_response = client.get(
"/api/devices",
headers={"Authorization": f"Bearer {editor_access_token}"},
)
assert editor_response.status_code == status.HTTP_200_OK
editor_devices = editor_response.json()
assert len(editor_devices) == 1
assert editor_devices[0]["name"] == "Editor Device"
def test_cannot_get_other_users_device(
self,
client,
editor_access_token: str,
admin_user: User,
):
device = db_device_handler.add_device(
Device(id="admin-only-device", user_id=admin_user.id, name="Admin Only")
)
response = client.get(
f"/api/devices/{device.id}",
headers={"Authorization": f"Bearer {editor_access_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_cannot_update_other_users_device(
self,
client,
editor_access_token: str,
admin_user: User,
):
device = db_device_handler.add_device(
Device(id="admin-protected-device", user_id=admin_user.id, name="Protected")
)
response = client.put(
f"/api/devices/{device.id}",
json={"name": "Hacked Name"},
headers={"Authorization": f"Bearer {editor_access_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
original = db_device_handler.get_device(
device_id=device.id, user_id=admin_user.id
)
assert original is not None
assert original.name == "Protected"
def test_cannot_delete_other_users_device(
self,
client,
editor_access_token: str,
admin_user: User,
):
device = db_device_handler.add_device(
Device(id="admin-nodelete-device", user_id=admin_user.id, name="No Delete")
)
response = client.delete(
f"/api/devices/{device.id}",
headers={"Authorization": f"Bearer {editor_access_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
still_exists = db_device_handler.get_device(
device_id=device.id, user_id=admin_user.id
)
assert still_exists is not None
class TestDeviceDuplicateHandling:
def test_duplicate_mac_address_returns_existing(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="existing-mac-device",
user_id=admin_user.id,
name="Existing Device",
mac_address="AA:BB:CC:DD:EE:FF",
)
)
response = client.post(
"/api/devices",
json={
"name": "New Device",
"mac_address": "AA:BB:CC:DD:EE:FF",
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["device_id"] == "existing-mac-device"
assert data["name"] == "Existing Device"
def test_duplicate_hostname_platform_returns_existing(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="existing-hostname-device",
user_id=admin_user.id,
name="Existing Device",
hostname="my-device",
platform="android",
)
)
response = client.post(
"/api/devices",
json={
"name": "New Device",
"hostname": "my-device",
"platform": "android",
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["device_id"] == "existing-hostname-device"
assert data["name"] == "Existing Device"
def test_duplicate_with_allow_existing_false_returns_409(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="reject-duplicate-device",
user_id=admin_user.id,
name="Existing Device",
mac_address="FF:EE:DD:CC:BB:AA",
)
)
response = client.post(
"/api/devices",
json={
"name": "New Device",
"mac_address": "FF:EE:DD:CC:BB:AA",
"allow_existing": False,
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_409_CONFLICT
data = response.json()["detail"]
assert data["error"] == "device_exists"
assert data["device_id"] == "reject-duplicate-device"
def test_allow_existing_returns_existing_device(
self, client, access_token: str, admin_user: User
):
existing = db_device_handler.add_device(
Device(
id="allow-existing-device",
user_id=admin_user.id,
name="Existing Device",
mac_address="11:22:33:44:55:66",
)
)
response = client.post(
"/api/devices",
json={
"name": "New Device Name",
"mac_address": "11:22:33:44:55:66",
"allow_existing": True,
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["device_id"] == existing.id
assert data["name"] == "Existing Device"
def test_allow_existing_with_reset_syncs(
self, client, access_token: str, admin_user: User, rom
):
from handler.database import db_device_save_sync_handler, db_save_handler
from models.assets import Save
existing = db_device_handler.add_device(
Device(
id="reset-syncs-device",
user_id=admin_user.id,
name="Device With Syncs",
mac_address="77:88:99:AA:BB:CC",
)
)
save = db_save_handler.add_save(
Save(
file_name="test.sav",
file_name_no_tags="test",
file_name_no_ext="test",
file_extension="sav",
file_path="/saves",
file_size_bytes=100,
rom_id=rom.id,
user_id=admin_user.id,
)
)
db_device_save_sync_handler.upsert_sync(device_id=existing.id, save_id=save.id)
sync_before = db_device_save_sync_handler.get_sync(
device_id=existing.id, save_id=save.id
)
assert sync_before is not None
response = client.post(
"/api/devices",
json={
"mac_address": "77:88:99:AA:BB:CC",
"allow_existing": True,
"reset_syncs": True,
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["device_id"] == existing.id
sync_after = db_device_save_sync_handler.get_sync(
device_id=existing.id, save_id=save.id
)
assert sync_after is None
def test_allow_duplicate_creates_new_device(
self, client, access_token: str, admin_user: User
):
existing = db_device_handler.add_device(
Device(
id="original-device",
user_id=admin_user.id,
name="Original Device",
mac_address="DD:EE:FF:00:11:22",
)
)
response = client.post(
"/api/devices",
json={
"name": "Duplicate Install",
"mac_address": "DD:EE:FF:00:11:22",
"allow_duplicate": True,
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["device_id"] != existing.id
assert data["name"] == "Duplicate Install"
def test_no_conflict_without_fingerprint(self, client, access_token: str):
response1 = client.post(
"/api/devices",
json={"name": "Device 1"},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response1.status_code == status.HTTP_201_CREATED
response2 = client.post(
"/api/devices",
json={"name": "Device 2"},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response2.status_code == status.HTTP_201_CREATED
assert response1.json()["device_id"] != response2.json()["device_id"]
def test_hostname_only_no_conflict_without_platform(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="hostname-only-device",
user_id=admin_user.id,
name="Existing",
hostname="my-device",
)
)
response = client.post(
"/api/devices",
json={
"name": "New Device",
"hostname": "my-device",
},
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_201_CREATED
class TestSyncConfigMasking:
def test_ssh_credentials_masked_in_response(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="mask-dev-1",
user_id=admin_user.id,
name="SSH Device",
sync_config={
"ssh_host": "192.168.1.100",
"ssh_port": 22,
"ssh_username": "retro",
"ssh_password": "supersecret123",
"ssh_key_path": "/home/retro/.ssh/id_rsa",
"save_directories": [
{"platform_slug": "gba", "path": "/saves/gba"}
],
},
)
)
response = client.get(
"/api/devices/mask-dev-1",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
config = response.json()["sync_config"]
assert config["ssh_host"] == "192.168.1.100"
assert config["ssh_port"] == 22
assert config["ssh_username"] == "retro"
assert config["ssh_password"] == "********"
assert config["ssh_key_path"] == "********"
assert config["save_directories"] == [
{"platform_slug": "gba", "path": "/saves/gba"}
]
def test_null_sync_config_passes_through(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="mask-dev-2",
user_id=admin_user.id,
name="No Config Device",
)
)
response = client.get(
"/api/devices/mask-dev-2",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["sync_config"] is None
def test_sync_config_without_sensitive_fields(
self, client, access_token: str, admin_user: User
):
db_device_handler.add_device(
Device(
id="mask-dev-3",
user_id=admin_user.id,
sync_config={"ssh_host": "10.0.0.1", "ssh_port": 2222},
)
)
response = client.get(
"/api/devices/mask-dev-3",
headers={"Authorization": f"Bearer {access_token}"},
)
assert response.status_code == status.HTTP_200_OK
config = response.json()["sync_config"]
assert config["ssh_host"] == "10.0.0.1"
assert config["ssh_port"] == 2222