# #region Test.AuthService [C:3] [TYPE Module] [SEMANTICS test,auth,service] # @BRIEF Verify AuthService contracts — authenticate_user, create_session, provision_adfs_user. # @RELATION BINDS_TO -> [AuthService] # @TEST_EDGE: user_not_found -> authenticate_user returns None # @TEST_EDGE: user_inactive -> authenticate_user returns None # @TEST_EDGE: wrong_password -> authenticate_user returns None # @TEST_EDGE: adfs_new_user -> provision_adfs_user creates User and commits # @TEST_EDGE: adfs_existing_user -> provision_adfs_user syncs roles without creating # @TEST_EDGE: adfs_no_upn_fallback_email -> username derived from email when upn absent from pathlib import Path import sys from contextlib import contextmanager from unittest.mock import MagicMock, call, patch sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) import pytest # ── Helpers (C1 — anchor pair only) ── # #region _make_user def _make_user(**kwargs): defaults = { "id": "user-001", "username": "alice", "email": "alice@example.com", "password_hash": "$2b$12$FAKEHASH", "is_active": True, "roles": [], "last_login": None, } defaults.update(kwargs) user = MagicMock() for k, v in defaults.items(): setattr(user, k, v) return user # #endregion _make_user # #region _make_role def _make_role(name: str): role = MagicMock() role.name = name return role # #endregion _make_role # #region _noop_belief_scope @contextmanager def _noop_belief_scope(_id): yield # #endregion _noop_belief_scope # ── Fixtures ── @pytest.fixture() def mock_db(): db = MagicMock() db.commit = MagicMock() db.refresh = MagicMock() db.add = MagicMock() return db # ════════════════════════════════════════════════════════════════ # authenticate_user # ════════════════════════════════════════════════════════════════ class TestAuthenticateUser: """authenticate_user — credential validation and account-state gating.""" # #region test_authenticate_user_success # @BRIEF Active user with correct password returns User and updates last_login. @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.verify_password", return_value=True) @patch("src.services.auth_service.AuthRepository") def test_authenticate_user_success(self, MockRepo, _vp, _bs, mock_db): from src.services.auth_service import AuthService expected_user = _make_user(username="alice", is_active=True) instance = MockRepo.return_value instance.get_user_by_username.return_value = expected_user svc = AuthService(mock_db) result = svc.authenticate_user("alice", "correct-password") assert result is expected_user assert result.last_login is not None mock_db.commit.assert_called_once() mock_db.refresh.assert_called_once_with(expected_user) # #endregion test_authenticate_user_success # #region test_authenticate_user_not_found # @BRIEF Returns None when username does not exist in the database. @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.verify_password") @patch("src.services.auth_service.AuthRepository") def test_authenticate_user_not_found(self, MockRepo, mock_vp, _bs, mock_db): from src.services.auth_service import AuthService MockRepo.return_value.get_user_by_username.return_value = None svc = AuthService(mock_db) result = svc.authenticate_user("ghost", "pw") assert result is None mock_vp.assert_not_called() # #endregion test_authenticate_user_not_found # #region test_authenticate_user_inactive # @BRIEF Returns None when user exists but is_active is False. @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.verify_password") @patch("src.services.auth_service.AuthRepository") def test_authenticate_user_inactive(self, MockRepo, mock_vp, _bs, mock_db): from src.services.auth_service import AuthService inactive_user = _make_user(is_active=False) MockRepo.return_value.get_user_by_username.return_value = inactive_user svc = AuthService(mock_db) result = svc.authenticate_user("alice", "pw") assert result is None mock_vp.assert_not_called() # #endregion test_authenticate_user_inactive # #region test_authenticate_user_wrong_password # @BRIEF Returns None when password hash verification fails. @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.verify_password", return_value=False) @patch("src.services.auth_service.AuthRepository") def test_authenticate_user_wrong_password(self, MockRepo, _vp, _bs, mock_db): from src.services.auth_service import AuthService active_user = _make_user(is_active=True) MockRepo.return_value.get_user_by_username.return_value = active_user svc = AuthService(mock_db) result = svc.authenticate_user("alice", "wrong-password") assert result is None mock_db.commit.assert_not_called() # #endregion test_authenticate_user_wrong_password # ════════════════════════════════════════════════════════════════ # create_session # ════════════════════════════════════════════════════════════════ class TestCreateSession: """create_session — JWT issuance with role-based scopes.""" # #region test_create_session_returns_bearer_token # @BRIEF Returns dict with access_token and token_type='bearer'. @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.create_access_token", return_value="jwt-token-abc") @patch("src.services.auth_service.AuthRepository") def test_create_session_returns_bearer_token(self, MockRepo, mock_jwt, _bs, mock_db): from src.services.auth_service import AuthService role_admin = _make_role("admin") role_editor = _make_role("editor") user = _make_user(username="alice", roles=[role_admin, role_editor]) svc = AuthService(mock_db) session = svc.create_session(user) assert session == {"access_token": "jwt-token-abc", "token_type": "bearer"} mock_jwt.assert_called_once_with( data={"sub": "alice", "scopes": ["admin", "editor"]} ) # #endregion test_create_session_returns_bearer_token # #region test_create_session_no_roles # @BRIEF User with no roles produces empty scopes list. @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.create_access_token", return_value="jwt-empty") @patch("src.services.auth_service.AuthRepository") def test_create_session_no_roles(self, MockRepo, mock_jwt, _bs, mock_db): from src.services.auth_service import AuthService user = _make_user(username="bob", roles=[]) svc = AuthService(mock_db) session = svc.create_session(user) assert session["token_type"] == "bearer" mock_jwt.assert_called_once_with(data={"sub": "bob", "scopes": []}) # #endregion test_create_session_no_roles # ════════════════════════════════════════════════════════════════ # provision_adfs_user # ════════════════════════════════════════════════════════════════ class TestProvisionAdfsUser: """provision_adfs_user — JIT provisioning and role synchronization.""" # #region test_provision_new_adfs_user # @BRIEF Creates new User when username not found, sets ADFS fields, commits. @patch("src.services.auth_service.log_security_event") @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.AuthRepository") def test_provision_new_adfs_user(self, MockRepo, _bs, mock_log, mock_db): from src.services.auth_service import AuthService MockRepo.return_value.get_user_by_username.return_value = None mapped_role = _make_role("ad-users") MockRepo.return_value.get_roles_by_ad_groups.return_value = [mapped_role] svc = AuthService(mock_db) user_info = {"upn": "carol@corp.com", "email": "carol@corp.com", "name": "Carol", "groups": ["AD-Users"]} result = svc.provision_adfs_user(user_info) mock_db.add.assert_called_once() new_user = mock_db.add.call_args[0][0] assert new_user.username == "carol@corp.com" assert new_user.auth_source == "ADFS" assert new_user.is_ad_user is True assert new_user.is_active is True assert new_user.roles == [mapped_role] mock_db.commit.assert_called_once() mock_db.refresh.assert_called_once() mock_log.assert_called_once_with("USER_PROVISIONED", "carol@corp.com", {"source": "ADFS"}) # #endregion test_provision_new_adfs_user # #region test_provision_existing_adfs_user # @BRIEF Existing user gets roles synced without creating a new record. @patch("src.services.auth_service.log_security_event") @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.AuthRepository") def test_provision_existing_adfs_user(self, MockRepo, _bs, mock_log, mock_db): from src.services.auth_service import AuthService existing = _make_user(username="dave@corp.com") MockRepo.return_value.get_user_by_username.return_value = existing role1 = _make_role("analysts") MockRepo.return_value.get_roles_by_ad_groups.return_value = [role1] svc = AuthService(mock_db) result = svc.provision_adfs_user({"upn": "dave@corp.com", "groups": ["AD-Analysts"]}) mock_db.add.assert_not_called() mock_log.assert_not_called() assert existing.roles == [role1] assert existing.last_login is not None mock_db.commit.assert_called_once() # #endregion test_provision_existing_adfs_user # #region test_provision_adfs_user_email_fallback # @BRIEF Uses 'email' claim as username when 'upn' is absent. @patch("src.services.auth_service.log_security_event") @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.AuthRepository") def test_provision_adfs_user_email_fallback(self, MockRepo, _bs, mock_log, mock_db): from src.services.auth_service import AuthService MockRepo.return_value.get_user_by_username.return_value = None MockRepo.return_value.get_roles_by_ad_groups.return_value = [] svc = AuthService(mock_db) result = svc.provision_adfs_user({"email": "eve@corp.com", "groups": []}) new_user = mock_db.add.call_args[0][0] assert new_user.username == "eve@corp.com" # #endregion test_provision_adfs_user_email_fallback # #region test_provision_adfs_user_no_groups # @BRIEF Empty groups list results in empty roles and no error. @patch("src.services.auth_service.log_security_event") @patch("src.services.auth_service.belief_scope", side_effect=_noop_belief_scope) @patch("src.services.auth_service.AuthRepository") def test_provision_adfs_user_no_groups(self, MockRepo, _bs, mock_log, mock_db): from src.services.auth_service import AuthService existing = _make_user(username="frank@corp.com") MockRepo.return_value.get_user_by_username.return_value = existing MockRepo.return_value.get_roles_by_ad_groups.return_value = [] svc = AuthService(mock_db) result = svc.provision_adfs_user({"upn": "frank@corp.com"}) assert existing.roles == [] mock_db.commit.assert_called_once() # #endregion test_provision_adfs_user_no_groups # #endregion Test.AuthService