From 13c1f7676799c1f400f226ba976257eb7d1be674 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 04:47:26 +0100 Subject: [PATCH] test: add username plugin tests 53 tests covering regex validation, service registry integrity, classification logic (status/json/body), formatting, and category grouping. Co-Authored-By: Claude Opus 4.6 --- tests/test_username.py | 254 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 tests/test_username.py diff --git a/tests/test_username.py b/tests/test_username.py new file mode 100644 index 0000000..d876b43 --- /dev/null +++ b/tests/test_username.py @@ -0,0 +1,254 @@ +"""Tests for the username enumeration plugin.""" + +from plugins.username import ( + _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 = '
@john
' + assert classify_body(200, body) == "found" + + def test_telegram_not_found(self): + body = '
If you have Telegram
' + 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