e52ddb7dd4
CodeQL / Analyze (python) (push) Failing after 1s
Linting and testing / build (3.11) (push) Failing after 0s
Package exe with PyInstaller - Windows / build (push) Failing after 2s
Linting and testing / build (3.10) (push) Failing after 1s
Linting and testing / minimal-install (push) Failing after 1s
Update sites rating and statistics / build (push) Failing after 1s
Build docker image and push to DockerHub / docker (push) Failing after 1s
Linting and testing / build (3.12) (push) Failing after 1s
Linting and testing / build (3.14) (push) Failing after 1s
Linting and testing / build (3.13) (push) Failing after 2s
296 lines
11 KiB
Python
296 lines
11 KiB
Python
"""Maigret main module test functions"""
|
|
|
|
import asyncio
|
|
import copy
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from maigret.maigret import self_check, maigret
|
|
from maigret.maigret import (
|
|
extract_ids_from_page,
|
|
extract_ids_from_results,
|
|
)
|
|
from maigret.checking import site_self_check
|
|
from maigret.sites import MaigretSite, MaigretDatabase
|
|
from maigret.result import MaigretCheckResult, MaigretCheckStatus
|
|
from tests.conftest import RESULTS_EXAMPLE
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.asyncio
|
|
async def test_self_check_db(test_db):
|
|
# initalize logger to debug
|
|
logger = Mock()
|
|
|
|
assert test_db.sites_dict['InvalidActive'].disabled is False
|
|
assert test_db.sites_dict['ValidInactive'].disabled is True
|
|
assert test_db.sites_dict['ValidActive'].disabled is False
|
|
assert test_db.sites_dict['InvalidInactive'].disabled is True
|
|
|
|
await self_check(
|
|
test_db, test_db.sites_dict, logger, silent=False, auto_disable=True
|
|
)
|
|
|
|
assert test_db.sites_dict['InvalidActive'].disabled is True
|
|
assert test_db.sites_dict['ValidInactive'].disabled is False
|
|
assert test_db.sites_dict['ValidActive'].disabled is False
|
|
assert test_db.sites_dict['InvalidInactive'].disabled is True
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.asyncio
|
|
async def test_self_check_no_progressbar(test_db):
|
|
"""Verify that no_progressbar=True disables the alive_bar in self_check."""
|
|
logger = Mock()
|
|
|
|
with patch('maigret.checking.alive_bar') as mock_alive_bar:
|
|
mock_bar = Mock()
|
|
mock_alive_bar.return_value.__enter__ = Mock(return_value=mock_bar)
|
|
mock_alive_bar.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
await self_check(
|
|
test_db, test_db.sites_dict, logger, silent=True,
|
|
no_progressbar=True,
|
|
)
|
|
|
|
# First call is the self-check progress bar; subsequent calls are
|
|
# from inner search() invocations.
|
|
self_check_call = mock_alive_bar.call_args_list[0]
|
|
_, kwargs = self_check_call
|
|
assert kwargs.get('title') == 'Self-checking'
|
|
assert kwargs.get('disable') is True
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.asyncio
|
|
async def test_self_check_progressbar_enabled_by_default(test_db):
|
|
"""Verify that alive_bar is enabled by default (no_progressbar=False)."""
|
|
logger = Mock()
|
|
|
|
with patch('maigret.checking.alive_bar') as mock_alive_bar:
|
|
mock_bar = Mock()
|
|
mock_alive_bar.return_value.__enter__ = Mock(return_value=mock_bar)
|
|
mock_alive_bar.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
await self_check(
|
|
test_db, test_db.sites_dict, logger, silent=True,
|
|
)
|
|
|
|
self_check_call = mock_alive_bar.call_args_list[0]
|
|
_, kwargs = self_check_call
|
|
assert kwargs.get('title') == 'Self-checking'
|
|
assert kwargs.get('disable') is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_site_self_check_handles_exception(test_db):
|
|
"""Verify that site_self_check catches unexpected exceptions and returns a valid result."""
|
|
logger = Mock()
|
|
sem = asyncio.Semaphore(1)
|
|
site = test_db.sites_dict['ValidActive']
|
|
|
|
with patch('maigret.checking.maigret', side_effect=RuntimeError("test crash")):
|
|
result = await site_self_check(site, logger, sem, test_db)
|
|
|
|
assert isinstance(result, dict)
|
|
assert "issues" in result
|
|
assert len(result["issues"]) > 0
|
|
assert any("Unexpected error" in issue for issue in result["issues"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_self_check_handles_task_exception(test_db):
|
|
"""Verify that self_check continues when individual site checks raise exceptions."""
|
|
logger = Mock()
|
|
|
|
with patch('maigret.checking.maigret', side_effect=RuntimeError("test crash")):
|
|
result = await self_check(
|
|
test_db, test_db.sites_dict, logger, silent=True,
|
|
no_progressbar=True,
|
|
)
|
|
|
|
assert isinstance(result, dict)
|
|
assert 'results' in result
|
|
assert len(result['results']) == len(test_db.sites_dict)
|
|
for r in result['results']:
|
|
assert 'site_name' in r
|
|
assert 'issues' in r
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.skip(reason="broken, fixme")
|
|
def test_maigret_results(test_db):
|
|
logger = Mock()
|
|
|
|
username = 'Skyeng'
|
|
loop = asyncio.get_event_loop()
|
|
results = loop.run_until_complete(
|
|
maigret(username, site_dict=test_db.sites_dict, logger=logger, timeout=30)
|
|
)
|
|
|
|
assert isinstance(results, dict)
|
|
|
|
reddit_site = results['Reddit']['site']
|
|
assert isinstance(reddit_site, MaigretSite)
|
|
|
|
assert reddit_site.json == {
|
|
'tags': ['news', 'social', 'us'],
|
|
'checkType': 'status_code',
|
|
'presenseStrs': ['totalKarma'],
|
|
'disabled': True,
|
|
'alexaRank': 17,
|
|
'url': 'https://www.reddit.com/user/{username}',
|
|
'urlMain': 'https://www.reddit.com/',
|
|
'usernameClaimed': 'blue',
|
|
'usernameUnclaimed': 'noonewouldeverusethis7',
|
|
}
|
|
|
|
del results['Reddit']['site']
|
|
del results['GooglePlayStore']['site']
|
|
|
|
reddit_status = results['Reddit']['status']
|
|
assert isinstance(reddit_status, MaigretCheckResult)
|
|
assert reddit_status.status == MaigretCheckStatus.ILLEGAL
|
|
|
|
playstore_status = results['GooglePlayStore']['status']
|
|
assert isinstance(playstore_status, MaigretCheckResult)
|
|
assert playstore_status.status == MaigretCheckStatus.CLAIMED
|
|
|
|
del results['Reddit']['status']
|
|
del results['GooglePlayStore']['status']
|
|
|
|
assert results['Reddit'].get('future') is None
|
|
del results['GooglePlayStore']['future']
|
|
del results['GooglePlayStore']['checker']
|
|
|
|
assert results == RESULTS_EXAMPLE
|
|
|
|
|
|
@pytest.mark.slow
|
|
def test_extract_ids_from_url(default_db):
|
|
assert default_db.extract_ids_from_url('https://www.reddit.com/user/test') == {
|
|
'test': 'username'
|
|
}
|
|
assert default_db.extract_ids_from_url('https://vk.com/id123') == {'123': 'vk_id'}
|
|
assert default_db.extract_ids_from_url('https://vk.com/ida123') == {
|
|
'ida123': 'username'
|
|
}
|
|
assert default_db.extract_ids_from_url(
|
|
'https://my.mail.ru/yandex.ru/dipres8904/'
|
|
) == {'dipres8904': 'username'}
|
|
assert default_db.extract_ids_from_url(
|
|
'https://reviews.yandex.ru/user/adbced123'
|
|
) == {'adbced123': 'yandex_public_id'}
|
|
|
|
|
|
@pytest.mark.slow
|
|
def test_extract_ids_from_page(test_db):
|
|
logger = Mock()
|
|
extract_ids_from_page('https://www.reddit.com/user/test', logger) == {
|
|
'test': 'username'
|
|
}
|
|
|
|
|
|
def test_extract_ids_from_results_aggregates_usernames_and_links(default_db):
|
|
"""Covers the recursive ID-extraction path flagged at maigret.py:946
|
|
(`# TODO: tests`, `if recursive_search_enabled: extract_ids_from_results(...)`).
|
|
|
|
The previous incarnation of this test was a bare expression
|
|
(`extract_ids_from_results(...) == {...}`) with no `assert`, so it silently
|
|
passed regardless of the return value. It also pinned the result against
|
|
`test_db` (tests/db.json), whose site set never matched the Reddit URL and
|
|
thus returned `{'test1': ...}` — silently dropping `test2`. Restored here as
|
|
a real assertion against `default_db`, which carries the Reddit URL pattern.
|
|
"""
|
|
TEST_EXAMPLE: dict = copy.deepcopy(RESULTS_EXAMPLE)
|
|
TEST_EXAMPLE['Reddit']['ids_usernames'] = {'test1': 'yandex_public_id'}
|
|
TEST_EXAMPLE['Reddit']['ids_links'] = ['https://www.reddit.com/user/test2']
|
|
|
|
assert extract_ids_from_results(TEST_EXAMPLE, default_db) == {
|
|
'test1': 'yandex_public_id',
|
|
'test2': 'username',
|
|
}
|
|
|
|
|
|
def test_extract_ids_from_results_merges_ids_usernames_across_sites(default_db):
|
|
"""ids_usernames from every site must be merged into the result dict,
|
|
keyed by username with the per-site id type as the value."""
|
|
example = {
|
|
'SiteA': {'ids_usernames': {'alice': 'username', 'bob': 'telegram'}},
|
|
'SiteB': {'ids_usernames': {'carol': 'username'}},
|
|
}
|
|
assert extract_ids_from_results(example, default_db) == {
|
|
'alice': 'username',
|
|
'bob': 'telegram',
|
|
'carol': 'username',
|
|
}
|
|
|
|
|
|
def test_extract_ids_from_results_skips_empty_site_entry(default_db):
|
|
"""A site whose result dict is empty (`{}`) must be skipped via
|
|
`if not dictionary: continue` rather than raising on the subsequent
|
|
`.get('ids_usernames')` / `.get('ids_links')` calls."""
|
|
example = {
|
|
'EmptySite': {},
|
|
'SiteA': {'ids_usernames': {'alice': 'username'}},
|
|
}
|
|
assert extract_ids_from_results(example, default_db) == {'alice': 'username'}
|
|
|
|
|
|
def test_extract_ids_from_results_empty_input_returns_empty(default_db):
|
|
"""No sites searched → no extracted ids. Guards against accidental
|
|
KeyError / iteration over None."""
|
|
assert extract_ids_from_results({}, default_db) == {}
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Ctrl+C handling (https://github.com/soxoj/maigret/issues from
|
|
# "two presses needed to exit + traceback"). The contract:
|
|
#
|
|
# 1. First Ctrl+C during a running search → cancel that search but proceed
|
|
# to report generation with whatever was collected.
|
|
# 2. Any KeyboardInterrupt that escapes to __main__.py (e.g. second Ctrl+C
|
|
# during the report write, or interrupt before the search loop runs)
|
|
# must exit with the conventional SIGINT code (130) and a one-line
|
|
# message — never a Python traceback.
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def test_main_entrypoint_handles_top_level_keyboard_interrupt_cleanly():
|
|
"""Verify the __main__.py wrapper around asyncio.run catches
|
|
KeyboardInterrupt and exits with code 130 + a clean message. We can't
|
|
SIGINT the test process itself without disrupting pytest's signal
|
|
handlers, so we drive __main__.py in a subprocess with asyncio.run
|
|
monkey-patched to raise KeyboardInterrupt directly."""
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
repo_root = Path(__file__).resolve().parents[1]
|
|
script = (
|
|
"import asyncio, sys, runpy\n"
|
|
# Simulate the second-Ctrl+C-during-asyncio.run path:
|
|
"def _raise(*a, **kw):\n"
|
|
" raise KeyboardInterrupt()\n"
|
|
"asyncio.run = _raise\n"
|
|
# run_module executes maigret/__main__.py with __name__ == '__main__'
|
|
"runpy.run_module('maigret', run_name='__main__')\n"
|
|
)
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", script],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(repo_root),
|
|
)
|
|
# 130 is the conventional SIGINT exit code (128 + SIGINT=2)
|
|
assert result.returncode == 130, (
|
|
f"expected exit 130, got {result.returncode}. "
|
|
f"stdout={result.stdout!r} stderr={result.stderr!r}"
|
|
)
|
|
# The user-facing message replaces the traceback
|
|
assert "Maigret interrupted" in result.stderr
|
|
# No Python traceback may leak through
|
|
assert "Traceback" not in result.stderr
|
|
assert "KeyboardInterrupt" not in result.stderr
|