feat(adapter): implement JSON file storage adapter
Add JsonFileRoomStorage and JsonFileTrackingStorage implementations that implement the RoomStorage and TrackingStorage ports. - Stores room data at ~/.jigaido/data/<room_id>.json - Stores tracking data at ~/.jigaido/tracking/<room_id>_<user_id>.json - Implements all port methods: load, save, add_bounty, update_bounty, delete_bounty, get_bounty for rooms; load, save, track_bounty, untrack_bounty for tracking Fixes #9
This commit is contained in:
269
tests/test_json_file_adapter.py
Normal file
269
tests/test_json_file_adapter.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""Tests for adapters/storage/json_file.py — JSON file storage adapter."""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from core.models import Bounty, RoomData, TrackingData, TrackedBounty
|
||||
from core.ports import RoomStorage, TrackingStorage
|
||||
from adapters.storage.json_file import JsonFileRoomStorage, JsonFileTrackingStorage
|
||||
|
||||
|
||||
class TestJsonFileRoomStorage:
|
||||
"""Tests for JsonFileRoomStorage adapter."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self):
|
||||
"""Create a temporary directory for test files."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
yield Path(tmp)
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
@pytest.fixture
|
||||
def storage(self, temp_dir):
|
||||
"""Create a JsonFileRoomStorage instance with temp directory."""
|
||||
return JsonFileRoomStorage(data_dir=temp_dir)
|
||||
|
||||
def test_implements_room_storage_protocol(self, storage):
|
||||
"""Verify JsonFileRoomStorage implements RoomStorage protocol."""
|
||||
assert isinstance(storage, RoomStorage)
|
||||
|
||||
def test_load_returns_none_for_nonexistent_room(self, storage):
|
||||
"""Load returns None if room doesn't exist."""
|
||||
result = storage.load(-1001)
|
||||
assert result is None
|
||||
|
||||
def test_save_and_load_room(self, storage):
|
||||
"""Test saving and loading room data."""
|
||||
room = RoomData(room_id=-1001, bounties=[], next_id=1)
|
||||
storage.save(room)
|
||||
|
||||
loaded = storage.load(-1001)
|
||||
assert loaded is not None
|
||||
assert loaded.room_id == -1001
|
||||
assert loaded.next_id == 1
|
||||
assert loaded.bounties == []
|
||||
|
||||
def test_add_bounty_creates_room(self, storage):
|
||||
"""Test add_bounty creates room if it doesn't exist."""
|
||||
bounty = Bounty(
|
||||
id=1,
|
||||
text="Test bounty",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1000,
|
||||
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 bounty"
|
||||
assert room.next_id == 2
|
||||
|
||||
def test_add_bounty_appends_to_existing_room(self, storage):
|
||||
"""Test add_bounty appends to existing room."""
|
||||
bounty1 = Bounty(
|
||||
id=1,
|
||||
text="First",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1000,
|
||||
created_by_user_id=123,
|
||||
)
|
||||
bounty2 = Bounty(
|
||||
id=2,
|
||||
text="Second",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1001,
|
||||
created_by_user_id=123,
|
||||
)
|
||||
storage.add_bounty(-1001, bounty1)
|
||||
storage.add_bounty(-1001, bounty2)
|
||||
|
||||
room = storage.load(-1001)
|
||||
assert len(room.bounties) == 2
|
||||
assert room.next_id == 3
|
||||
|
||||
def test_update_bounty(self, storage):
|
||||
"""Test updating an existing bounty."""
|
||||
bounty = Bounty(
|
||||
id=1,
|
||||
text="Original",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1000,
|
||||
created_by_user_id=123,
|
||||
)
|
||||
storage.add_bounty(-1001, bounty)
|
||||
|
||||
updated = Bounty(
|
||||
id=1,
|
||||
text="Updated",
|
||||
link="https://example.com",
|
||||
due_date_ts=2000,
|
||||
created_at=1000,
|
||||
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"
|
||||
assert result.link == "https://example.com"
|
||||
|
||||
def test_delete_bounty(self, storage):
|
||||
"""Test deleting a bounty."""
|
||||
bounty = Bounty(
|
||||
id=1,
|
||||
text="To delete",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1000,
|
||||
created_by_user_id=123,
|
||||
)
|
||||
storage.add_bounty(-1001, bounty)
|
||||
storage.delete_bounty(-1001, 1)
|
||||
|
||||
result = storage.get_bounty(-1001, 1)
|
||||
assert result is None
|
||||
|
||||
def test_get_bounty_returns_none_for_nonexistent(self, storage):
|
||||
"""Test get_bounty returns None for non-existent bounty."""
|
||||
result = storage.get_bounty(-1001, 999)
|
||||
assert result is None
|
||||
|
||||
def test_update_bounty_for_nonexistent_room(self, storage):
|
||||
"""Test update_bounty does nothing for non-existent room."""
|
||||
bounty = Bounty(
|
||||
id=1,
|
||||
text="Test",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1000,
|
||||
created_by_user_id=123,
|
||||
)
|
||||
result = storage.update_bounty(-1001, bounty)
|
||||
# Should not raise, should just return
|
||||
|
||||
|
||||
class TestJsonFileTrackingStorage:
|
||||
"""Tests for JsonFileTrackingStorage adapter."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self):
|
||||
"""Create a temporary directory for test files."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
yield Path(tmp)
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
@pytest.fixture
|
||||
def storage(self, temp_dir):
|
||||
"""Create a JsonFileTrackingStorage instance with temp directory."""
|
||||
return JsonFileTrackingStorage(tracking_dir=temp_dir)
|
||||
|
||||
def test_implements_tracking_storage_protocol(self, storage):
|
||||
"""Verify JsonFileTrackingStorage implements TrackingStorage protocol."""
|
||||
assert isinstance(storage, TrackingStorage)
|
||||
|
||||
def test_load_returns_none_for_nonexistent_tracking(self, storage):
|
||||
"""Load returns None if tracking doesn't exist."""
|
||||
result = storage.load(-1001, 123456)
|
||||
assert result is None
|
||||
|
||||
def test_save_and_load_tracking(self, storage):
|
||||
"""Test saving and loading tracking data."""
|
||||
tracking = TrackingData(
|
||||
room_id=-1001,
|
||||
user_id=123456,
|
||||
tracked=[
|
||||
TrackedBounty(bounty_id=1, created_at=1000),
|
||||
],
|
||||
)
|
||||
storage.save(tracking)
|
||||
|
||||
loaded = storage.load(-1001, 123456)
|
||||
assert loaded is not None
|
||||
assert loaded.room_id == -1001
|
||||
assert loaded.user_id == 123456
|
||||
assert len(loaded.tracked) == 1
|
||||
|
||||
def test_track_bounty_creates_tracking(self, storage):
|
||||
"""Test track_bounty creates tracking if it doesn't exist."""
|
||||
tracked = TrackedBounty(bounty_id=5, created_at=1000)
|
||||
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_track_bounty_appends_to_existing(self, storage):
|
||||
"""Test track_bounty appends to existing tracking."""
|
||||
tracked1 = TrackedBounty(bounty_id=1, created_at=1000)
|
||||
tracked2 = TrackedBounty(bounty_id=2, created_at=1001)
|
||||
storage.track_bounty(-1001, 123456, tracked1)
|
||||
storage.track_bounty(-1001, 123456, tracked2)
|
||||
|
||||
result = storage.load(-1001, 123456)
|
||||
assert len(result.tracked) == 2
|
||||
|
||||
def test_untrack_bounty(self, storage):
|
||||
"""Test untracking a bounty."""
|
||||
tracked = TrackedBounty(bounty_id=5, created_at=1000)
|
||||
storage.track_bounty(-1001, 123456, tracked)
|
||||
storage.untrack_bounty(-1001, 123456, 5)
|
||||
|
||||
result = storage.load(-1001, 123456)
|
||||
assert len(result.tracked) == 0
|
||||
|
||||
def test_untrack_nonexistent_does_not_raise(self, storage):
|
||||
"""Test untracking non-existent bounty doesn't raise."""
|
||||
# Should not raise
|
||||
storage.untrack_bounty(-1001, 123456, 999)
|
||||
|
||||
def test_tracking_persists_across_storage_instances(self, temp_dir):
|
||||
"""Test that tracking data persists when using different storage instances."""
|
||||
storage1 = JsonFileTrackingStorage(tracking_dir=temp_dir)
|
||||
tracked = TrackedBounty(bounty_id=5, created_at=1000)
|
||||
storage1.track_bounty(-1001, 123456, tracked)
|
||||
|
||||
# Create a new storage instance pointing to the same directory
|
||||
storage2 = JsonFileTrackingStorage(tracking_dir=temp_dir)
|
||||
result = storage2.load(-1001, 123456)
|
||||
assert result is not None
|
||||
assert len(result.tracked) == 1
|
||||
|
||||
|
||||
class TestJsonFileRoomStoragePersistence:
|
||||
"""Test that room data persists across storage instances."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self):
|
||||
"""Create a temporary directory for test files."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
yield Path(tmp)
|
||||
shutil.rmtree(tmp)
|
||||
|
||||
def test_room_persists_across_storage_instances(self, temp_dir):
|
||||
"""Test room data persists when using different storage instances."""
|
||||
storage1 = JsonFileRoomStorage(data_dir=temp_dir)
|
||||
bounty = Bounty(
|
||||
id=1,
|
||||
text="Persisted bounty",
|
||||
link=None,
|
||||
due_date_ts=None,
|
||||
created_at=1000,
|
||||
created_by_user_id=123,
|
||||
)
|
||||
storage1.add_bounty(-1001, bounty)
|
||||
|
||||
# Create a new storage instance pointing to the same directory
|
||||
storage2 = JsonFileRoomStorage(data_dir=temp_dir)
|
||||
room = storage2.load(-1001)
|
||||
assert room is not None
|
||||
assert len(room.bounties) == 1
|
||||
assert room.bounties[0].text == "Persisted bounty"
|
||||
Reference in New Issue
Block a user