Add tests + fix db.py SQLite commit pattern

Tests:
- tests/test_commands.py: parse_args, extract_args, format_bounty
- tests/test_db.py: full CRUD + tracking + reminders
- tests/conftest.py: temp DB fixture
- requirements-dev.txt: pytest + pytest-asyncio

db.py fixes:
- Explicit conn.commit() after every write (SQLite row_factory
  disables implicit transaction management)
- fetchone() before commit() (can't commit while cursor open)
- Functions return dict instead of sqlite3.Row
This commit is contained in:
shokollm
2026-04-01 08:41:44 +00:00
parent 9f0ad2d404
commit 7957947a04
6 changed files with 543 additions and 29 deletions

View File

@@ -0,0 +1,296 @@
"""Tests for db.py"""
import time
import pytest
import db as _db
class TestUsers:
def test_upsert_user_creates_new(self):
uid = _db.upsert_user(123, "alice")
assert uid > 0
row = _db.get_user_by_telegram_id(123)
assert row is not None
assert row["telegram_user_id"] == 123
assert row["username"] == "alice"
def test_upsert_user_updates_username(self):
_db.upsert_user(123, "alice")
uid2 = _db.upsert_user(123, "alice_updated")
assert uid2 == _db.upsert_user(123, "alice") # same id
row = _db.get_user_by_telegram_id(123)
assert row["username"] == "alice_updated"
def test_get_user_by_telegram_id_not_found(self):
row = _db.get_user_by_telegram_id(999999)
assert row is None
class TestGroups:
def test_upsert_group_creates_new(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
assert gid > 0
row = _db.get_group(-100123)
assert row is not None
assert row["telegram_chat_id"] == -100123
assert row["creator_user_id"] == uid
def test_upsert_group_idempotent(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid1 = _db.upsert_group(-100123, uid)
gid2 = _db.upsert_group(-100123, uid)
assert gid1 == gid2
def test_get_group_creator_user_id(self, fresh_db):
uid = _db.upsert_user(1, "creator")
_db.upsert_group(-100123, uid)
assert _db.get_group_creator_user_id(_db.get_group(-100123)["id"]) == uid
def test_get_group_not_found(self, fresh_db):
row = _db.get_group(-999999)
assert row is None
class TestGroupAdmins:
def test_add_remove_is_admin(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
assert not _db.is_group_admin(gid, uid)
added = _db.add_group_admin(gid, uid)
assert added is True
assert _db.is_group_admin(gid, uid) is True
# Adding again returns False (already admin)
assert _db.add_group_admin(gid, uid) is False
removed = _db.remove_group_admin(gid, uid)
assert removed is True
assert _db.is_group_admin(gid, uid) is False
def test_remove_nonexistent_admin_returns_false(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
assert _db.remove_group_admin(gid, uid) is False
def test_is_group_creator(self, fresh_db):
uid = _db.upsert_user(1, "creator")
other = _db.upsert_user(2, "other")
gid = _db.upsert_group(-100123, uid)
_db.add_group_admin(gid, uid)
assert _db.is_group_creator(gid, uid) is True
assert _db.is_group_creator(gid, other) is False
def test_get_user_by_username(self, fresh_db):
uid = _db.upsert_user(1, "alice")
row = _db.get_user_by_username("alice")
assert row is not None
assert row["id"] == uid
assert _db.get_user_by_username("nobody") is None
class TestBounties:
def test_add_bounty_group(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
_db.add_group_admin(gid, uid)
bid = _db.add_bounty(
group_id=gid,
created_by_user_id=uid,
informed_by_username="bob",
text="Fix bug",
link="https://github.com/bob/repo",
due_date_ts=int(time.time()) + 86400,
)
assert bid > 0
bounty = _db.get_bounty(bid)
assert bounty["text"] == "Fix bug"
assert bounty["link"] == "https://github.com/bob/repo"
assert bounty["informed_by_username"] == "bob"
assert bounty["group_id"] == gid
def test_add_bounty_personal(self, fresh_db):
uid = _db.upsert_user(1, "alice")
bid = _db.add_bounty(
group_id=None,
created_by_user_id=uid,
informed_by_username="alice",
text="Personal reminder",
link=None,
due_date_ts=None,
)
assert bid > 0
bounty = _db.get_bounty(bid)
assert bounty["group_id"] is None
assert bounty["text"] == "Personal reminder"
def test_add_bounty_duplicate_link_rejected(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
_db.add_bounty(gid, uid, "user1", "text1", "https://example.com", None)
with pytest.raises(ValueError, match="Link already exists"):
_db.add_bounty(gid, uid, "user2", "text2", "https://example.com", None)
def test_add_bounty_null_link_allows_multiples(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
bid1 = _db.add_bounty(gid, uid, "user1", "text only 1", None, None)
bid2 = _db.add_bounty(gid, uid, "user2", "text only 2", None, None)
assert bid1 != bid2
def test_get_group_bounties(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
_db.add_group_admin(gid, uid)
_db.add_bounty(gid, uid, "user", "bounty1", None, None)
_db.add_bounty(gid, uid, "user", "bounty2", None, None)
bounties = _db.get_group_bounties(gid)
assert len(bounties) == 2
def test_get_user_personal_bounties(self, fresh_db):
uid = _db.upsert_user(1, "alice")
_db.add_bounty(None, uid, "alice", "personal1", None, None)
_db.add_bounty(None, uid, "alice", "personal2", None, None)
# Group bounty should not appear
other = _db.upsert_user(2, "bob")
gid = _db.upsert_group(-100, other)
_db.add_bounty(gid, other, "bob", "group bounty", None, None)
personal = _db.get_user_personal_bounties(uid)
assert len(personal) == 2
def test_update_bounty(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
bid = _db.add_bounty(gid, uid, "user", "old text", None, None)
_db.update_bounty(bid, "new text", None, None)
updated = _db.get_bounty(bid)
assert updated["text"] == "new text"
def test_update_bounty_duplicate_link_rejected(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
_db.add_bounty(gid, uid, "user1", "bounty1", "https://a.com", None)
bid2 = _db.add_bounty(gid, uid, "user2", "bounty2", None, None)
with pytest.raises(ValueError, match="Link already exists"):
_db.update_bounty(bid2, None, "https://a.com", None)
def test_delete_bounty(self, fresh_db):
uid = _db.upsert_user(1, "creator")
gid = _db.upsert_group(-100123, uid)
bid = _db.add_bounty(gid, uid, "user", "to delete", None, None)
assert _db.delete_bounty(bid) is True
assert _db.get_bounty(bid) is None
# Deleting again returns False
assert _db.delete_bounty(bid) is False
class TestTracking:
def test_track_untrack_is_tracking(self, fresh_db):
uid = _db.upsert_user(1, "alice")
uid2 = _db.upsert_user(2, "bob")
gid = _db.upsert_group(-100123, uid)
bid = _db.add_bounty(gid, uid, "alice", "task", None, None)
assert _db.track_bounty(uid, bid) is True
assert _db.is_tracking(uid, bid) is True
# Track again → False (already tracking)
assert _db.track_bounty(uid, bid) is False
# Other user tracking same bounty
assert _db.track_bounty(uid2, bid) is True
assert _db.is_tracking(uid2, bid) is True
# Untrack
assert _db.untrack_bounty(uid, bid) is True
assert _db.is_tracking(uid, bid) is False
assert _db.is_tracking(uid2, bid) is True # other user still tracking
# Untrack again → False
assert _db.untrack_bounty(uid, bid) is False
def test_get_user_tracked_bounties_in_group(self, fresh_db):
uid = _db.upsert_user(1, "alice")
gid = _db.upsert_group(-100123, uid)
bid1 = _db.add_bounty(gid, uid, "alice", "task1", None, None)
bid2 = _db.add_bounty(gid, uid, "alice", "task2", None, None)
# Different group bounty
other_gid = _db.upsert_group(-100124, uid)
bid3 = _db.add_bounty(other_gid, uid, "alice", "other group task", None, None)
_db.track_bounty(uid, bid1)
_db.track_bounty(uid, bid3)
tracked = _db.get_user_tracked_bounties_in_group(uid, gid)
assert len(tracked) == 1
assert tracked[0]["id"] == bid1
def test_get_user_tracked_bounties_personal(self, fresh_db):
uid = _db.upsert_user(1, "alice")
bid1 = _db.add_bounty(None, uid, "alice", "personal1", None, None)
bid2 = _db.add_bounty(None, uid, "alice", "personal2", None, None)
gid = _db.upsert_group(-100123, uid)
bid3 = _db.add_bounty(gid, uid, "alice", "group task", None, None)
_db.track_bounty(uid, bid1)
_db.track_bounty(uid, bid3)
tracked = _db.get_user_tracked_bounties_personal(uid)
assert len(tracked) == 1
assert tracked[0]["id"] == bid1
class TestReminders:
def test_get_bounties_due_soon(self, fresh_db):
uid = _db.upsert_user(1, "alice")
gid = _db.upsert_group(-100123, uid)
now = int(time.time())
# Due in 3 days (< 7 days)
bid_soon = _db.add_bounty(gid, uid, "alice", "soon", None, now + 3 * 86400)
# Due in 10 days (> 7 days)
_db.add_bounty(gid, uid, "alice", "later", None, now + 10 * 86400)
# No due date
bid_no_date = _db.add_bounty(gid, uid, "alice", "no date", None, None)
_db.track_bounty(uid, bid_soon)
_db.track_bounty(uid, bid_no_date)
due = _db.get_bounties_due_soon(uid, days=7)
assert len(due) == 1
assert due[0]["id"] == bid_soon
def test_reminder_log_prevents_duplicate_reminders(self, fresh_db):
uid = _db.upsert_user(1, "alice")
gid = _db.upsert_group(-100123, uid)
now = int(time.time())
bid = _db.add_bounty(gid, uid, "alice", "task", None, now + 2 * 86400)
_db.track_bounty(uid, bid)
due1 = _db.get_bounties_due_soon(uid, days=7)
assert len(due1) == 1
# Log that we reminded
_db.log_reminder(uid, bid)
# Should not appear again
due2 = _db.get_bounties_due_soon(uid, days=7)
assert len(due2) == 0
def test_get_all_user_ids(self, fresh_db):
_db.upsert_user(1, "alice")
_db.upsert_user(2, "bob")
ids = _db.get_all_user_ids()
assert sorted(ids) == [1, 2]