56 lines
1.8 KiB
Python
56 lines
1.8 KiB
Python
"""
|
|
tests/test_panel_mfa.py
|
|
-----------------------
|
|
Behaviour tests for the superadmin-panel two-factor helper (control/mfa.py).
|
|
|
|
The panel routes are thin wrappers over this module (and mirror app/utils/mfa.py,
|
|
which is covered by tests/test_mfa.py). These tests lock the panel helper's
|
|
crypto behaviour independently so the control-plane mirror can't silently drift.
|
|
|
|
Pure logic — no control DB, no MySQL, no Flask app required.
|
|
"""
|
|
|
|
import pyotp
|
|
|
|
|
|
def test_totp_round_trip_and_rejection():
|
|
from control import mfa
|
|
s = mfa.new_secret()
|
|
assert mfa.verify_totp(s, pyotp.TOTP(s).now()) is True
|
|
assert mfa.verify_totp(s, '000000') is False
|
|
assert mfa.verify_totp(s, 'abc') is False
|
|
assert mfa.verify_totp('', '123456') is False
|
|
assert mfa.verify_totp(s, None) is False
|
|
|
|
|
|
def test_recovery_codes_are_hashed_single_use():
|
|
from control import mfa
|
|
plaintext, hashed = mfa.generate_recovery_codes()
|
|
assert len(plaintext) == len(hashed) == 10
|
|
# Never stored in plaintext.
|
|
assert all(p not in hashed for p in plaintext)
|
|
|
|
matched, remaining = mfa.check_and_consume_recovery(hashed, plaintext[3])
|
|
assert matched is True
|
|
assert len(remaining) == 9
|
|
|
|
# Consumed code cannot be reused.
|
|
reused, _ = mfa.check_and_consume_recovery(remaining, plaintext[3])
|
|
assert reused is False
|
|
|
|
# A different, unused code still works.
|
|
ok, remaining2 = mfa.check_and_consume_recovery(remaining, plaintext[0])
|
|
assert ok is True
|
|
assert len(remaining2) == 8
|
|
|
|
|
|
def test_provisioning_uri_and_qr_are_well_formed():
|
|
from control import mfa
|
|
s = mfa.new_secret()
|
|
uri = mfa.provisioning_uri(s, 'admin@example.com')
|
|
assert uri.startswith('otpauth://totp/')
|
|
assert 'JQC%20Admin' in uri or 'JQC Admin' in uri
|
|
svg = mfa.qr_svg(uri)
|
|
assert svg.lstrip().startswith('<?xml')
|
|
assert '<svg' in svg
|