"""Tests for core/ports.py — storage interfaces.""" import pytest from core.models import Bounty, RoomData, TrackingData, TrackedBounty from core.ports import RoomStorage, TrackingStorage class SimpleRoomStorage: """Minimal mock without ensure_room - tests if add_bounty works without it. This mock only has the basic CRUD methods. It does NOT implement ensure_room(). If add_bounty() still works with this simple mock, then ensure_room() may not be needed as a public Protocol method. """ def __init__(self): self._rooms: dict[int, RoomData] = {} def load(self, room_id: int) -> RoomData | None: return self._rooms.get(room_id) def save(self, room_data: RoomData) -> None: self._rooms[room_data.room_id] = room_data def add_bounty(self, room_id: int, bounty: Bounty) -> None: if room_id not in self._rooms: self._rooms[room_id] = RoomData(room_id=room_id, bounties=[], next_id=1) self._rooms[room_id].bounties.append(bounty) def update_bounty(self, room_id: int, bounty: Bounty) -> None: if room_id in self._rooms: for i, b in enumerate(self._rooms[room_id].bounties): if b.id == bounty.id: self._rooms[room_id].bounties[i] = bounty break def delete_bounty(self, room_id: int, bounty_id: int) -> None: if room_id in self._rooms: self._rooms[room_id].bounties = [ b for b in self._rooms[room_id].bounties if b.id != bounty_id ] def get_bounty(self, room_id: int, bounty_id: int) -> Bounty | None: if room_id in self._rooms: for b in self._rooms[room_id].bounties: if b.id == bounty_id: return b return None def list_bounties(self, room_id: int) -> list[Bounty]: if room_id not in self._rooms: return [] return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] def list_all_bounties( self, room_id: int, include_deleted: bool = True ) -> list[Bounty]: if room_id not in self._rooms: return [] if include_deleted: return self._rooms[room_id].bounties return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] class SimpleTrackingStorage: """Minimal mock without ensure_tracking - tests if track_bounty works without it. This mock only has the basic methods. It does NOT implement ensure_tracking(). If track_bounty() still works with this simple mock, then ensure_tracking() may not be needed as a public Protocol method. """ def __init__(self): self._tracking: dict[tuple[int, int], TrackingData] = {} def load(self, room_id: int, user_id: int) -> TrackingData | None: return self._tracking.get((room_id, user_id)) def save(self, tracking_data: TrackingData) -> None: self._tracking[(tracking_data.room_id, tracking_data.user_id)] = tracking_data def track_bounty(self, room_id: int, user_id: int, tracked: TrackedBounty) -> None: key = (room_id, user_id) if key not in self._tracking: self._tracking[key] = TrackingData( room_id=room_id, user_id=user_id, tracked=[] ) self._tracking[key].tracked.append(tracked) def untrack_bounty(self, room_id: int, user_id: int, bounty_id: int) -> None: key = (room_id, user_id) if key in self._tracking: self._tracking[key].tracked = [ t for t in self._tracking[key].tracked if t.bounty_id != bounty_id ] class MockRoomStorage: """Mock implementation of RoomStorage for testing.""" def __init__(self): self._rooms: dict[int, RoomData] = {} def load(self, room_id: int) -> RoomData | None: return self._rooms.get(room_id) def save(self, room_data: RoomData) -> None: self._rooms[room_data.room_id] = room_data def add_bounty(self, room_id: int, bounty: Bounty) -> None: if room_id not in self._rooms: self._rooms[room_id] = RoomData(room_id=room_id, bounties=[], next_id=1) self._rooms[room_id].bounties.append(bounty) def update_bounty(self, room_id: int, bounty: Bounty) -> None: if room_id in self._rooms: for i, b in enumerate(self._rooms[room_id].bounties): if b.id == bounty.id: self._rooms[room_id].bounties[i] = bounty break def delete_bounty(self, room_id: int, bounty_id: int) -> None: if room_id in self._rooms: self._rooms[room_id].bounties = [ b for b in self._rooms[room_id].bounties if b.id != bounty_id ] def get_bounty(self, room_id: int, bounty_id: int) -> Bounty | None: if room_id in self._rooms: for b in self._rooms[room_id].bounties: if b.id == bounty_id: return b return None def list_bounties(self, room_id: int) -> list[Bounty]: if room_id not in self._rooms: return [] return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] def list_all_bounties( self, room_id: int, include_deleted: bool = True ) -> list[Bounty]: if room_id not in self._rooms: return [] if include_deleted: return self._rooms[room_id].bounties return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] class MockTrackingStorage: """Mock implementation of TrackingStorage for testing.""" def __init__(self): self._tracking: dict[tuple[int, int], TrackingData] = {} def load(self, room_id: int, user_id: int) -> TrackingData | None: return self._tracking.get((room_id, user_id)) def save(self, tracking_data: TrackingData) -> None: self._tracking[(tracking_data.room_id, tracking_data.user_id)] = tracking_data def track_bounty(self, room_id: int, user_id: int, tracked: TrackedBounty) -> None: key = (room_id, user_id) if key not in self._tracking: self._tracking[key] = TrackingData( room_id=room_id, user_id=user_id, tracked=[] ) self._tracking[key].tracked.append(tracked) def untrack_bounty(self, room_id: int, user_id: int, bounty_id: int) -> None: key = (room_id, user_id) if key in self._tracking: self._tracking[key].tracked = [ t for t in self._tracking[key].tracked if t.bounty_id != bounty_id ] class TestRoomStorage: def test_simple_storage_without_ensure_room(self): """Test that SimpleRoomStorage (no ensure_room) still works. This verifies that ensure_room() is NOT needed as a public Protocol method, since add_bounty() can create the room internally without it. """ storage = SimpleRoomStorage() bounty = Bounty( id=1, text="Test", link=None, due_date_ts=None, created_at=0, created_by_user_id=123, ) storage.add_bounty(-1001, bounty) room = storage.load(-1001) assert room is not None assert len(room.bounties) == 1 assert room.bounties[0].text == "Test" assert room.next_id == 1 def test_mock_implements_room_storage_protocol(self): storage: RoomStorage = MockRoomStorage() assert isinstance(storage, RoomStorage) def test_add_bounty_creates_room_if_not_exists(self): storage = MockRoomStorage() bounty = Bounty( id=1, text="Test", link=None, due_date_ts=None, created_at=0, created_by_user_id=123, ) storage.add_bounty(-1001, bounty) room = storage.load(-1001) assert room is not None assert len(room.bounties) == 1 assert room.bounties[0].text == "Test" def test_update_bounty(self): storage = MockRoomStorage() bounty = Bounty( id=1, text="Original", link=None, due_date_ts=None, created_at=0, created_by_user_id=123, ) storage.add_bounty(-1001, bounty) updated = Bounty( id=1, text="Updated", link=None, due_date_ts=None, created_at=0, created_by_user_id=123, ) storage.update_bounty(-1001, updated) result = storage.get_bounty(-1001, 1) assert result is not None assert result.text == "Updated" def test_delete_bounty(self): storage = MockRoomStorage() bounty = Bounty( id=1, text="Test", link=None, due_date_ts=None, created_at=0, created_by_user_id=123, ) storage.add_bounty(-1001, bounty) storage.delete_bounty(-1001, 1) assert storage.get_bounty(-1001, 1) is None class TestTrackingStorage: def test_simple_storage_without_ensure_tracking(self): """Test that SimpleTrackingStorage (no ensure_tracking) still works. This verifies that ensure_tracking() is NOT needed as a public Protocol method, since track_bounty() can create the tracking entry internally without it. """ storage = SimpleTrackingStorage() tracked = TrackedBounty(bounty_id=5, created_at=0) storage.track_bounty(-1001, 123456, tracked) result = storage.load(-1001, 123456) assert result is not None assert len(result.tracked) == 1 assert result.tracked[0].bounty_id == 5 def test_mock_implements_tracking_storage_protocol(self): storage: TrackingStorage = MockTrackingStorage() assert isinstance(storage, TrackingStorage) def test_track_bounty_creates_tracking_if_not_exists(self): storage = MockTrackingStorage() tracked = TrackedBounty(bounty_id=5, created_at=0) storage.track_bounty(-1001, 123456, tracked) result = storage.load(-1001, 123456) assert result is not None assert len(result.tracked) == 1 assert result.tracked[0].bounty_id == 5 def test_untrack_bounty(self): storage = MockTrackingStorage() tracked = TrackedBounty(bounty_id=5, created_at=0) storage.track_bounty(-1001, 123456, tracked) storage.untrack_bounty(-1001, 123456, 5) result = storage.load(-1001, 123456) assert result is not None assert len(result.tracked) == 0