1515import sys
1616from datetime import datetime
1717
18- import pytest
18+ import pytest_asyncio
1919from sqlalchemy .ext .asyncio import AsyncSession , async_sessionmaker , create_async_engine
2020from sqlalchemy .pool import StaticPool
2121
2626from src .db .models import Base , Chat , Message
2727
2828
29- async def _make_adapter_with_messages ():
30- """Spin up an in-memory SQLite DB seeded with a forum chat + mixed-topic messages."""
29+ @pytest_asyncio .fixture
30+ async def forum_adapter ():
31+ """In-memory SQLite DB seeded with a forum chat + mixed-topic messages."""
3132 engine = create_async_engine (
3233 "sqlite+aiosqlite://" ,
3334 poolclass = StaticPool ,
@@ -50,9 +51,9 @@ async def _make_adapter_with_messages():
5051 # (id, date, text, reply_to_top_id)
5152 (1 , datetime (2024 , 1 , 1 , 10 ), "pre-forum general #1" , None ),
5253 (2 , datetime (2024 , 1 , 2 , 10 ), "pre-forum general #2" , None ),
53- (3 , datetime (2024 , 3 , 1 , 10 ), "sexy topic msg" , 47 ),
54- (4 , datetime (2024 , 3 , 2 , 10 ), "another sexy msg" , 47 ),
55- (5 , datetime (2024 , 4 , 1 , 10 ), "photos topic" , 144 ),
54+ (3 , datetime (2024 , 3 , 1 , 10 ), "topic-47 msg #1 " , 47 ),
55+ (4 , datetime (2024 , 3 , 2 , 10 ), "topic-47 msg #2 " , 47 ),
56+ (5 , datetime (2024 , 4 , 1 , 10 ), "topic-144 photos " , 144 ),
5657 # Post-forum-enable: Telegram sets reply_to_top_id=1 explicitly on
5758 # new General messages. Must appear alongside NULL rows under topic_id=1.
5859 (6 , datetime (2024 , 5 , 1 , 10 ), "post-forum general (explicit 1)" , 1 ),
@@ -61,42 +62,59 @@ async def _make_adapter_with_messages():
6162 session .add (Message (id = mid , chat_id = chat_id , date = dt , text = body , reply_to_top_id = top ))
6263 await session .commit ()
6364
64- return DatabaseAdapter (db_manager ), engine , chat_id
65+ yield DatabaseAdapter (db_manager ), chat_id
6566
67+ await engine .dispose ()
6668
67- @ pytest . mark . asyncio
68- async def test_general_topic_includes_null_and_explicit_one ():
69+
70+ async def test_general_topic_includes_null_and_explicit_one (forum_adapter ):
6971 """topic_id=1 (General) must match both NULL reply_to_top_id (pre-forum) and
7072 explicit reply_to_top_id=1 (post-forum-enable) messages."""
71- adapter , engine , chat_id = await _make_adapter_with_messages ()
72- try :
73- messages = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 1 )
74- ids = sorted (m ["id" ] for m in messages )
75- assert ids == [1 , 2 , 6 ], f"General (topic_id=1) must return NULL and explicit-1 messages, got { ids } "
76- finally :
77- await engine .dispose ()
73+ adapter , chat_id = forum_adapter
74+ messages = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 1 )
75+ ids = sorted (m ["id" ] for m in messages )
76+ assert ids == [1 , 2 , 6 ], f"General (topic_id=1) must return NULL and explicit-1 messages, got { ids } "
7877
7978
80- @pytest .mark .asyncio
81- async def test_non_general_topic_filters_strictly ():
79+ async def test_non_general_topic_filters_strictly (forum_adapter ):
8280 """Non-General topic IDs must still filter by strict equality on reply_to_top_id."""
83- adapter , engine , chat_id = await _make_adapter_with_messages ()
84- try :
85- sexy = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 47 )
86- assert sorted (m ["id" ] for m in sexy ) == [3 , 4 ]
81+ adapter , chat_id = forum_adapter
82+ topic_47 = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 47 )
83+ assert sorted (m ["id" ] for m in topic_47 ) == [3 , 4 ]
8784
88- photos = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 144 )
89- assert sorted (m ["id" ] for m in photos ) == [5 ]
90- finally :
91- await engine .dispose ()
85+ photos = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 144 )
86+ assert sorted (m ["id" ] for m in photos ) == [5 ]
9287
9388
94- @pytest .mark .asyncio
95- async def test_no_topic_filter_returns_all ():
89+ async def test_no_topic_filter_returns_all (forum_adapter ):
9690 """Without topic_id, all messages in the chat must be returned regardless of reply_to_top_id."""
97- adapter , engine , chat_id = await _make_adapter_with_messages ()
98- try :
99- all_msgs = await adapter .get_messages_paginated (chat_id = chat_id )
100- assert sorted (m ["id" ] for m in all_msgs ) == [1 , 2 , 3 , 4 , 5 , 6 ]
101- finally :
102- await engine .dispose ()
91+ adapter , chat_id = forum_adapter
92+ all_msgs = await adapter .get_messages_paginated (chat_id = chat_id )
93+ assert sorted (m ["id" ] for m in all_msgs ) == [1 , 2 , 3 , 4 , 5 , 6 ]
94+
95+
96+ async def test_nonexistent_topic_returns_empty (forum_adapter ):
97+ """A topic_id with no matching messages must return an empty list."""
98+ adapter , chat_id = forum_adapter
99+ messages = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 999 )
100+ assert messages == []
101+
102+
103+ async def test_topic_zero_returns_empty (forum_adapter ):
104+ """topic_id=0 is not a valid Telegram topic — must not match NULL rows via coalesce."""
105+ adapter , chat_id = forum_adapter
106+ messages = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 0 )
107+ assert messages == []
108+
109+
110+ async def test_topic_filter_combined_with_search (forum_adapter ):
111+ """topic_id and search filters must both apply — only matching messages returned."""
112+ adapter , chat_id = forum_adapter
113+ # Search within General topic (id=1) — only "pre-forum general #1" matches
114+ messages = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 1 , search = "general #1" )
115+ ids = sorted (m ["id" ] for m in messages )
116+ assert ids == [1 ]
117+
118+ # Search term exists in topic-47 but not in General
119+ messages = await adapter .get_messages_paginated (chat_id = chat_id , topic_id = 1 , search = "topic-47" )
120+ assert messages == []
0 commit comments