|
8 | 8 | by testing context isolation relative to the current state. |
9 | 9 | """ |
10 | 10 |
|
| 11 | +import json |
| 12 | + |
11 | 13 | import pytest |
| 14 | +import yaml |
12 | 15 |
|
| 16 | +from tortoise.config import AppConfig, DBUrlConfig, TortoiseConfig |
13 | 17 | from tortoise.connection import ConnectionHandler |
14 | 18 | from tortoise.context import ( |
15 | 19 | TortoiseContext, |
16 | 20 | get_current_context, |
17 | 21 | require_context, |
| 22 | + set_global_context, |
18 | 23 | tortoise_test_context, |
19 | 24 | ) |
20 | 25 | from tortoise.exceptions import ConfigurationError |
21 | 26 |
|
22 | 27 |
|
| 28 | +def _clear_global_context(): |
| 29 | + """Clear the global context to ensure test isolation.""" |
| 30 | + import tortoise.context as ctx_module |
| 31 | + |
| 32 | + ctx_module._global_context = None |
| 33 | + |
| 34 | + |
| 35 | +@pytest.fixture(autouse=True) |
| 36 | +def cleanup_global_context(): |
| 37 | + """Fixture to clear global context before and after each test.""" |
| 38 | + _clear_global_context() |
| 39 | + yield |
| 40 | + _clear_global_context() |
| 41 | + |
| 42 | + |
23 | 43 | class TestTortoiseContextInstantiation: |
24 | 44 | """Test cases for TortoiseContext instantiation.""" |
25 | 45 |
|
@@ -198,6 +218,26 @@ async def test_apps_cleared_on_async_context_exit(self): |
198 | 218 | assert ctx.apps is None |
199 | 219 | assert ctx.inited is False |
200 | 220 |
|
| 221 | + def test_set_global_context_sets_global(self): |
| 222 | + """set_global_context sets the global context.""" |
| 223 | + ctx = TortoiseContext() |
| 224 | + set_global_context(ctx) |
| 225 | + |
| 226 | + # Verify global context is set |
| 227 | + import tortoise.context as ctx_module |
| 228 | + |
| 229 | + assert ctx_module._global_context is ctx |
| 230 | + |
| 231 | + def test_set_global_context_raises_when_already_set(self): |
| 232 | + """set_global_context raises ConfigurationError when already set.""" |
| 233 | + ctx1 = TortoiseContext() |
| 234 | + ctx2 = TortoiseContext() |
| 235 | + |
| 236 | + set_global_context(ctx1) |
| 237 | + |
| 238 | + with pytest.raises(ConfigurationError): |
| 239 | + set_global_context(ctx2) |
| 240 | + |
201 | 241 |
|
202 | 242 | class TestGetModel: |
203 | 243 | """Test cases for get_model method.""" |
@@ -271,6 +311,64 @@ async def test_init_raises_with_invalid_config_no_apps(self): |
271 | 311 |
|
272 | 312 | assert 'Config must define "apps" section' in str(exc_info.value) |
273 | 313 |
|
| 314 | + @pytest.mark.asyncio |
| 315 | + @pytest.mark.parametrize("serializer", [json.dumps, yaml.dump]) |
| 316 | + async def test_init_with_config_file(self, tmp_path, serializer): |
| 317 | + """init() with config_file (JSON/YAML) initializes context correctly.""" |
| 318 | + suffix = ".yaml" if serializer is yaml.dump else ".json" |
| 319 | + config_file = tmp_path / f"config{suffix}" |
| 320 | + |
| 321 | + config = TortoiseConfig( |
| 322 | + connections={"default": DBUrlConfig("sqlite://:memory:")}, |
| 323 | + apps={"models": AppConfig(models=["tests.testmodels"])}, |
| 324 | + ) |
| 325 | + config_dict = config.to_dict() |
| 326 | + config_file.write_text(serializer(config_dict)) |
| 327 | + |
| 328 | + async with TortoiseContext() as ctx: |
| 329 | + await ctx.init(config_file=str(config_file)) |
| 330 | + |
| 331 | + assert ctx.inited is True |
| 332 | + assert ctx._connections is not None |
| 333 | + conn = ctx.connections.get("default") |
| 334 | + assert conn is not None |
| 335 | + assert ctx.apps is not None |
| 336 | + |
| 337 | + @pytest.mark.asyncio |
| 338 | + async def test_init_raises_with_config_and_config_file(self, tmp_path): |
| 339 | + """init() raises when both config and config_file are provided.""" |
| 340 | + config = TortoiseConfig( |
| 341 | + connections={"default": DBUrlConfig("sqlite://:memory:")}, |
| 342 | + apps={"models": AppConfig(models=["tests.testmodels"])}, |
| 343 | + ) |
| 344 | + config_dict = config.to_dict() |
| 345 | + |
| 346 | + config_file = tmp_path / "config.json" |
| 347 | + config_file.write_text(json.dumps(config_dict)) |
| 348 | + |
| 349 | + ctx = TortoiseContext() |
| 350 | + |
| 351 | + with pytest.raises(ConfigurationError) as exc_info: |
| 352 | + await ctx.init( |
| 353 | + config={"connections": {}, "apps": {}}, |
| 354 | + config_file=str(config_file), |
| 355 | + ) |
| 356 | + |
| 357 | + assert "Cannot specify both 'config' and 'config_file'" in str(exc_info.value) |
| 358 | + |
| 359 | + @pytest.mark.asyncio |
| 360 | + async def test_init_raises_with_invalid_config_file_extension(self, tmp_path): |
| 361 | + """init() raises when config_file has unsupported extension.""" |
| 362 | + config_file = tmp_path / "config.txt" |
| 363 | + config_file.write_text("some config") |
| 364 | + |
| 365 | + ctx = TortoiseContext() |
| 366 | + |
| 367 | + with pytest.raises(ConfigurationError) as exc_info: |
| 368 | + await ctx.init(config_file=str(config_file)) |
| 369 | + |
| 370 | + assert "Unknown config extension .txt" in str(exc_info.value) |
| 371 | + |
274 | 372 |
|
275 | 373 | class TestGenerateSchemas: |
276 | 374 | """Test cases for generate_schemas method.""" |
@@ -467,6 +565,9 @@ async def test_sequential_contexts_isolated(self): |
467 | 565 | class TestTimezoneAndRouters: |
468 | 566 | """Test cases for timezone and routers configuration.""" |
469 | 567 |
|
| 568 | + class TestRouter: |
| 569 | + pass |
| 570 | + |
470 | 571 | def test_context_default_timezone_settings(self): |
471 | 572 | """Context has default timezone settings.""" |
472 | 573 | ctx = TortoiseContext() |
@@ -515,6 +616,98 @@ async def test_tortoise_test_context_with_timezone(self): |
515 | 616 | assert ctx.use_tz is True |
516 | 617 | assert ctx.timezone == "Asia/Tokyo" |
517 | 618 |
|
| 619 | + @pytest.mark.asyncio |
| 620 | + async def test_init_routers_empty_list(self): |
| 621 | + """_init_routers with empty list initializes correctly.""" |
| 622 | + async with TortoiseContext() as ctx: |
| 623 | + await ctx.init( |
| 624 | + db_url="sqlite://:memory:", |
| 625 | + modules={"models": ["tests.testmodels"]}, |
| 626 | + routers=[], |
| 627 | + ) |
| 628 | + |
| 629 | + assert ctx.routers == [] |
| 630 | + |
| 631 | + @pytest.mark.asyncio |
| 632 | + async def test_init_routers_with_type(self): |
| 633 | + """_init_routers accepts router type directly.""" |
| 634 | + |
| 635 | + async with TortoiseContext() as ctx: |
| 636 | + await ctx.init( |
| 637 | + db_url="sqlite://:memory:", |
| 638 | + modules={"models": ["tests.testmodels"]}, |
| 639 | + routers=[TestTimezoneAndRouters.TestRouter], |
| 640 | + ) |
| 641 | + |
| 642 | + assert ctx.routers == [TestTimezoneAndRouters.TestRouter] |
| 643 | + |
| 644 | + @pytest.mark.asyncio |
| 645 | + async def test_init_routers_with_string_path(self): |
| 646 | + """_init_routers accepts router as string path.""" |
| 647 | + async with TortoiseContext() as ctx: |
| 648 | + await ctx.init( |
| 649 | + db_url="sqlite://:memory:", |
| 650 | + modules={"models": ["tests.testmodels"]}, |
| 651 | + routers=["tortoise.router.ConnectionRouter"], |
| 652 | + ) |
| 653 | + |
| 654 | + from tortoise.router import ConnectionRouter |
| 655 | + |
| 656 | + assert ctx.routers == [ConnectionRouter] |
| 657 | + |
| 658 | + @pytest.mark.asyncio |
| 659 | + async def test_init_routers_mixed_types_and_strings(self): |
| 660 | + """_init_routers accepts mixed router types and strings.""" |
| 661 | + |
| 662 | + async with TortoiseContext() as ctx: |
| 663 | + await ctx.init( |
| 664 | + db_url="sqlite://:memory:", |
| 665 | + modules={"models": ["tests.testmodels"]}, |
| 666 | + routers=[TestTimezoneAndRouters.TestRouter, "tortoise.router.ConnectionRouter"], |
| 667 | + ) |
| 668 | + |
| 669 | + from tortoise.router import ConnectionRouter |
| 670 | + |
| 671 | + assert ctx.routers == [TestTimezoneAndRouters.TestRouter, ConnectionRouter] |
| 672 | + |
| 673 | + @pytest.mark.asyncio |
| 674 | + async def test_init_routers_invalid_router_type(self): |
| 675 | + """_init_routers raises on invalid router type.""" |
| 676 | + async with TortoiseContext() as ctx: |
| 677 | + with pytest.raises(ConfigurationError) as exc_info: |
| 678 | + await ctx.init( |
| 679 | + db_url="sqlite://:memory:", |
| 680 | + modules={"models": ["tests.testmodels"]}, |
| 681 | + routers=["not_a_valid_router_path"], |
| 682 | + ) |
| 683 | + |
| 684 | + assert "Can't import router" in str(exc_info.value) |
| 685 | + |
| 686 | + @pytest.mark.asyncio |
| 687 | + async def test_init_routers_invalid_router_item(self): |
| 688 | + """_init_routers raises when router is neither string nor type.""" |
| 689 | + async with TortoiseContext() as ctx: |
| 690 | + with pytest.raises(ConfigurationError) as exc_info: |
| 691 | + await ctx.init( |
| 692 | + db_url="sqlite://:memory:", |
| 693 | + modules={"models": ["tests.testmodels"]}, |
| 694 | + routers=[123], |
| 695 | + ) |
| 696 | + |
| 697 | + assert "Router must be either str or type" in str(exc_info.value) |
| 698 | + |
| 699 | + @pytest.mark.asyncio |
| 700 | + async def test_init_routers_none_uses_empty_list(self): |
| 701 | + """_init_routers with None uses empty list.""" |
| 702 | + async with TortoiseContext() as ctx: |
| 703 | + await ctx.init( |
| 704 | + db_url="sqlite://:memory:", |
| 705 | + modules={"models": ["tests.testmodels"]}, |
| 706 | + routers=None, |
| 707 | + ) |
| 708 | + |
| 709 | + assert ctx.routers == [] |
| 710 | + |
518 | 711 |
|
519 | 712 | class TestTortoiseConfigValidation: |
520 | 713 | """Test cases for TortoiseConfig validation in ctx.init().""" |
|
0 commit comments