# retoor import base64 import pytest from devplacepy.services.community import runtime from devplacepy.services.community.discoverer import LoginLearner from devplacepy.services.community.extract import detect_captcha, find_otp_form, parse_html, totp from devplacepy.services.community.runtime import ( CaptchaRequired, Collector, LoginFailed, SecondFactorRequired, code_challenge, ) from tests.conftest import run_async from tests.unit.services.community.fakes import FakeFetcher async def no_sleep(_seconds): return None RFC_SECRET = base64.b32encode(b"12345678901234567890").decode() @pytest.mark.parametrize( "moment,expected", [(59, "94287082"), (1111111109, "07081804"), (1111111111, "14050471"), (1234567890, "89005924"), (2000000000, "69279037"), (20000000000, "65353130")], ) def test_totp_matches_the_rfc_6238_vectors(moment, expected): assert totp(RFC_SECRET, moment, digits=8) == expected def test_totp_default_is_six_digits_zero_padded_and_tolerates_formatting(): code = totp(RFC_SECRET.lower(), 59) assert len(code) == 6 and code.isdigit() spaced = " ".join(RFC_SECRET[index : index + 4] for index in range(0, len(RFC_SECRET), 4)) assert totp(spaced, 59) == code assert totp(RFC_SECRET.rstrip("="), 59) == code assert totp(RFC_SECRET, 59 + 30) != code assert totp(RFC_SECRET, 59) == totp(RFC_SECRET, 30) OTP_PAGE = """
""" LOGIN_PAGE = """
""" HOME_OPEN = "

Welcome back to the forum, you are signed in

" def test_otp_forms_and_captcha_markers_are_recognised(): form = find_otp_form(parse_html(OTP_PAGE), "https://x.test/login") assert form == {"action": "https://x.test/2fa", "method": "POST", "field": "otp_code", "fields": {"csrf": "t2"}} assert find_otp_form(parse_html(LOGIN_PAGE), "https://x.test/") is None named = parse_html('
') assert find_otp_form(named, "https://x.test/")["field"] == "verification_code" hidden_token = parse_html('
') assert find_otp_form(hidden_token, "https://x.test/") is None assert detect_captcha('
') and detect_captcha("
") assert not detect_captcha("

no puzzle here

") def form_schema(): return { "community": "club", "origin": "club.test", "base_url": "https://club.test/", "feeds": [], "limits": {"delay_seconds": 0, "retries": 1}, "login": { "mode": "form", "url": "https://club.test/login", "action": "https://club.test/login", "method": "POST", "fields": {"csrf": "t1"}, "username_field": "user", "password_field": "pass", "identity": "username", }, } def club(state, challenge=True, good_code="123456"): def login(data): if data.get("user") == "bob" and data.get("pass") == "pw": if challenge: return (200, OTP_PAGE) state["in"] = True return (200, "ok") return (200, LOGIN_PAGE) def second(data): state["codes"] = state.get("codes", []) + [data.get("otp_code")] state["in"] = data.get("otp_code") == good_code and data.get("csrf") == "t2" return (200, "ok" if state["in"] else OTP_PAGE) return { "https://club.test/login": LOGIN_PAGE, "https://club.test/": lambda: HOME_OPEN if state.get("in") else LOGIN_PAGE, "post:https://club.test/login": login, "post:https://club.test/2fa": second, } def test_a_form_login_with_a_second_factor_submits_the_current_code(monkeypatch): monkeypatch.setattr(runtime, "totp", lambda secret: "123456") state = {} collector = Collector(FakeFetcher(club(state)), form_schema(), sleep=no_sleep, credentials={"username": "bob", "password": "pw", "totp": RFC_SECRET}) assert run_async(collector.login()) is True assert state["in"] and state["codes"] == ["123456"] def test_a_second_factor_without_a_secret_says_what_is_missing(): collector = Collector(FakeFetcher(club({})), form_schema(), sleep=no_sleep, credentials={"username": "bob", "password": "pw"}) with pytest.raises(SecondFactorRequired, match="authenticator secret"): run_async(collector.login()) def test_a_rejected_code_is_reported_not_retried(monkeypatch): monkeypatch.setattr(runtime, "totp", lambda secret: "000000") state = {} collector = Collector(FakeFetcher(club(state)), form_schema(), sleep=no_sleep, credentials={"username": "bob", "password": "pw", "totp": RFC_SECRET}) with pytest.raises(SecondFactorRequired, match="rejected the one-time code"): run_async(collector.login()) assert state["codes"] == ["000000"] def test_a_login_without_a_second_factor_is_unchanged(): state = {} collector = Collector(FakeFetcher(club(state, challenge=False)), form_schema(), sleep=no_sleep, credentials={"username": "bob", "password": "pw"}) assert run_async(collector.login()) is True and state["in"] def test_wrong_password_still_fails_as_before(): collector = Collector(FakeFetcher(club({})), form_schema(), sleep=no_sleep, credentials={"username": "bob", "password": "nope"}) with pytest.raises(LoginFailed, match="rejected the stored credentials"): run_async(collector.login()) def test_a_captcha_on_the_login_page_is_never_attempted(): routes = club({}) routes["https://club.test/login"] = LOGIN_PAGE.replace("not json")}) assert run_async(LoginLearner(html_only).find_json("https://api.test/", {"username": "bob", "password": "x"})) is None