"""Tests for get_user_password fallback chain in password_utils.""" from unittest.mock import MagicMock, patch import flask from local_deep_research.web.auth.password_utils import ( get_user_password, resolve_user_password, ) MODULE = "local_deep_research.web.auth.password_utils" DB_MANAGER = "local_deep_research.database.encrypted_db.db_manager" class TestGetUserPassword: """Tests for the 3-source fallback chain in get_user_password.""" def test_returns_password_from_session_store(self): """Path 1: session_password_store has a password for the user.""" app = flask.Flask(__name__) with app.test_request_context(): mock_store = MagicMock() mock_store.get_session_password.return_value = "stored_pass" with ( patch(f"{MODULE}.session", {"session_id": "sid123"}), patch( f"{MODULE}.session_password_store", mock_store, create=True, ), patch( "local_deep_research.database.session_passwords.session_password_store", mock_store, ), ): result = get_user_password("alice") assert result == "stored_pass" mock_store.get_session_password.assert_called_once_with( "alice", "sid123" ) def test_returns_password_from_flask_g(self): """Path 2: no session password, but g.user_password is set.""" app = flask.Flask(__name__) with app.test_request_context(): mock_store = MagicMock() mock_store.get_session_password.return_value = None with ( patch(f"{MODULE}.session", {"session_id": "sid1"}), patch( f"{MODULE}.session_password_store", mock_store, create=True, ), patch( "local_deep_research.database.session_passwords.session_password_store", mock_store, ), patch(f"{MODULE}.g") as mock_g, ): mock_g.user_password = "g_password" # Make getattr work on the mock type(mock_g).user_password = "g_password" result = get_user_password("bob") assert result == "g_password" def test_returns_password_from_temp_auth_token(self): """Path 3: temp_auth_token matches username in temp_auth_store.""" app = flask.Flask(__name__) with app.test_request_context(): mock_session_store = MagicMock() mock_session_store.get_session_password.return_value = None mock_temp_store = MagicMock() mock_temp_store.peek_auth.return_value = ("carol", "temp_pass") mock_session = MagicMock() mock_session.get.side_effect = lambda k, *a: { "session_id": "sid2", "temp_auth_token": "tok42", }.get(k, *a) with ( patch(f"{MODULE}.session", mock_session), patch( f"{MODULE}.session_password_store", mock_session_store, create=True, ), patch( "local_deep_research.database.session_passwords.session_password_store", mock_session_store, ), patch(f"{MODULE}.g") as mock_g, patch( "local_deep_research.database.temp_auth.temp_auth_store", mock_temp_store, ), ): mock_g.user_password = None type(mock_g).user_password = None result = get_user_password("carol") assert result == "temp_pass" mock_temp_store.peek_auth.assert_called_once_with("tok42") def test_returns_none_when_nothing_found(self): """Path 4: none of the sources have a password.""" app = flask.Flask(__name__) with app.test_request_context(): mock_session_store = MagicMock() mock_session_store.get_session_password.return_value = None mock_temp_store = MagicMock() mock_temp_store.peek_auth.return_value = None mock_session = MagicMock() mock_session.get.side_effect = lambda k, *a: { "session_id": "sid3", "temp_auth_token": "tok99", }.get(k, *a) with ( patch(f"{MODULE}.session", mock_session), patch( f"{MODULE}.session_password_store", mock_session_store, create=True, ), patch( "local_deep_research.database.session_passwords.session_password_store", mock_session_store, ), patch(f"{MODULE}.g") as mock_g, patch( "local_deep_research.database.temp_auth.temp_auth_store", mock_temp_store, ), ): mock_g.user_password = None type(mock_g).user_password = None result = get_user_password("dave") assert result is None def test_returns_none_when_temp_auth_username_mismatch(self): """Path 5: temp_auth_token exists but username doesn't match.""" app = flask.Flask(__name__) with app.test_request_context(): mock_session_store = MagicMock() mock_session_store.get_session_password.return_value = None mock_temp_store = MagicMock() # Token belongs to "other_user", not "eve" mock_temp_store.peek_auth.return_value = ( "other_user", "some_pass", ) mock_session = MagicMock() mock_session.get.side_effect = lambda k, *a: { "session_id": "sid4", "temp_auth_token": "tok77", }.get(k, *a) with ( patch(f"{MODULE}.session", mock_session), patch( f"{MODULE}.session_password_store", mock_session_store, create=True, ), patch( "local_deep_research.database.session_passwords.session_password_store", mock_session_store, ), patch(f"{MODULE}.g") as mock_g, patch( "local_deep_research.database.temp_auth.temp_auth_store", mock_temp_store, ), ): mock_g.user_password = None type(mock_g).user_password = None result = get_user_password("eve") assert result is None def test_no_session_id_skips_session_store(self): """Path 6: no session_id so session password store is skipped.""" app = flask.Flask(__name__) with app.test_request_context(): mock_session_store = MagicMock() # Should never be called mock_session_store.get_session_password.return_value = ( "SHOULD_NOT_USE" ) with ( patch(f"{MODULE}.session", {"session_id": None}), patch( f"{MODULE}.session_password_store", mock_session_store, create=True, ), patch( "local_deep_research.database.session_passwords.session_password_store", mock_session_store, ), patch(f"{MODULE}.g") as mock_g, ): mock_g.user_password = "from_g" type(mock_g).user_password = "from_g" result = get_user_password("frank") assert result == "from_g" mock_session_store.get_session_password.assert_not_called() class TestResolveUserPassword: """Tests for resolve_user_password, the shared research-entry guard. Returns (password, session_expired); session_expired is True only when the DB is encrypted AND no password is available (#4457). """ def test_password_present_is_not_expired(self): """A real password -> (password, False), regardless of encryption.""" with ( patch(f"{MODULE}.get_user_password", return_value="pw"), patch(DB_MANAGER) as mock_db, ): mock_db.has_encryption = True password, session_expired = resolve_user_password("alice") assert password == "pw" assert session_expired is False def test_no_password_encrypted_is_expired(self): """Encrypted DB + no password -> (None, True): caller must reject.""" with ( patch(f"{MODULE}.get_user_password", return_value=None), patch(DB_MANAGER) as mock_db, ): mock_db.has_encryption = True password, session_expired = resolve_user_password("alice") assert password is None assert session_expired is True def test_no_password_unencrypted_is_not_expired(self): """Unencrypted DB + no password -> (None, False): caller proceeds.""" with ( patch(f"{MODULE}.get_user_password", return_value=None), patch(DB_MANAGER) as mock_db, ): mock_db.has_encryption = False password, session_expired = resolve_user_password("alice") assert password is None assert session_expired is False