You've already forked transaction-tracker
fix(tracker): is_processed now checks expires_at, not just file existence 🐛
Previously is_processed() returned True for any record file that existed, relying entirely on cleanup_expired() (called at __init__) to delete stale files. Because cleanup runs at container startup — before Akahu transactions are fetched — any record that expired exactly on that startup would be deleted and then immediately missed, letting the duplicate through. Fix: is_processed() reads the expires_at field from the JSON and returns False if the record has expired, regardless of whether cleanup has run. Also adds migrate_ttl.py script to retroactively extend expires_at on existing records that were written under a shorter TTL, and bumps version to 0.1.3.
This commit is contained in:
@@ -215,6 +215,43 @@ class TestTransactionProcessor:
|
||||
with pytest.raises(TransactionAlreadyProcessedError):
|
||||
multi_decorated("multi_1", 100.00)
|
||||
|
||||
def test_is_processed_respects_expiry_without_cleanup(self, temp_dir):
|
||||
"""Expired record must not block re-processing even if cleanup hasn't run.
|
||||
|
||||
This is the regression test for the bug where is_processed() only checked
|
||||
file existence. cleanup_expired() runs at startup and deletes expired files,
|
||||
but if the expired file is deleted AFTER a duplicate arrives in the same run
|
||||
the duplicate would slip through. The fix: is_processed() reads expires_at
|
||||
from the JSON directly.
|
||||
"""
|
||||
import json
|
||||
|
||||
processor = TransactionProcessor(storage_dir=temp_dir, ttl_days=1)
|
||||
|
||||
@processor.unique_transaction()
|
||||
def pay(transaction_id, amount):
|
||||
return "ok"
|
||||
|
||||
pay("ttl_check", 50.0)
|
||||
|
||||
record_path = os.path.join(temp_dir, "ttl_check.json")
|
||||
assert os.path.exists(record_path)
|
||||
|
||||
# Manually backdate expires_at so the record is expired
|
||||
with open(record_path) as f:
|
||||
record = json.load(f)
|
||||
record["expires_at"] = (datetime.now() - timedelta(seconds=1)).isoformat()
|
||||
with open(record_path, "w") as f:
|
||||
json.dump(record, f)
|
||||
|
||||
# is_processed must return False — the file still exists but it's expired
|
||||
assert not processor.tracker.is_processed("ttl_check")
|
||||
|
||||
def test_is_processed_returns_true_for_valid_record(self, processor):
|
||||
"""is_processed returns True for a record that exists and has not expired."""
|
||||
processor.tracker.mark_processed("valid_tx")
|
||||
assert processor.tracker.is_processed("valid_tx")
|
||||
|
||||
def test_non_string_transaction_id(self, processor):
|
||||
"""Test handling of non-string transaction IDs."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user