Skip to content

Commit 7556f67

Browse files
author
0xMett
committed
test: add tests for chat title storage, service delegation, and target parsing
18 tests covering InMemoryRepository new methods (register with title, find by title, update title), ModerationService delegation, and _parse_target_and_message edge cases.
1 parent 1016056 commit 7556f67

1 file changed

Lines changed: 173 additions & 0 deletions

File tree

tests/test_announce.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Tests for InMemoryRepository chat methods and announce target parsing."""
2+
3+
import asyncio
4+
5+
from python_italy_bot.db.in_memory import InMemoryRepository
6+
from python_italy_bot.handlers.announce import _parse_target_and_message
7+
from python_italy_bot.services.moderation import ModerationService
8+
9+
10+
# -- InMemoryRepository tests --
11+
12+
13+
def test_register_chat_without_title() -> None:
14+
"""Register a chat without title stores None."""
15+
repo = InMemoryRepository()
16+
asyncio.run(repo.register_chat(-100123))
17+
chats = asyncio.run(repo.get_all_chats())
18+
assert chats == [-100123]
19+
20+
21+
def test_register_chat_with_title() -> None:
22+
"""Register a chat with title stores both."""
23+
repo = InMemoryRepository()
24+
asyncio.run(repo.register_chat(-100123, "Python Italia"))
25+
chats = asyncio.run(repo.get_all_chats_with_titles())
26+
assert len(chats) == 1
27+
assert chats[0].chat_id == -100123
28+
assert chats[0].title == "Python Italia"
29+
30+
31+
def test_register_chat_updates_title() -> None:
32+
"""Re-registering a chat updates its title."""
33+
repo = InMemoryRepository()
34+
asyncio.run(repo.register_chat(-100123, "Old Name"))
35+
asyncio.run(repo.register_chat(-100123, "New Name"))
36+
chats = asyncio.run(repo.get_all_chats_with_titles())
37+
assert len(chats) == 1
38+
assert chats[0].title == "New Name"
39+
40+
41+
def test_get_all_chats_returns_ids_only() -> None:
42+
"""get_all_chats returns only chat IDs."""
43+
repo = InMemoryRepository()
44+
asyncio.run(repo.register_chat(-100001, "Group A"))
45+
asyncio.run(repo.register_chat(-100002, "Group B"))
46+
chats = asyncio.run(repo.get_all_chats())
47+
assert set(chats) == {-100001, -100002}
48+
49+
50+
def test_get_all_chats_with_titles_returns_chat_objects() -> None:
51+
"""get_all_chats_with_titles returns Chat dataclass instances."""
52+
repo = InMemoryRepository()
53+
asyncio.run(repo.register_chat(-100001, "Group A"))
54+
asyncio.run(repo.register_chat(-100002))
55+
chats = asyncio.run(repo.get_all_chats_with_titles())
56+
assert len(chats) == 2
57+
by_id = {c.chat_id: c for c in chats}
58+
assert by_id[-100001].title == "Group A"
59+
assert by_id[-100002].title is None
60+
61+
62+
def test_find_chats_by_title_case_insensitive() -> None:
63+
"""find_chats_by_title matches case-insensitively."""
64+
repo = InMemoryRepository()
65+
asyncio.run(repo.register_chat(-100001, "Python Italia"))
66+
asyncio.run(repo.register_chat(-100002, "Python Torino"))
67+
asyncio.run(repo.register_chat(-100003, "JavaScript Italia"))
68+
69+
results = asyncio.run(repo.find_chats_by_title("python"))
70+
assert len(results) == 2
71+
ids = {c.chat_id for c in results}
72+
assert ids == {-100001, -100002}
73+
74+
75+
def test_find_chats_by_title_partial_match() -> None:
76+
"""find_chats_by_title matches partial strings."""
77+
repo = InMemoryRepository()
78+
asyncio.run(repo.register_chat(-100001, "Python Italia"))
79+
results = asyncio.run(repo.find_chats_by_title("Ital"))
80+
assert len(results) == 1
81+
assert results[0].chat_id == -100001
82+
83+
84+
def test_find_chats_by_title_no_match() -> None:
85+
"""find_chats_by_title returns empty list when no match."""
86+
repo = InMemoryRepository()
87+
asyncio.run(repo.register_chat(-100001, "Python Italia"))
88+
results = asyncio.run(repo.find_chats_by_title("Rust"))
89+
assert results == []
90+
91+
92+
def test_find_chats_by_title_skips_none_titles() -> None:
93+
"""find_chats_by_title skips chats without a title."""
94+
repo = InMemoryRepository()
95+
asyncio.run(repo.register_chat(-100001))
96+
results = asyncio.run(repo.find_chats_by_title("anything"))
97+
assert results == []
98+
99+
100+
# -- ModerationService delegation tests --
101+
102+
103+
def test_moderation_service_register_chat_with_title() -> None:
104+
"""ModerationService.register_chat forwards title to repository."""
105+
repo = InMemoryRepository()
106+
service = ModerationService(repo)
107+
asyncio.run(service.register_chat(-100001, "Test Group"))
108+
chats = asyncio.run(service.get_all_chats_with_titles())
109+
assert len(chats) == 1
110+
assert chats[0].title == "Test Group"
111+
112+
113+
def test_moderation_service_find_chats_by_title() -> None:
114+
"""ModerationService.find_chats_by_title delegates to repository."""
115+
repo = InMemoryRepository()
116+
service = ModerationService(repo)
117+
asyncio.run(service.register_chat(-100001, "Python Italia"))
118+
asyncio.run(service.register_chat(-100002, "Python Torino"))
119+
results = asyncio.run(service.find_chats_by_title("Torino"))
120+
assert len(results) == 1
121+
assert results[0].chat_id == -100002
122+
123+
124+
# -- Target parsing tests --
125+
126+
127+
def test_parse_target_and_message_with_pipe() -> None:
128+
"""Pipe separator splits target from message."""
129+
target, message = _parse_target_and_message("Python Italia | Hello everyone!")
130+
assert target == "Python Italia"
131+
assert message == "Hello everyone!"
132+
133+
134+
def test_parse_target_and_message_without_pipe() -> None:
135+
"""No pipe means no target, full text is message."""
136+
target, message = _parse_target_and_message("Hello everyone!")
137+
assert target is None
138+
assert message == "Hello everyone!"
139+
140+
141+
def test_parse_target_and_message_numeric_id() -> None:
142+
"""Numeric target with pipe."""
143+
target, message = _parse_target_and_message("-100123 | Announcement")
144+
assert target == "-100123"
145+
assert message == "Announcement"
146+
147+
148+
def test_parse_target_and_message_username() -> None:
149+
"""Username target with pipe."""
150+
target, message = _parse_target_and_message("@pythonita | News update")
151+
assert target == "@pythonita"
152+
assert message == "News update"
153+
154+
155+
def test_parse_target_and_message_pipe_in_message() -> None:
156+
"""Only the first pipe is used as delimiter."""
157+
target, message = _parse_target_and_message("group | msg with | pipe")
158+
assert target == "group"
159+
assert message == "msg with | pipe"
160+
161+
162+
def test_parse_target_and_message_empty_target() -> None:
163+
"""Empty target before pipe falls back to no-target mode."""
164+
target, message = _parse_target_and_message(" | Hello")
165+
assert target is None
166+
assert message == " | Hello"
167+
168+
169+
def test_parse_target_and_message_empty_message() -> None:
170+
"""Empty message after pipe falls back to no-target mode."""
171+
target, message = _parse_target_and_message("group | ")
172+
assert target is None
173+
assert message == "group | "

0 commit comments

Comments
 (0)