Files
jigaido/tests/test_json_file.py
shokollm 4885be0752 fix: cleanup codebase and sync SPEC with actual permissions
Phase 1: Ruff lint fixes
- Remove unused imports across all files
- Remove unused variables (now_utc, tz, ctx)
- Fix f-string without placeholders
- Fix E402 import order with noqa comments

Phase 2: Remove confusing hard delete from storage
- Removed delete_bounty() from RoomStorage Protocol (never used by app)
- Removed delete_bounty() from JsonFileRoomStorage (was hard delete)
- Removed corresponding tests (hard delete was never used)

Phase 3: Sync SPEC.md with actual code behavior
- Updated overview: admins can add/edit/delete (not 'anyone' + 'creator')
- Updated command table: /add, /edit, /delete are admin only
- Updated error handling messages

Test results: 96 passed (2 hard delete tests removed)
2026-04-09 10:01:02 +00:00

259 lines
9.2 KiB
Python

"""Tests for adapters/storage/json_file.py — JSON file storage adapter."""
import json
import tempfile
from pathlib import Path
from adapters.storage.json_file import JsonFileRoomStorage, JsonFileTrackingStorage
from core.models import Bounty, RoomData, TrackingData, TrackedBounty
class TestJsonFileRoomStorage:
"""Unit tests for JsonFileRoomStorage."""
def setup_method(self):
"""Set up a temporary directory for each test."""
self.temp_dir = tempfile.mkdtemp()
self.storage = JsonFileRoomStorage(data_dir=Path(self.temp_dir))
def teardown_method(self):
"""Clean up temporary directory after each test."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def _create_bounty(
self,
id=1,
text="Test bounty",
link=None,
due_date_ts=None,
created_at=0,
created_by_user_id=123,
):
"""Helper to create a Bounty."""
return Bounty(
id=id,
text=text,
link=link,
due_date_ts=due_date_ts,
created_at=created_at,
created_by_user_id=created_by_user_id,
)
def test_load_returns_none_for_nonexistent_room(self):
"""Test that load returns None for a room that doesn't exist."""
result = self.storage.load(-1001)
assert result is None
def test_save_and_load_room(self):
"""Test that save and load work correctly."""
room = RoomData(room_id=-1001, bounties=[], next_id=1)
self.storage.save(room)
loaded = self.storage.load(-1001)
assert loaded is not None
assert loaded.room_id == -1001
assert loaded.bounties == []
assert loaded.next_id == 1
def test_add_bounty_creates_room(self):
"""Test that add_bounty creates a room if it doesn't exist."""
bounty = self._create_bounty()
self.storage.add_bounty(-1001, bounty)
loaded = self.storage.load(-1001)
assert loaded is not None
assert len(loaded.bounties) == 1
assert loaded.bounties[0].text == "Test bounty"
def test_add_bounty_increments_next_id(self):
"""Test that add_bounty properly handles next_id."""
bounty1 = self._create_bounty(id=1)
bounty2 = self._create_bounty(id=2)
self.storage.add_bounty(-1001, bounty1)
self.storage.add_bounty(-1001, bounty2)
loaded = self.storage.load(-1001)
assert loaded.next_id == 3 # Should be max id + 1
def test_update_bounty(self):
"""Test that update_bounty correctly updates a bounty."""
bounty = self._create_bounty(id=1, text="Original")
self.storage.add_bounty(-1001, bounty)
updated = self._create_bounty(id=1, text="Updated")
self.storage.update_bounty(-1001, updated)
loaded = self.storage.load(-1001)
assert loaded.bounties[0].text == "Updated"
def test_update_bounty_nonexistent_room(self):
"""Test that update_bounty does nothing for nonexistent room."""
updated = self._create_bounty(id=1, text="Updated")
self.storage.update_bounty(-1001, updated) # Should not raise
assert self.storage.load(-1001) is None
def test_get_bounty_found(self):
"""Test that get_bounty returns the bounty when found."""
bounty = self._create_bounty(id=1)
self.storage.add_bounty(-1001, bounty)
result = self.storage.get_bounty(-1001, 1)
assert result is not None
assert result.text == "Test bounty"
def test_get_bounty_not_found(self):
"""Test that get_bounty returns None when not found."""
result = self.storage.get_bounty(-1001, 999)
assert result is None
def test_file_path_format(self):
"""Test that room data is stored in correct location."""
room = RoomData(room_id=-1001, bounties=[], next_id=1)
self.storage.save(room)
expected_path = Path(self.temp_dir) / "-1001.json"
assert expected_path.exists()
def test_atomic_write(self):
"""Test that data is written atomically."""
room = RoomData(room_id=-1001, bounties=[], next_id=1)
self.storage.save(room)
# Check that the file is valid JSON
file_path = Path(self.temp_dir) / "-1001.json"
with open(file_path) as f:
data = json.load(f)
assert data["room_id"] == -1001
class TestJsonFileTrackingStorage:
"""Unit tests for JsonFileTrackingStorage."""
def setup_method(self):
"""Set up a temporary directory for each test."""
self.temp_dir = tempfile.mkdtemp()
self.storage = JsonFileTrackingStorage(tracking_dir=Path(self.temp_dir))
def teardown_method(self):
"""Clean up temporary directory after each test."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def _create_tracked(self, bounty_id=1, created_at=0):
"""Helper to create a TrackedBounty."""
return TrackedBounty(bounty_id=bounty_id, created_at=created_at)
def test_load_returns_none_for_nonexistent_tracking(self):
"""Test that load returns None when no tracking exists."""
result = self.storage.load(-1001, 123456)
assert result is None
def test_save_and_load_tracking(self):
"""Test that save and load work correctly."""
tracking = TrackingData(room_id=-1001, user_id=123456, tracked=[])
self.storage.save(tracking)
loaded = self.storage.load(-1001, 123456)
assert loaded is not None
assert loaded.room_id == -1001
assert loaded.user_id == 123456
def test_track_bounty(self):
"""Test that track_bounty adds a bounty to tracking."""
tracked = self._create_tracked(bounty_id=5)
self.storage.track_bounty(-1001, 123456, tracked)
loaded = self.storage.load(-1001, 123456)
assert loaded is not None
assert len(loaded.tracked) == 1
assert loaded.tracked[0].bounty_id == 5
def test_untrack_bounty(self):
"""Test that untrack_bounty removes a bounty from tracking."""
tracked = self._create_tracked(bounty_id=5)
self.storage.track_bounty(-1001, 123456, tracked)
self.storage.untrack_bounty(-1001, 123456, 5)
loaded = self.storage.load(-1001, 123456)
assert loaded is not None
assert len(loaded.tracked) == 0
def test_untrack_bounty_nonexistent(self):
"""Test that untrack_bounty handles nonexistent tracking gracefully."""
self.storage.untrack_bounty(-1001, 123456, 999) # Should not raise
def test_file_path_format(self):
"""Test that tracking data is stored in correct location."""
tracked = self._create_tracked(bounty_id=5)
self.storage.track_bounty(-1001, 123456, tracked)
expected_path = Path(self.temp_dir) / "-1001_123456.json"
assert expected_path.exists()
def test_multiple_tracked_bounties(self):
"""Test tracking multiple bounties."""
self.storage.track_bounty(-1001, 123456, self._create_tracked(bounty_id=1))
self.storage.track_bounty(-1001, 123456, self._create_tracked(bounty_id=2))
self.storage.track_bounty(-1001, 123456, self._create_tracked(bounty_id=3))
loaded = self.storage.load(-1001, 123456)
assert len(loaded.tracked) == 3
def test_different_users_independent_tracking(self):
"""Test that different users have independent tracking."""
self.storage.track_bounty(-1001, 111, self._create_tracked(bounty_id=1))
self.storage.track_bounty(-1001, 222, self._create_tracked(bounty_id=1))
loaded_111 = self.storage.load(-1001, 111)
loaded_222 = self.storage.load(-1001, 222)
assert len(loaded_111.tracked) == 1
assert len(loaded_222.tracked) == 1
class TestDuplicateTrackingBehavior:
"""Test that duplicate tracking is handled correctly.
Note: The deduplication logic is in the TrackingService layer,
not in the adapter. This test verifies the adapter behavior
when the same bounty is tracked multiple times (which would only
happen if the service layer has a bug).
"""
def setup_method(self):
"""Set up a temporary directory for each test."""
self.temp_dir = tempfile.mkdtemp()
self.storage = JsonFileTrackingStorage(tracking_dir=Path(self.temp_dir))
def teardown_method(self):
"""Clean up temporary directory after each test."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_adapter_allows_duplicate_tracking(self):
"""The adapter does NOT prevent duplicate tracking.
This is by design - deduplication should be handled by
TrackingService.track_bounty(), not the storage adapter.
"""
# Add same bounty twice via adapter (bypassing service)
self.storage.track_bounty(
-1001, 123456, TrackedBounty(bounty_id=5, created_at=0)
)
self.storage.track_bounty(
-1001, 123456, TrackedBounty(bounty_id=5, created_at=0)
)
loaded = self.storage.load(-1001, 123456)
# Adapter allows duplicates - service should prevent them
assert len(loaded.tracked) == 2
assert loaded.tracked[0].bounty_id == 5
assert loaded.tracked[1].bounty_id == 5