""" PgMemoryProvider 單元測試 =========================== 測試案例: - test_save_and_load: 儲存並讀取 - test_upsert: 更新已存在的資料 - test_delete: 刪除 - test_exists: 存在檢查 - test_sqlite_forbidden: SQLite 禁止測試 """ import pytest from unittest.mock import patch, AsyncMock, MagicMock from pydantic import BaseModel from lewooogo_data.providers.pg_memory import ( PgMemoryProvider, get_database_url, DEFAULT_PG_URL, ) class SampleModel(BaseModel): """測試用的 Pydantic Model""" id: str name: str value: int = 0 # ============================================================================= # 資料庫 URL 測試 # ============================================================================= class TestDatabaseUrl: """資料庫 URL 相關測試""" def test_default_url(self): """測試預設 URL""" with patch.dict("os.environ", {}, clear=True): url = get_database_url() assert url == DEFAULT_PG_URL def test_env_url(self): """測試環境變數 URL""" custom_url = "postgresql+asyncpg://user:pass@localhost:5432/mydb" with patch.dict("os.environ", {"DATABASE_URL": custom_url}): url = get_database_url() assert url == custom_url def test_sqlite_forbidden(self): """測試 SQLite 被禁止""" sqlite_url = "sqlite:///test.db" with patch.dict("os.environ", {"DATABASE_URL": sqlite_url}): url = get_database_url() # 統帥鐵律: SQLite 被禁止,應返回預設 PostgreSQL assert url == DEFAULT_PG_URL assert "sqlite" not in url.lower() # ============================================================================= # PgMemoryProvider 基本功能測試 # ============================================================================= class TestPgMemoryProviderBasic: """基本功能測試""" @pytest.mark.asyncio async def test_load(self, patch_pg_session, mock_session): """測試載入資料""" # Arrange provider = PgMemoryProvider(model_class=SampleModel) # 預先放入資料 expected_data = SampleModel(id="pg-1", name="PG Test", value=100) mock_session._storage["pg-1"] = expected_data # Act result = await provider.load("pg-1") # Assert assert result is not None assert result.id == "pg-1" assert result.name == "PG Test" assert result.value == 100 @pytest.mark.asyncio async def test_load_nonexistent(self, patch_pg_session, mock_session): """測試載入不存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # Act result = await provider.load("nonexistent") # Assert assert result is None @pytest.mark.asyncio async def test_save(self, patch_pg_session, mock_session): """測試儲存資料""" provider = PgMemoryProvider(model_class=SampleModel) data = SampleModel(id="pg-2", name="Save Test", value=200) # Act result = await provider.save("pg-2", data) # Assert assert result is True mock_session.add.assert_called_once() mock_session.commit.assert_called_once() @pytest.mark.asyncio async def test_save_ttl_ignored(self, patch_pg_session, mock_session): """測試儲存時 TTL 被忽略 (PostgreSQL 永久保存)""" provider = PgMemoryProvider(model_class=SampleModel) data = SampleModel(id="pg-3", name="TTL Ignored", value=300) # Act - 傳入 TTL 應被忽略 result = await provider.save("pg-3", data, ttl_seconds=3600) # Assert assert result is True mock_session.add.assert_called_once() # ============================================================================= # 刪除測試 # ============================================================================= class TestPgMemoryProviderDelete: """刪除相關測試""" @pytest.mark.asyncio async def test_delete_existing(self, patch_pg_session, mock_session): """測試刪除存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # 預先放入資料 existing_data = SampleModel(id="del-1", name="To Delete", value=0) mock_session._storage["del-1"] = existing_data # Act result = await provider.delete("del-1") # Assert assert result is True mock_session.delete.assert_called_once() mock_session.commit.assert_called() @pytest.mark.asyncio async def test_delete_nonexistent(self, patch_pg_session, mock_session): """測試刪除不存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # Act result = await provider.delete("nonexistent") # Assert assert result is False # ============================================================================= # 存在檢查測試 # ============================================================================= class TestPgMemoryProviderExists: """存在檢查測試""" @pytest.mark.asyncio async def test_exists_true(self, patch_pg_session, mock_session): """測試存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # 預先放入資料 existing_data = SampleModel(id="exist-1", name="Exists", value=1) mock_session._storage["exist-1"] = existing_data # Act result = await provider.exists("exist-1") # Assert assert result is True @pytest.mark.asyncio async def test_exists_false(self, patch_pg_session, mock_session): """測試不存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # Act result = await provider.exists("nonexistent") # Assert assert result is False # ============================================================================= # 更新測試 # ============================================================================= class TestPgMemoryProviderUpdate: """更新相關測試""" @pytest.mark.asyncio async def test_update_existing(self, patch_pg_session, mock_session): """測試更新存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # 預先放入資料 (使用可變物件) class MutableModel: def __init__(self): self.id = "upd-1" self.name = "Original" self.value = 10 existing = MutableModel() mock_session._storage["upd-1"] = existing # Act result = await provider.update("upd-1", {"value": 99, "name": "Updated"}) # Assert assert result is True assert existing.value == 99 assert existing.name == "Updated" mock_session.commit.assert_called() @pytest.mark.asyncio async def test_update_nonexistent(self, patch_pg_session, mock_session): """測試更新不存在的資料""" provider = PgMemoryProvider(model_class=SampleModel) # Act result = await provider.update("nonexistent", {"value": 999}) # Assert assert result is False # ============================================================================= # 錯誤處理測試 # ============================================================================= class TestPgMemoryProviderErrors: """錯誤處理測試""" @pytest.mark.asyncio async def test_load_without_model_class(self, patch_pg_session): """測試無 model_class 時 load 拋異常""" provider = PgMemoryProvider(model_class=None) # Act & Assert with pytest.raises(ValueError, match="model_class is required"): await provider.load("any-key") @pytest.mark.asyncio async def test_delete_without_model_class(self, patch_pg_session): """測試無 model_class 時 delete 拋異常""" provider = PgMemoryProvider(model_class=None) # Act & Assert with pytest.raises(ValueError, match="model_class is required"): await provider.delete("any-key") @pytest.mark.asyncio async def test_update_without_model_class(self, patch_pg_session): """測試無 model_class 時 update 拋異常""" provider = PgMemoryProvider(model_class=None) # Act & Assert with pytest.raises(ValueError, match="model_class is required"): await provider.update("any-key", {"value": 1})