Files
romm/backend/tests/handler/database/test_device_save_sync_handler.py
nendo 287c487308 feat(saves): expose per-device sync attribution and origin device
saves responses now include one device_syncs entry per device that has
synced a save, not just the caller's, so clients can tell which devices
hold a save. is_current is computed per entry and the caller's own entry
is ordered first for backward compatibility.

add a saves.origin_device_id column (migration 0081) recording the
device that created a save, set on initial upload only, surfaced as
origin_device_id on the save schema.
2026-06-05 20:25:01 +09:00

272 lines
10 KiB
Python

from datetime import datetime, timezone
from handler.database import (
db_device_handler,
db_device_save_sync_handler,
db_save_handler,
)
from models.assets import Save
from models.device import Device
from models.rom import Rom
from models.user import User
class TestGetSync:
def test_get_existing_sync(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="sync-dev-1", user_id=admin_user.id)
)
db_device_save_sync_handler.upsert_sync(device.id, save.id)
result = db_device_save_sync_handler.get_sync(device.id, save.id)
assert result is not None
assert result.device_id == device.id
assert result.save_id == save.id
def test_get_nonexistent_sync(self, admin_user: User):
result = db_device_save_sync_handler.get_sync("no-device", 999)
assert result is None
class TestGetSyncsForDeviceAndSaves:
def test_returns_matching_syncs(self, admin_user: User, rom: Rom):
device = db_device_handler.add_device(
Device(id="bulk-dev-1", user_id=admin_user.id)
)
saves = []
for i in range(3):
s = db_save_handler.add_save(
Save(
rom_id=rom.id,
user_id=admin_user.id,
file_name=f"bulk_{i}.sav",
file_name_no_tags=f"bulk_{i}",
file_name_no_ext=f"bulk_{i}",
file_extension="sav",
emulator="emu",
file_path=f"{rom.platform_slug}/saves",
file_size_bytes=100,
)
)
saves.append(s)
db_device_save_sync_handler.upsert_sync(device.id, s.id)
result = db_device_save_sync_handler.get_syncs_for_device_and_saves(
device.id, [saves[0].id, saves[2].id]
)
assert len(result) == 2
ids = {r.save_id for r in result}
assert saves[0].id in ids
assert saves[2].id in ids
def test_empty_save_ids_returns_empty(self, admin_user: User):
result = db_device_save_sync_handler.get_syncs_for_device_and_saves(
"any-dev", []
)
assert result == []
class TestGetSyncsForSaves:
def test_returns_syncs_across_devices_with_name(
self, admin_user: User, rom: Rom, save: Save
):
dev_a = db_device_handler.add_device(
Device(id="multi-dev-a", user_id=admin_user.id, name="Device A")
)
dev_b = db_device_handler.add_device(
Device(id="multi-dev-b", user_id=admin_user.id, name="Device B")
)
db_device_save_sync_handler.upsert_sync(dev_a.id, save.id)
db_device_save_sync_handler.upsert_sync(dev_b.id, save.id)
result = db_device_save_sync_handler.get_syncs_for_saves([save.id])
assert set(result.keys()) == {save.id}
by_device = {sync.device_id: name for sync, name in result[save.id]}
assert by_device == {dev_a.id: "Device A", dev_b.id: "Device B"}
def test_filters_to_requested_saves(self, admin_user: User, rom: Rom):
device = db_device_handler.add_device(
Device(id="multi-dev-c", user_id=admin_user.id, name="Device C")
)
saves = []
for i in range(3):
s = db_save_handler.add_save(
Save(
rom_id=rom.id,
user_id=admin_user.id,
file_name=f"multi_{i}.sav",
file_name_no_tags=f"multi_{i}",
file_name_no_ext=f"multi_{i}",
file_extension="sav",
emulator="emu",
file_path=f"{rom.platform_slug}/saves",
file_size_bytes=100,
)
)
saves.append(s)
db_device_save_sync_handler.upsert_sync(device.id, s.id)
result = db_device_save_sync_handler.get_syncs_for_saves(
[saves[0].id, saves[2].id]
)
assert set(result.keys()) == {saves[0].id, saves[2].id}
def test_empty_save_ids_returns_empty(self, admin_user: User):
result = db_device_save_sync_handler.get_syncs_for_saves([])
assert result == {}
class TestOriginDeviceCascade:
def test_origin_device_id_nulled_on_device_delete(self, admin_user: User, rom: Rom):
device = db_device_handler.add_device(
Device(id="origin-dev", user_id=admin_user.id, name="Origin")
)
save = db_save_handler.add_save(
Save(
rom_id=rom.id,
user_id=admin_user.id,
file_name="origin_cascade.sav",
file_name_no_tags="origin_cascade",
file_name_no_ext="origin_cascade",
file_extension="sav",
emulator="emu",
file_path=f"{rom.platform_slug}/saves",
file_size_bytes=100,
origin_device_id=device.id,
)
)
assert save.origin_device_id == device.id
db_device_handler.delete_device(device.id, admin_user.id)
refreshed = db_save_handler.get_save(user_id=admin_user.id, id=save.id)
assert refreshed is not None
assert refreshed.origin_device_id is None
class TestUpsertSync:
def test_creates_new_sync(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="upsert-dev-1", user_id=admin_user.id)
)
result = db_device_save_sync_handler.upsert_sync(device.id, save.id)
assert result.device_id == device.id
assert result.save_id == save.id
assert result.is_untracked is False
assert result.last_synced_at is not None
def test_updates_existing_sync(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="upsert-dev-2", user_id=admin_user.id)
)
earlier = datetime(2020, 1, 1, tzinfo=timezone.utc)
db_device_save_sync_handler.upsert_sync(device.id, save.id, synced_at=earlier)
later = datetime(2025, 6, 1, tzinfo=timezone.utc)
result = db_device_save_sync_handler.upsert_sync(
device.id, save.id, synced_at=later
)
assert result.last_synced_at == later
assert result.is_untracked is False
def test_upsert_clears_untracked(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="upsert-dev-3", user_id=admin_user.id)
)
db_device_save_sync_handler.set_untracked(device.id, save.id, True)
sync = db_device_save_sync_handler.get_sync(device.id, save.id)
assert sync is not None
assert sync.is_untracked is True
db_device_save_sync_handler.upsert_sync(device.id, save.id)
sync = db_device_save_sync_handler.get_sync(device.id, save.id)
assert sync is not None
assert sync.is_untracked is False
def test_upsert_with_custom_timestamp(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="upsert-dev-4", user_id=admin_user.id)
)
ts = datetime(2024, 3, 15, 12, 0, 0, tzinfo=timezone.utc)
result = db_device_save_sync_handler.upsert_sync(
device.id, save.id, synced_at=ts
)
assert result.last_synced_at == ts
class TestSetUntracked:
def test_set_untracked_on_existing(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="untrack-dev-1", user_id=admin_user.id)
)
db_device_save_sync_handler.upsert_sync(device.id, save.id)
result = db_device_save_sync_handler.set_untracked(device.id, save.id, True)
assert result is not None
assert result.is_untracked is True
def test_set_tracked_on_existing(self, admin_user: User, rom: Rom, save: Save):
device = db_device_handler.add_device(
Device(id="untrack-dev-2", user_id=admin_user.id)
)
db_device_save_sync_handler.upsert_sync(device.id, save.id)
db_device_save_sync_handler.set_untracked(device.id, save.id, True)
result = db_device_save_sync_handler.set_untracked(device.id, save.id, False)
assert result is not None
assert result.is_untracked is False
def test_set_untracked_creates_new_record(
self, admin_user: User, rom: Rom, save: Save
):
device = db_device_handler.add_device(
Device(id="untrack-dev-3", user_id=admin_user.id)
)
result = db_device_save_sync_handler.set_untracked(device.id, save.id, True)
assert result is not None
assert result.is_untracked is True
assert result.device_id == device.id
def test_set_tracked_nonexistent_returns_none(self, admin_user: User):
device = db_device_handler.add_device(
Device(id="untrack-dev-4", user_id=admin_user.id)
)
result = db_device_save_sync_handler.set_untracked(device.id, 999999, False)
assert result is None
class TestDeleteSyncsForDevice:
def test_deletes_all_syncs(self, admin_user: User, rom: Rom):
device = db_device_handler.add_device(
Device(id="del-dev-1", user_id=admin_user.id)
)
for i in range(3):
s = db_save_handler.add_save(
Save(
rom_id=rom.id,
user_id=admin_user.id,
file_name=f"del_{i}.sav",
file_name_no_tags=f"del_{i}",
file_name_no_ext=f"del_{i}",
file_extension="sav",
emulator="emu",
file_path=f"{rom.platform_slug}/saves",
file_size_bytes=100,
)
)
db_device_save_sync_handler.upsert_sync(device.id, s.id)
db_device_save_sync_handler.delete_syncs_for_device(device.id)
result = db_device_save_sync_handler.get_syncs_for_device_and_saves(
device.id, [1, 2, 3]
)
assert len(result) == 0
def test_delete_nonexistent_device_no_error(self):
db_device_save_sync_handler.delete_syncs_for_device("nonexistent-device")