Load plugins.username via importlib.util.spec_from_file_location since plugins/ is not a Python package on sys.path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
267 lines
8.5 KiB
Python
267 lines
8.5 KiB
Python
"""Tests for the username enumeration plugin."""
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# plugins/ is not a Python package -- load the module from file path
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"plugins.username", Path(__file__).resolve().parent.parent / "plugins" / "username.py",
|
|
)
|
|
_mod = importlib.util.module_from_spec(_spec)
|
|
sys.modules[_spec.name] = _mod
|
|
_spec.loader.exec_module(_mod)
|
|
|
|
from plugins.username import ( # noqa: E402
|
|
_BY_CATEGORY,
|
|
_BY_NAME,
|
|
_SERVICES,
|
|
_USERNAME_RE,
|
|
_Service,
|
|
classify,
|
|
classify_body,
|
|
classify_json,
|
|
classify_status,
|
|
format_list,
|
|
format_single,
|
|
format_summary,
|
|
)
|
|
|
|
|
|
class TestUsernameRegex:
|
|
def test_valid_simple(self):
|
|
assert _USERNAME_RE.match("john")
|
|
|
|
def test_valid_with_dots(self):
|
|
assert _USERNAME_RE.match("john.doe")
|
|
|
|
def test_valid_with_hyphens(self):
|
|
assert _USERNAME_RE.match("john-doe")
|
|
|
|
def test_valid_with_underscores(self):
|
|
assert _USERNAME_RE.match("john_doe")
|
|
|
|
def test_valid_numeric(self):
|
|
assert _USERNAME_RE.match("user123")
|
|
|
|
def test_valid_max_length(self):
|
|
assert _USERNAME_RE.match("a" * 39)
|
|
|
|
def test_invalid_too_long(self):
|
|
assert _USERNAME_RE.match("a" * 40) is None
|
|
|
|
def test_invalid_empty(self):
|
|
assert _USERNAME_RE.match("") is None
|
|
|
|
def test_invalid_spaces(self):
|
|
assert _USERNAME_RE.match("john doe") is None
|
|
|
|
def test_invalid_special_chars(self):
|
|
assert _USERNAME_RE.match("john@doe") is None
|
|
assert _USERNAME_RE.match("john!") is None
|
|
assert _USERNAME_RE.match("user#1") is None
|
|
|
|
def test_invalid_slash(self):
|
|
assert _USERNAME_RE.match("user/name") is None
|
|
|
|
|
|
class TestServiceRegistry:
|
|
def test_no_duplicate_names(self):
|
|
names = [s.name for s in _SERVICES]
|
|
assert len(names) == len(set(names))
|
|
|
|
def test_all_have_required_fields(self):
|
|
for svc in _SERVICES:
|
|
assert svc.name, "service must have a name"
|
|
assert svc.url, "service must have a url"
|
|
assert "{user}" in svc.url, f"{svc.name}: url must contain {{user}}"
|
|
assert svc.method in ("status", "json", "body"), (
|
|
f"{svc.name}: invalid method '{svc.method}'"
|
|
)
|
|
assert svc.category in ("dev", "social", "media", "other"), (
|
|
f"{svc.name}: invalid category '{svc.category}'"
|
|
)
|
|
|
|
def test_minimum_service_count(self):
|
|
assert len(_SERVICES) >= 20
|
|
|
|
def test_lookup_table_matches(self):
|
|
assert len(_BY_NAME) == len(_SERVICES)
|
|
|
|
def test_categories_cover_all(self):
|
|
total = sum(len(svcs) for svcs in _BY_CATEGORY.values())
|
|
assert total == len(_SERVICES)
|
|
|
|
def test_all_four_categories_present(self):
|
|
for cat in ("dev", "social", "media", "other"):
|
|
assert cat in _BY_CATEGORY, f"missing category: {cat}"
|
|
assert len(_BY_CATEGORY[cat]) > 0
|
|
|
|
|
|
class TestClassifyStatus:
|
|
def test_found(self):
|
|
assert classify_status(200) == "found"
|
|
|
|
def test_not_found(self):
|
|
assert classify_status(404) == "not_found"
|
|
|
|
def test_error_zero(self):
|
|
assert classify_status(0) == "error"
|
|
|
|
def test_error_redirect(self):
|
|
assert classify_status(302) == "error"
|
|
|
|
def test_error_server(self):
|
|
assert classify_status(500) == "error"
|
|
|
|
|
|
class TestClassifyJson:
|
|
def _svc(self, name: str) -> _Service:
|
|
return _Service(name=name, url="https://example.com/{user}", method="json", category="dev")
|
|
|
|
def test_github_found(self):
|
|
body = '{"login": "john", "id": 123}'
|
|
assert classify_json(self._svc("GitHub"), 200, body) == "found"
|
|
|
|
def test_github_not_found(self):
|
|
assert classify_json(self._svc("GitHub"), 404, "") == "not_found"
|
|
|
|
def test_gitlab_found(self):
|
|
body = '[{"id": 1, "username": "john"}]'
|
|
assert classify_json(self._svc("GitLab"), 200, body) == "found"
|
|
|
|
def test_gitlab_not_found(self):
|
|
assert classify_json(self._svc("GitLab"), 200, "[]") == "not_found"
|
|
|
|
def test_docker_hub_found(self):
|
|
body = '{"id": "abc123", "username": "john"}'
|
|
assert classify_json(self._svc("Docker Hub"), 200, body) == "found"
|
|
|
|
def test_docker_hub_not_found(self):
|
|
assert classify_json(self._svc("Docker Hub"), 404, "") == "not_found"
|
|
|
|
def test_keybase_found(self):
|
|
body = '{"them": [{"id": "abc"}]}'
|
|
assert classify_json(self._svc("Keybase"), 200, body) == "found"
|
|
|
|
def test_keybase_not_found(self):
|
|
body = '{"them": []}'
|
|
assert classify_json(self._svc("Keybase"), 200, body) == "not_found"
|
|
|
|
def test_devto_found(self):
|
|
body = '{"username": "john"}'
|
|
assert classify_json(self._svc("Dev.to"), 200, body) == "found"
|
|
|
|
def test_devto_not_found(self):
|
|
assert classify_json(self._svc("Dev.to"), 404, "") == "not_found"
|
|
|
|
def test_reddit_found(self):
|
|
body = '{"kind": "t2", "data": {"name": "john"}}'
|
|
assert classify_json(self._svc("Reddit"), 200, body) == "found"
|
|
|
|
def test_reddit_not_found(self):
|
|
body = '{"error": 404}'
|
|
assert classify_json(self._svc("Reddit"), 200, body) == "not_found"
|
|
|
|
def test_gravatar_found(self):
|
|
body = '{"entry": [{"id": "123"}]}'
|
|
assert classify_json(self._svc("Gravatar"), 200, body) == "found"
|
|
|
|
def test_gravatar_not_found(self):
|
|
assert classify_json(self._svc("Gravatar"), 404, "") == "not_found"
|
|
|
|
def test_error_on_bad_json(self):
|
|
assert classify_json(self._svc("GitHub"), 200, "not json") == "error"
|
|
|
|
def test_error_on_empty_body(self):
|
|
assert classify_json(self._svc("GitHub"), 200, "") == "error"
|
|
|
|
def test_error_on_zero_status(self):
|
|
assert classify_json(self._svc("GitHub"), 0, "") == "error"
|
|
|
|
|
|
class TestClassifyBody:
|
|
def test_telegram_found(self):
|
|
body = '<div class="tgme_page_extra">@john</div>'
|
|
assert classify_body(200, body) == "found"
|
|
|
|
def test_telegram_not_found(self):
|
|
body = '<div>If you have <strong>Telegram</strong></div>'
|
|
assert classify_body(200, body) == "not_found"
|
|
|
|
def test_error_on_zero_status(self):
|
|
assert classify_body(0, "") == "error"
|
|
|
|
|
|
class TestClassifyRouter:
|
|
def test_routes_to_status(self):
|
|
svc = _Service("Test", "https://example.com/{user}", "status", "other")
|
|
assert classify(svc, 200, "") == "found"
|
|
assert classify(svc, 404, "") == "not_found"
|
|
|
|
def test_routes_to_json(self):
|
|
svc = _Service("GitHub", "https://api.github.com/users/{user}", "json", "dev")
|
|
assert classify(svc, 200, '{"login": "x"}') == "found"
|
|
|
|
def test_routes_to_body(self):
|
|
svc = _Service("Telegram", "https://t.me/{user}", "body", "social")
|
|
assert classify(svc, 200, "tgme_page_extra") == "found"
|
|
|
|
|
|
class TestFormatSummary:
|
|
def test_all_found(self):
|
|
results = [("GitHub", "found"), ("GitLab", "found")]
|
|
lines = format_summary("john", results)
|
|
assert "2 found" in lines[0]
|
|
assert "0 not found" in lines[0]
|
|
assert "Found: GitHub, GitLab" in lines[1]
|
|
|
|
def test_mixed_results(self):
|
|
results = [
|
|
("GitHub", "found"),
|
|
("Reddit", "not_found"),
|
|
("Twitch", "error"),
|
|
]
|
|
lines = format_summary("john", results)
|
|
assert "1 found" in lines[0]
|
|
assert "1 not found" in lines[0]
|
|
assert "1 errors" in lines[0]
|
|
|
|
def test_none_found(self):
|
|
results = [("GitHub", "not_found"), ("Reddit", "not_found")]
|
|
lines = format_summary("john", results)
|
|
assert "0 found" in lines[0]
|
|
assert len(lines) == 1 # No "Found:" line
|
|
|
|
|
|
class TestFormatSingle:
|
|
def test_found_with_url(self):
|
|
svc = _BY_NAME["github"]
|
|
line = format_single(svc, "john", "found")
|
|
assert "found" in line
|
|
assert "https://github.com/john" in line
|
|
|
|
def test_not_found(self):
|
|
svc = _BY_NAME["github"]
|
|
line = format_single(svc, "john", "not_found")
|
|
assert "not_found" in line
|
|
assert "https://" not in line
|
|
|
|
def test_error(self):
|
|
svc = _BY_NAME["github"]
|
|
line = format_single(svc, "john", "error")
|
|
assert "error" in line
|
|
|
|
|
|
class TestFormatList:
|
|
def test_has_all_categories(self):
|
|
lines = format_list()
|
|
labels = [line.split(":")[0] for line in lines]
|
|
assert "Dev" in labels
|
|
assert "Social" in labels
|
|
assert "Media" in labels
|
|
assert "Other" in labels
|
|
|
|
def test_line_count(self):
|
|
assert len(format_list()) == 4
|