Skip to content

Commit 8643afb

Browse files
authored
Merge pull request #195 from vmi98/tests/engine
Add tests for engine.py
2 parents 28e1f70 + bc5f603 commit 8643afb

File tree

2 files changed

+313
-1
lines changed

2 files changed

+313
-1
lines changed

tests/test_engine.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
"""
2+
user-scanner Engine Unit Tests
3+
4+
This suite tests the core orchestration logic of 'engine.py'.
5+
It verifies:
6+
1. Automated mode detection (Email vs Username) based on file paths.
7+
2. Metadata enrichment (Site Name, Category) of Result objects.
8+
3. Execution flow for both sync and async module validation functions.
9+
4. Error handling and result flattening for batch/category scans.
10+
11+
Note: These tests use Mocking and Stubs (via unittest.mock and pytest-monkeypatch)
12+
to isolate the engine from the network. No actual API requests are performed.
13+
14+
"""
15+
16+
17+
import pytest
18+
from types import ModuleType
19+
from user_scanner.core import engine
20+
from unittest.mock import Mock, AsyncMock, call
21+
from user_scanner.core.result import Result
22+
23+
24+
@pytest.fixture
25+
def anyio_backend():
26+
return 'asyncio'
27+
28+
29+
@pytest.fixture
30+
def github_stub():
31+
module = ModuleType("github")
32+
module.__file__ = "/some/path/email_scan/dev/github.py"
33+
return module
34+
35+
36+
@pytest.fixture
37+
def patreon_stub():
38+
module = ModuleType("patreon")
39+
module.__file__ = "/some/path/user_scan/creator/patreon.py"
40+
return module
41+
42+
43+
@pytest.fixture
44+
def medium_stub():
45+
module = ModuleType("medium")
46+
module.__file__ = "/some/path/user_scan/creator/medium.py"
47+
return module
48+
49+
50+
# check
51+
@pytest.mark.anyio
52+
async def test_async_module_func(monkeypatch, github_stub):
53+
async_mock = AsyncMock()
54+
async_mock.return_value = Result.taken()
55+
github_stub.validate_github = async_mock
56+
57+
result = await engine.check(github_stub, "some_email")
58+
59+
assert result.username == "some_email"
60+
assert result.is_email is True
61+
async_mock.assert_awaited_once_with("some_email")
62+
63+
64+
@pytest.mark.anyio
65+
async def test_sync_module_func(github_stub):
66+
sync_mock = Mock()
67+
sync_mock.return_value = Result.taken()
68+
github_stub.validate_github = sync_mock
69+
70+
result = await engine.check(github_stub, "some_email")
71+
72+
assert result.username == "some_email"
73+
assert result.is_email is True
74+
sync_mock.assert_called_once_with("some_email")
75+
76+
77+
@pytest.mark.anyio
78+
async def test_module_func_raise_exception(github_stub):
79+
async_mock = AsyncMock()
80+
async_mock.side_effect = Exception("Exception")
81+
github_stub.validate_github = async_mock
82+
83+
result = await engine.check(github_stub, "some_email")
84+
85+
assert result.username == "some_email"
86+
assert "Exception" in str(result.reason)
87+
async_mock.assert_awaited_once()
88+
89+
90+
@pytest.mark.anyio
91+
async def test_missing_validate_func():
92+
module = ModuleType("github")
93+
result = await engine.check(module, "some_username")
94+
95+
assert result.username == "some_username"
96+
assert "Function validate_github not found" in str(result.reason)
97+
98+
99+
@pytest.mark.anyio
100+
async def test_default_category_email(github_stub):
101+
github_stub.__file__ = "/some/path/email_scan/github.py"
102+
async_mock = AsyncMock()
103+
async_mock.return_value = Result.taken()
104+
github_stub.validate_github = async_mock
105+
106+
result = await engine.check(github_stub, "some_email")
107+
108+
assert result.is_email is True
109+
assert result.category == "Email"
110+
111+
112+
@pytest.mark.anyio
113+
async def test_default_category_username(github_stub):
114+
github_stub.__file__ = "/some/path/user_scan/github.py"
115+
async_mock = AsyncMock()
116+
async_mock.return_value = Result.taken()
117+
github_stub.validate_github = async_mock
118+
119+
result = await engine.check(github_stub, "some_username")
120+
121+
assert result.is_email is False
122+
assert result.category == "Username"
123+
124+
125+
@pytest.mark.anyio
126+
async def test_default_category(monkeypatch, github_stub):
127+
async_mock = AsyncMock()
128+
async_mock.return_value = Result.taken()
129+
github_stub.validate_github = async_mock
130+
131+
monkeypatch.setattr(engine, "find_category", lambda x: "Dev")
132+
result = await engine.check(github_stub, "some_email")
133+
134+
assert result.is_email is True
135+
assert result.category == "Dev"
136+
137+
138+
@pytest.mark.anyio
139+
async def test_metadata_enrichment(patreon_stub):
140+
async_mock = AsyncMock()
141+
async_mock.return_value = Result.taken()
142+
patreon_stub.validate_patreon = async_mock
143+
144+
result = await engine.check(patreon_stub, "some_username")
145+
146+
assert result.username == "some_username"
147+
assert result.is_email is False
148+
assert result.site_name == "Patreon"
149+
assert result.category == "Creator"
150+
151+
152+
# check_category
153+
@pytest.mark.anyio
154+
async def test_check_category(monkeypatch, patreon_stub, medium_stub):
155+
async_mock = AsyncMock()
156+
async_mock.return_value = Result.taken()
157+
158+
monkeypatch.setattr(engine, "load_categories", lambda is_email: {
159+
"creator": "/fake/path"
160+
})
161+
monkeypatch.setattr(engine, "load_modules",
162+
lambda x: [patreon_stub, medium_stub])
163+
monkeypatch.setattr(engine, "check", async_mock)
164+
165+
result = await engine.check_category("creator", "some_username", is_email=False)
166+
calls = [call(patreon_stub, "some_username"),
167+
call(medium_stub, "some_username")]
168+
169+
assert len(result) == 2
170+
async_mock.assert_has_calls(calls)
171+
172+
173+
@pytest.mark.anyio
174+
async def test_is_email_passed_correctly(monkeypatch, github_stub):
175+
async_mock = AsyncMock()
176+
async_mock.return_value = Result.taken()
177+
178+
sync_mock = Mock()
179+
sync_mock.return_value = {"dev": "/fake/path"}
180+
181+
monkeypatch.setattr(engine, "load_categories", sync_mock)
182+
monkeypatch.setattr(engine, "load_modules", lambda x: [github_stub])
183+
github_stub.validate_github = async_mock
184+
185+
result = await engine.check_category("dev", "some_email")
186+
187+
assert len(result) == 1
188+
sync_mock.assert_called_once_with(is_email=True)
189+
190+
191+
@pytest.mark.anyio
192+
async def test_case_insensitive_category_match(monkeypatch, github_stub):
193+
async_mock = AsyncMock()
194+
async_mock.return_value = Result.taken()
195+
196+
monkeypatch.setattr(engine, "load_categories", lambda is_email: {
197+
"dev": "/fake/path"
198+
})
199+
monkeypatch.setattr(engine, "load_modules", lambda x: [github_stub])
200+
github_stub.validate_github = async_mock
201+
202+
result = await engine.check_category("Dev", "some_email")
203+
204+
assert len(result) == 1
205+
async_mock.assert_awaited_once_with("some_email")
206+
207+
208+
@pytest.mark.anyio
209+
async def test_empty_category_list(monkeypatch):
210+
monkeypatch.setattr(engine, "load_categories", lambda is_email: {
211+
"dev": "/fake/path"
212+
})
213+
monkeypatch.setattr(engine, "load_modules", lambda x: [])
214+
result = await engine.check_category("dev", "some_email")
215+
216+
assert result == []
217+
218+
219+
@pytest.mark.anyio
220+
async def test_category_not_found_email(monkeypatch):
221+
monkeypatch.setattr(engine, "load_categories",
222+
lambda is_email: {"dev": "/fake/path"})
223+
with pytest.raises(ValueError) as exc_info:
224+
await engine.check_category("unknown", "some_email")
225+
226+
assert "email_scan" in str(exc_info.value)
227+
228+
229+
@pytest.mark.anyio
230+
async def test_category_not_found_username(monkeypatch):
231+
monkeypatch.setattr(engine, "load_categories",
232+
lambda is_email: {"dev": "/fake/path"})
233+
with pytest.raises(ValueError) as exc_info:
234+
await engine.check_category("unknown", "some_username", is_email=False)
235+
236+
assert "user_scan" in str(exc_info.value)
237+
238+
239+
# check all
240+
@pytest.mark.anyio
241+
async def test_check_all(monkeypatch):
242+
async_mock = AsyncMock()
243+
async_mock.return_value = [Result.taken(), Result.available()]
244+
monkeypatch.setattr(engine, "load_categories", lambda is_email: {
245+
"creator": "/fake/path",
246+
"dev": "/fake/path2"
247+
})
248+
monkeypatch.setattr(engine, "check_category", async_mock)
249+
250+
result = await engine.check_all("some_email")
251+
calls = [call("creator", "some_email", True),
252+
call("dev", "some_email", True)]
253+
254+
assert len(result) == 4
255+
async_mock.assert_has_calls(calls)
256+
257+
258+
@pytest.mark.anyio
259+
async def test_check_all_email_passed_correct(monkeypatch):
260+
async_mock = AsyncMock()
261+
async_mock.return_value = [Result.taken(), Result.available()]
262+
sync_mock = Mock()
263+
sync_mock.return_value = {
264+
"creator": "/fake/path",
265+
"dev": "/fake/path2"
266+
}
267+
268+
monkeypatch.setattr(engine, "load_categories", sync_mock)
269+
monkeypatch.setattr(engine, "check_category", async_mock)
270+
271+
await engine.check_all("some_username", is_email=False)
272+
calls = [call("creator", "some_username", False),
273+
call("dev", "some_username", False)]
274+
275+
sync_mock.assert_called_once_with(is_email=False)
276+
async_mock.assert_has_calls(calls, any_order=False)
277+
278+
279+
@pytest.mark.anyio
280+
async def test_check_all_nested_lis_flatten(monkeypatch):
281+
async_mock = AsyncMock()
282+
async_mock.side_effect = [[Result.taken(), Result.available()],
283+
[Result.taken()]]
284+
monkeypatch.setattr(engine, "load_categories", lambda is_email: {
285+
"creator": "/fake/path",
286+
"dev": "/fake/path2"
287+
})
288+
monkeypatch.setattr(engine, "check_category", async_mock)
289+
290+
result = await engine.check_all("some_email")
291+
292+
assert len(result) == 3
293+
294+
295+
@pytest.mark.anyio
296+
async def test_check_all_category_exception(monkeypatch):
297+
async def check_category_side_effect(cat_name, target, is_email=True):
298+
if cat_name == "dev":
299+
raise ValueError("Failed")
300+
return [Result.taken(), Result.available()]
301+
302+
monkeypatch.setattr(engine, "load_categories", lambda is_email: {
303+
"creator": "/fake/path",
304+
"dev": "/fake/path2"
305+
})
306+
monkeypatch.setattr(engine, "check_category", check_category_side_effect)
307+
308+
with pytest.raises(ValueError) as exc_info:
309+
await engine.check_all("some_email")
310+
311+
assert "Failed" in str(exc_info.value)

user_scanner/core/engine.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import inspect
23
from typing import List
34
from types import ModuleType
45

@@ -23,7 +24,7 @@ async def check(module: ModuleType, target: str) -> Result:
2324
)
2425

2526
try:
26-
if asyncio.iscoroutinefunction(func):
27+
if inspect.iscoroutinefunction(func):
2728
result = await func(target)
2829
else:
2930
result = func(target)

0 commit comments

Comments
 (0)