154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
# Copyright (c) 2026 PowerOn AG
|
|
# All rights reserved.
|
|
"""
|
|
Unit tests for modules.auth.mfaService.
|
|
|
|
Tests TOTP generation, verification, encryption round-trip, and the
|
|
three-rule MFA obligation resolver.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
import pyotp
|
|
|
|
from modules.auth.mfaService import (
|
|
_generateSecret,
|
|
buildTotp,
|
|
generateSetup,
|
|
confirmSetup,
|
|
verifyCode,
|
|
isMfaRequired,
|
|
_isMfaRequireAdminsEnabled,
|
|
)
|
|
|
|
|
|
class TestTotpBasics:
|
|
def test_generateSecret_returns_base32(self):
|
|
secret = _generateSecret()
|
|
assert isinstance(secret, str)
|
|
assert len(secret) >= 16
|
|
|
|
def testbuildTotp_generates_valid_code(self):
|
|
secret = _generateSecret()
|
|
totp = buildTotp(secret)
|
|
code = totp.now()
|
|
assert len(code) == 6
|
|
assert code.isdigit()
|
|
|
|
def test_verifyCode_accepts_current_code(self):
|
|
secret = _generateSecret()
|
|
totp = buildTotp(secret)
|
|
code = totp.now()
|
|
encrypted = f"FAKE_ENC:{secret}"
|
|
|
|
with patch("modules.auth.mfaService.decryptSecret", return_value=secret):
|
|
assert verifyCode(encrypted, code) is True
|
|
|
|
def test_verifyCode_rejects_wrong_code(self):
|
|
secret = _generateSecret()
|
|
encrypted = f"FAKE_ENC:{secret}"
|
|
|
|
with patch("modules.auth.mfaService.decryptSecret", return_value=secret):
|
|
assert verifyCode(encrypted, "000000") is False
|
|
|
|
|
|
class TestGenerateSetup:
|
|
@patch("modules.auth.mfaService._encryptSecret", return_value="ENC_SECRET")
|
|
def test_returns_uri_and_encrypted_secret(self, _mock_enc):
|
|
result = generateSetup(userId="u1", username="testuser")
|
|
assert "encryptedSecret" in result
|
|
assert "provisioningUri" in result
|
|
assert result["encryptedSecret"] == "ENC_SECRET"
|
|
assert "otpauth://totp/" in result["provisioningUri"]
|
|
assert "PowerOn" in result["provisioningUri"]
|
|
|
|
|
|
class TestConfirmSetup:
|
|
def test_confirmSetup_with_valid_code(self):
|
|
secret = _generateSecret()
|
|
totp = buildTotp(secret)
|
|
code = totp.now()
|
|
|
|
with patch("modules.auth.mfaService.decryptSecret", return_value=secret):
|
|
assert confirmSetup("ENC", code) is True
|
|
|
|
def test_confirmSetup_with_invalid_code(self):
|
|
secret = _generateSecret()
|
|
with patch("modules.auth.mfaService.decryptSecret", return_value=secret):
|
|
assert confirmSetup("ENC", "999999") is False
|
|
|
|
def test_confirmSetup_handles_decryption_error(self):
|
|
with patch("modules.auth.mfaService.decryptSecret", side_effect=Exception("decrypt error")):
|
|
assert confirmSetup("BAD_ENC", "123456") is False
|
|
|
|
|
|
class TestIsMfaRequired:
|
|
def _makeUser(self, mfaEnabled=False, isSysAdmin=False, isPlatformAdmin=False):
|
|
u = MagicMock()
|
|
u.mfaEnabled = mfaEnabled
|
|
u.isSysAdmin = isSysAdmin
|
|
u.isPlatformAdmin = isPlatformAdmin
|
|
return u
|
|
|
|
def _makeMandate(self, mfaRequired=False):
|
|
m = MagicMock()
|
|
m.mfaRequired = mfaRequired
|
|
return m
|
|
|
|
def test_mfaEnabled_user_always_required(self):
|
|
user = self._makeUser(mfaEnabled=True)
|
|
assert isMfaRequired(user) is True
|
|
|
|
@patch("modules.auth.mfaService._isMfaRequireAdminsEnabled", return_value=True)
|
|
def test_sysadmin_with_config_key(self, _mock):
|
|
user = self._makeUser(isSysAdmin=True)
|
|
assert isMfaRequired(user) is True
|
|
|
|
@patch("modules.auth.mfaService._isMfaRequireAdminsEnabled", return_value=True)
|
|
def test_platformadmin_with_config_key(self, _mock):
|
|
user = self._makeUser(isPlatformAdmin=True)
|
|
assert isMfaRequired(user) is True
|
|
|
|
@patch("modules.auth.mfaService._isMfaRequireAdminsEnabled", return_value=False)
|
|
def test_admin_without_config_key_not_required(self, _mock):
|
|
user = self._makeUser(isSysAdmin=True)
|
|
assert isMfaRequired(user) is False
|
|
|
|
@patch("modules.auth.mfaService._isMfaRequireAdminsEnabled", return_value=False)
|
|
def test_mandate_with_mfaRequired(self, _mock):
|
|
user = self._makeUser()
|
|
mandate = self._makeMandate(mfaRequired=True)
|
|
assert isMfaRequired(user, mandates=[mandate]) is True
|
|
|
|
@patch("modules.auth.mfaService._isMfaRequireAdminsEnabled", return_value=False)
|
|
def test_mandate_without_mfaRequired(self, _mock):
|
|
user = self._makeUser()
|
|
mandate = self._makeMandate(mfaRequired=False)
|
|
assert isMfaRequired(user, mandates=[mandate]) is False
|
|
|
|
@patch("modules.auth.mfaService._isMfaRequireAdminsEnabled", return_value=False)
|
|
def test_regular_user_no_mandate_not_required(self, _mock):
|
|
user = self._makeUser()
|
|
assert isMfaRequired(user) is False
|
|
|
|
|
|
class TestConfigKey:
|
|
@patch("modules.auth.mfaService.APP_CONFIG")
|
|
def test_config_true(self, mock_cfg):
|
|
mock_cfg.get.return_value = "true"
|
|
assert _isMfaRequireAdminsEnabled() is True
|
|
|
|
@patch("modules.auth.mfaService.APP_CONFIG")
|
|
def test_config_false(self, mock_cfg):
|
|
mock_cfg.get.return_value = "false"
|
|
assert _isMfaRequireAdminsEnabled() is False
|
|
|
|
@patch("modules.auth.mfaService.APP_CONFIG")
|
|
def test_config_empty(self, mock_cfg):
|
|
mock_cfg.get.return_value = ""
|
|
assert _isMfaRequireAdminsEnabled() is False
|
|
|
|
@patch("modules.auth.mfaService.APP_CONFIG")
|
|
def test_config_one(self, mock_cfg):
|
|
mock_cfg.get.return_value = "1"
|
|
assert _isMfaRequireAdminsEnabled() is True
|