Skip to content

Commit b5d2f5f

Browse files
committed
test: make the coverage gate cover the whole bot
Add __init__.py to the src/ packages so coverage.py descends into them and reports every shipped module, instead of only the ones a test happens to import. register.py and regex_message.py were absent from the report at 0% real coverage while the gate read 100%. Cover register.py, regex_message.py and bot.py, drop bot.py from omit, and add an integration test that dispatches synthetic Slack events through a real AsyncApp with the Slack web client mocked, exercising registration, matching, detection and reply in one path. The same missing __init__.py in tests/ is why the suite needed PYTHONPATH=. outside Docker; it now runs with a bare pytest.
1 parent 43cf78f commit b5d2f5f

14 files changed

Lines changed: 270 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pythonpath = ["src",]
2323

2424
[tool.coverage.run]
2525
branch = true
26-
omit = ["*/tests/*", "src/bot.py"]
26+
omit = ["*/tests/*"]
2727

2828
[tool.coverage.report]
2929
show_missing = true

src/__init__.py

Whitespace-only changes.

src/config/__init__.py

Whitespace-only changes.

src/listeners/__init__.py

Whitespace-only changes.

src/listeners/messages/__init__.py

Whitespace-only changes.

src/rules/__init__.py

Whitespace-only changes.

tests/config/__init__.py

Whitespace-only changes.

tests/integration/__init__.py

Whitespace-only changes.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from json import dumps
2+
from os import environ
3+
from unittest import IsolatedAsyncioTestCase
4+
from unittest.mock import AsyncMock, MagicMock, patch
5+
6+
from slack_bolt.async_app import AsyncApp
7+
from slack_bolt.request.async_request import AsyncBoltRequest
8+
from slack_sdk.web.async_client import AsyncWebClient
9+
10+
from src.listeners.register import register_listeners
11+
12+
# Obviously fake values: no real credential, workspace or person is referenced here.
13+
FAKE_ENVIRONMENT = {"SLACK_BOT_TOKEN": "xoxb-not-a-real-bot-credential"}
14+
FAKE_CPF = '000.000.000-00'
15+
BOT_USER_ID = 'U000BOT'
16+
CHANNEL_ID = 'C000000'
17+
USER_ID = 'U000000'
18+
MESSAGE_TS = '1700000000.000100'
19+
EVENT_TS = '1700000000.000200'
20+
21+
22+
def build_event_request(event: dict) -> AsyncBoltRequest:
23+
body = {
24+
'team_id': 'T000000',
25+
'api_app_id': 'A000000',
26+
'type': 'event_callback',
27+
'event_id': 'Ev000000',
28+
'event_time': 1700000000,
29+
'event': event,
30+
}
31+
return AsyncBoltRequest(body=dumps(body), headers={'content-type': ['application/json']})
32+
33+
34+
def build_new_message_event(text: str) -> dict:
35+
return {
36+
'type': 'message',
37+
'channel': CHANNEL_ID,
38+
'channel_type': 'channel',
39+
'user': USER_ID,
40+
'text': text,
41+
'ts': MESSAGE_TS,
42+
'event_ts': MESSAGE_TS,
43+
}
44+
45+
46+
def build_edited_message_event(text: str) -> dict:
47+
return {
48+
'type': 'message',
49+
'subtype': 'message_changed',
50+
'channel': CHANNEL_ID,
51+
'channel_type': 'channel',
52+
'ts': EVENT_TS,
53+
'event_ts': EVENT_TS,
54+
'message': {'type': 'message', 'user': USER_ID, 'text': text, 'ts': MESSAGE_TS},
55+
'previous_message': {'type': 'message', 'user': USER_ID, 'text': 'nothing to see here', 'ts': MESSAGE_TS},
56+
}
57+
58+
59+
class MessageFlowTest(IsolatedAsyncioTestCase):
60+
"""Drives a synthetic Slack event through a real AsyncApp: registration, matching, detection and
61+
reply. Nothing is mocked below the Slack web client, so the real pattern engine runs."""
62+
63+
async def asyncSetUp(self) -> None:
64+
auth_test_result = MagicMock()
65+
auth_test_result.get.side_effect = {
66+
'user_id': BOT_USER_ID,
67+
'team_id': 'T000000',
68+
'bot_id': 'B000000',
69+
}.get
70+
auth_test_result.headers = {'x-oauth-scopes': 'chat:write'}
71+
72+
self.post_message_mock = self.patch_web_client('chat_postMessage', AsyncMock())
73+
self.patch_web_client('auth_test', AsyncMock(return_value=auth_test_result))
74+
75+
# process_before_response makes async_dispatch await the listeners instead of scheduling them,
76+
# so the assertions below do not race the event loop.
77+
with patch.dict(environ, FAKE_ENVIRONMENT):
78+
self.app = AsyncApp(request_verification_enabled=False, process_before_response=True)
79+
await register_listeners(self.app)
80+
81+
def patch_web_client(self, method: str, replacement: AsyncMock) -> AsyncMock:
82+
patcher = patch.object(AsyncWebClient, method, new=replacement)
83+
self.addCleanup(patcher.stop)
84+
return patcher.start()
85+
86+
async def test_new_message_with_sensitive_data_is_answered_in_thread_with_success(self):
87+
# Act
88+
response = await self.app.async_dispatch(build_event_request(build_new_message_event(f'meu {FAKE_CPF}')))
89+
90+
# Assert
91+
self.assertEqual(response.status, 200)
92+
self.post_message_mock.assert_awaited_once()
93+
call_args = self.post_message_mock.call_args
94+
self.assertEqual(call_args[1]['channel'], CHANNEL_ID)
95+
self.assertEqual(call_args[1]['thread_ts'], MESSAGE_TS)
96+
self.assertIn(f'<@{USER_ID}>', call_args[1]['text'])
97+
98+
async def test_new_message_without_sensitive_data_is_ignored_with_success(self):
99+
# Act
100+
response = await self.app.async_dispatch(build_event_request(build_new_message_event('bom dia a todos')))
101+
102+
# Assert
103+
self.assertEqual(response.status, 200)
104+
self.post_message_mock.assert_not_awaited()
105+
106+
async def test_edited_message_with_sensitive_data_is_answered_in_thread_with_success(self):
107+
# Act
108+
response = await self.app.async_dispatch(build_event_request(build_edited_message_event(f'meu {FAKE_CPF}')))
109+
110+
# Assert
111+
self.assertEqual(response.status, 200)
112+
self.post_message_mock.assert_awaited_once()
113+
call_args = self.post_message_mock.call_args
114+
self.assertEqual(call_args[1]['thread_ts'], MESSAGE_TS)
115+
self.assertIn(f'<@{USER_ID}>', call_args[1]['text'])
116+
117+
async def test_edited_message_without_sensitive_data_is_ignored_with_success(self):
118+
# Act
119+
response = await self.app.async_dispatch(build_event_request(build_edited_message_event('bom dia a todos')))
120+
121+
# Assert
122+
self.assertEqual(response.status, 200)
123+
self.post_message_mock.assert_not_awaited()

tests/listeners/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)