python-neva 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {python_neva-0.2.1 → python_neva-0.3.0}/PKG-INFO +3 -1
  2. {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/app.py +3 -3
  3. python_neva-0.3.0/neva/testing/__init__.py +5 -0
  4. python_neva-0.3.0/neva/testing/fixtures.py +41 -0
  5. python_neva-0.3.0/neva/testing/http.py +21 -0
  6. python_neva-0.3.0/neva/testing/test_case.py +20 -0
  7. {python_neva-0.2.1 → python_neva-0.3.0}/pyproject.toml +9 -2
  8. {python_neva-0.2.1 → python_neva-0.3.0}/ruff.toml +3 -0
  9. python_neva-0.3.0/specifications/future_ideas.md +5 -0
  10. python_neva-0.3.0/tests/__init__.py +1 -0
  11. python_neva-0.3.0/tests/conftest.py +5 -0
  12. python_neva-0.3.0/tests/test_example_usage.py +188 -0
  13. python_neva-0.3.0/tests/test_fixtures.py +135 -0
  14. python_neva-0.3.0/tests/test_test_case.py +110 -0
  15. {python_neva-0.2.1 → python_neva-0.3.0}/uv.lock +14 -3
  16. python_neva-0.2.1/docs/README.md +0 -580
  17. {python_neva-0.2.1 → python_neva-0.3.0}/.envrc +0 -0
  18. {python_neva-0.2.1 → python_neva-0.3.0}/.gitignore +0 -0
  19. {python_neva-0.2.1 → python_neva-0.3.0}/.pre-commit-config.yaml +0 -0
  20. {python_neva-0.2.1 → python_neva-0.3.0}/.python-version +0 -0
  21. {python_neva-0.2.1 → python_neva-0.3.0}/README.md +0 -0
  22. {python_neva-0.2.1 → python_neva-0.3.0}/neva/__init__.py +0 -0
  23. {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/__init__.py +0 -0
  24. {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/application.py +0 -0
  25. {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/config.py +0 -0
  26. {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/facade.py +0 -0
  27. {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/service_provider.py +0 -0
  28. {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/__init__.py +0 -0
  29. {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/base_providers.py +0 -0
  30. {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/loader.py +0 -0
  31. {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/provider.py +0 -0
  32. {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/repository.py +0 -0
  33. {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/__init__.py +0 -0
  34. {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/config.py +0 -0
  35. {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/manager.py +0 -0
  36. {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/provider.py +0 -0
  37. {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/repository.py +0 -0
  38. {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/__init__.py +0 -0
  39. {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/dispatcher.py +0 -0
  40. {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/event.py +0 -0
  41. {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/event_registry.py +0 -0
  42. {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/interface.py +0 -0
  43. {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/listener.py +0 -0
  44. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/__init__.py +0 -0
  45. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/logging/__init__.py +0 -0
  46. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/logging/manager.py +0 -0
  47. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/logging/provider.py +0 -0
  48. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/middleware/__init__.py +0 -0
  49. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/middleware/correlation.py +0 -0
  50. {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/middleware/profiler.py +0 -0
  51. {python_neva-0.2.1 → python_neva-0.3.0}/neva/py.typed +0 -0
  52. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/__init__.py +0 -0
  53. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/accessors.py +0 -0
  54. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/__init__.py +0 -0
  55. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/app.py +0 -0
  56. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/app.pyi +0 -0
  57. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/config.py +0 -0
  58. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/config.pyi +0 -0
  59. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/log.py +0 -0
  60. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/log.pyi +0 -0
  61. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/results.py +0 -0
  62. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/strconv.py +0 -0
  63. {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/time.py +0 -0
  64. {python_neva-0.2.1 → python_neva-0.3.0}/specifications/events.md +0 -0
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: dependency-injector>=4.48.3
7
7
  Requires-Dist: dishka>=1.7.2
8
8
  Requires-Dist: fastapi-cli>=0.0.16
9
9
  Requires-Dist: fastapi[all]>=0.124.0
10
+ Requires-Dist: flexmock>=0.13.0
10
11
  Requires-Dist: pyinstrument>=5.1.1
12
+ Requires-Dist: pytest>=9.0.2
11
13
  Requires-Dist: structlog>=25.5.0
12
14
  Requires-Dist: tortoise-orm[accel]>=0.25.3
@@ -13,7 +13,6 @@ from typing import Any, Callable
13
13
  import fastapi
14
14
  import dishka
15
15
  from dishka.integrations.fastapi import setup_dishka
16
- from starlette.middleware import Middleware
17
16
  from starlette.routing import BaseRoute
18
17
  from starlette.types import StatefulLifespan, StatelessLifespan
19
18
 
@@ -29,7 +28,7 @@ class App(fastapi.FastAPI):
29
28
  self,
30
29
  *,
31
30
  routes: list[BaseRoute] | None = None,
32
- middlewares: Sequence[Middleware] | None = None,
31
+ middlewares: Sequence[type] | None = None,
33
32
  lifespan: StatelessLifespan["App"] | StatefulLifespan["App"] | None = None,
34
33
  config_path: str | Path | None = None,
35
34
  ) -> None:
@@ -59,8 +58,9 @@ class App(fastapi.FastAPI):
59
58
  docs_url=config.get("app.docs_url", default="/docs").unwrap(),
60
59
  redoc_url=config.get("app.redoc_url", default="/redoc").unwrap(),
61
60
  lifespan=self._create_lifespan(),
62
- middleware=middlewares,
63
61
  )
62
+ for middleware in middlewares or []:
63
+ self.add_middleware(middleware)
64
64
 
65
65
  setup_dishka(self.application.container, app=self)
66
66
 
@@ -0,0 +1,5 @@
1
+ """Testing utilities for Neva applications."""
2
+
3
+ from neva.testing.test_case import TestCase
4
+
5
+ __all__ = ["TestCase"]
@@ -0,0 +1,41 @@
1
+ """Fixtures for testing."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from pathlib import Path
5
+ import pytest
6
+
7
+ from neva.arch import Application, App
8
+
9
+
10
+ @pytest.fixture
11
+ def test_config(tmp_path: Path) -> Path:
12
+ """Returns a test config directory."""
13
+ config_dir = tmp_path / "config"
14
+ config_dir.mkdir()
15
+ _ = (config_dir / "app.py").write_text(
16
+ """config = { "name": "TestApp", "debug": True, "environment": "testing"}"""
17
+ )
18
+ _ = (config_dir / "providers.py").write_text("""config = { "providers": []}""")
19
+ return config_dir
20
+
21
+
22
+ @pytest.fixture
23
+ async def application(test_config: Path) -> AsyncIterator[Application]:
24
+ """Pytest fixture for app lifecycle.
25
+
26
+ Yields:
27
+ AsyncIterator[Application]: The application instance.
28
+ """
29
+ app = Application(config_path=test_config)
30
+ async with app.lifespan():
31
+ yield app
32
+
33
+
34
+ @pytest.fixture
35
+ def webapp(test_config: Path) -> App:
36
+ """Pytest fixture for the HTTP Neva app.
37
+
38
+ Returns:
39
+ App: The Neva application instance.
40
+ """
41
+ return App(config_path=test_config)
@@ -0,0 +1,21 @@
1
+ """HTTP Test helpers."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from httpx import ASGITransport, AsyncClient
5
+ import pytest
6
+
7
+ from neva.arch import App
8
+
9
+
10
+ @pytest.fixture
11
+ async def http_client(webapp: App) -> AsyncIterator[AsyncClient]:
12
+ """An async httpx client to test the application.
13
+
14
+ Yields:
15
+ An async httpx client.
16
+ """
17
+ async with AsyncClient(
18
+ transport=ASGITransport(webapp),
19
+ base_url="http://localhost:8000",
20
+ ) as client:
21
+ yield client
@@ -0,0 +1,20 @@
1
+ """Base test case class."""
2
+
3
+ from typing import TypeVar
4
+
5
+ import pytest
6
+
7
+ from neva.arch import Application
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class TestCase:
13
+ """Base test case with auto-injected app and helper methods."""
14
+
15
+ app: Application
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _inject_app(self, application: Application) -> None:
19
+ """Auto-inject app fixture into self.app."""
20
+ self.app = application
@@ -7,7 +7,7 @@ packages = ["neva"]
7
7
 
8
8
  [project]
9
9
  name = "python-neva"
10
- version = "0.2.1"
10
+ version = "0.3.0"
11
11
  description = "Add your description here"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
@@ -16,7 +16,9 @@ dependencies = [
16
16
  "dishka>=1.7.2",
17
17
  "fastapi-cli>=0.0.16",
18
18
  "fastapi[all]>=0.124.0",
19
+ "flexmock>=0.13.0",
19
20
  "pyinstrument>=5.1.1",
21
+ "pytest>=9.0.2",
20
22
  "structlog>=25.5.0",
21
23
  "tortoise-orm[accel]>=0.25.3",
22
24
  ]
@@ -27,7 +29,6 @@ dev = [
27
29
  "poethepoet>=0.38.0",
28
30
  "polyfactory>=3.1.0",
29
31
  "pre-commit>=4.5.0",
30
- "pytest>=9.0.2",
31
32
  "pytest-asyncio>=0.25.3",
32
33
  "pytest-benchmark>=5.2.3",
33
34
  "pytest-cov>=7.0.0",
@@ -41,6 +42,12 @@ asyncio_default_fixture_loop_scope = "function"
41
42
  testpaths = ["tests"]
42
43
 
43
44
  [tool.poe.tasks]
45
+ # Code quality
44
46
  lint = "ruff check"
45
47
  fmt = "ruff format"
46
48
  tc = "ty check"
49
+
50
+ # Tests
51
+ test = "pytest"
52
+ test-cov = "pytest --cov=neva --cov-report=term-missing"
53
+ test-full = "test-cov tests/"
@@ -23,6 +23,9 @@ select = [
23
23
  preview = true
24
24
  ignore = ["D203", "D213", "RUF029"]
25
25
 
26
+ [lint.per-file-ignores]
27
+ "tests/*" = ["D", "S101"]
28
+
26
29
  [lint.isort]
27
30
  force-single-line = false
28
31
  lines-after-imports = 2
@@ -0,0 +1,5 @@
1
+ # Future ideas
2
+
3
+ ## Testing
4
+
5
+ Provide scoped fixtures for testing in addition to the one we have.
@@ -0,0 +1 @@
1
+ """Neva test suite."""
@@ -0,0 +1,5 @@
1
+ """Test configuration and fixtures."""
2
+
3
+ # Import fixtures from neva.testing
4
+ # This makes them available to all tests
5
+ pytest_plugins = ["neva.testing.fixtures"]
@@ -0,0 +1,188 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from neva.arch import Application
6
+ from neva.config import ConfigRepository
7
+
8
+
9
+ class TestBasicUsage:
10
+ @pytest.mark.asyncio
11
+ async def test_simple_config_access(
12
+ self,
13
+ application: Application,
14
+ ) -> None:
15
+ config = application.make(ConfigRepository).unwrap()
16
+
17
+ app_name = config.get("app.name").unwrap()
18
+ assert app_name == "TestApp"
19
+
20
+ is_debug = config.get("app.debug").unwrap()
21
+ assert is_debug is True
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_config_with_default(
25
+ self,
26
+ application: Application,
27
+ ) -> None:
28
+ config = application.make(ConfigRepository).unwrap()
29
+
30
+ missing = config.get("app.missing_key", "default_value").unwrap()
31
+ assert missing == "default_value"
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_multiple_config_keys(
35
+ self,
36
+ application: Application,
37
+ ) -> None:
38
+ config = application.make(ConfigRepository).unwrap()
39
+
40
+ assert config.get("app.name").unwrap() == "TestApp"
41
+ assert config.get("app.debug").unwrap() is True
42
+ assert config.get("app.environment").unwrap() == "testing"
43
+
44
+
45
+ class TestCustomConfig:
46
+ @pytest.fixture
47
+ def test_config(
48
+ self,
49
+ tmp_path: Path,
50
+ ) -> Path:
51
+ config_dir = tmp_path / "config"
52
+ config_dir.mkdir()
53
+
54
+ _ = (config_dir / "app.py").write_text(
55
+ """
56
+ config = {
57
+ "name": "CustomApp",
58
+ "debug": False,
59
+ "environment": "custom",
60
+ "custom_feature": True,
61
+ }
62
+ """
63
+ )
64
+
65
+ _ = (config_dir / "providers.py").write_text("""config = {"providers": []}""")
66
+
67
+ return config_dir
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_uses_custom_config(
71
+ self,
72
+ application: Application,
73
+ ) -> None:
74
+ config = application.make(ConfigRepository).unwrap()
75
+
76
+ assert config.get("app.name").unwrap() == "CustomApp"
77
+ assert config.get("app.debug").unwrap() is False
78
+ assert config.get("app.environment").unwrap() == "custom"
79
+ assert config.get("app.custom_feature").unwrap() is True
80
+
81
+
82
+ class TestConfigManipulation:
83
+ @pytest.mark.asyncio
84
+ async def test_add_database_config(
85
+ self,
86
+ test_config: Path,
87
+ ) -> None:
88
+ _ = (test_config / "database.py").write_text(
89
+ """
90
+ config = {
91
+ "default": "sqlite",
92
+ "connections": {
93
+ "sqlite": {
94
+ "driver": "sqlite",
95
+ "database": ":memory:",
96
+ }
97
+ }
98
+ }
99
+ """
100
+ )
101
+
102
+ app = Application(config_path=test_config)
103
+
104
+ async with app.lifespan():
105
+ config = app.make(ConfigRepository).unwrap()
106
+
107
+ # Verify database config is loaded
108
+ assert config.get("database.default").unwrap() == "sqlite"
109
+ assert config.get("database.connections.sqlite.driver").unwrap() == "sqlite"
110
+ assert (
111
+ config.get("database.connections.sqlite.database").unwrap()
112
+ == ":memory:"
113
+ )
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_add_multiple_config_files(
117
+ self,
118
+ test_config: Path,
119
+ ) -> None:
120
+ _ = (test_config / "cache.py").write_text(
121
+ """config = {"driver": "memory", "ttl": 3600}"""
122
+ )
123
+
124
+ _ = (test_config / "logging.py").write_text(
125
+ """config = {"level": "DEBUG", "format": "json"}"""
126
+ )
127
+
128
+ app = Application(config_path=test_config)
129
+
130
+ async with app.lifespan():
131
+ config = app.make(ConfigRepository).unwrap()
132
+
133
+ # Verify both configs are loaded
134
+ assert config.get("cache.driver").unwrap() == "memory"
135
+ assert config.get("cache.ttl").unwrap() == 3600
136
+ assert config.get("logging.level").unwrap() == "DEBUG"
137
+ assert config.get("logging.format").unwrap() == "json"
138
+
139
+
140
+ class TestAppLifecycle:
141
+ @pytest.mark.asyncio
142
+ async def test_manual_app_creation(
143
+ self,
144
+ test_config: Path,
145
+ ) -> None:
146
+ app = Application(config_path=test_config)
147
+
148
+ async with app.lifespan():
149
+ config = app.make(ConfigRepository).unwrap()
150
+ assert config.get("app.name").unwrap() == "TestApp"
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_multiple_apps_in_one_test(
154
+ self,
155
+ test_config: Path,
156
+ ) -> None:
157
+ app1 = Application(config_path=test_config)
158
+ async with app1.lifespan():
159
+ config1 = app1.make(ConfigRepository).unwrap()
160
+ assert config1.get("app.name").unwrap() == "TestApp"
161
+
162
+ app2 = Application(config_path=test_config)
163
+ async with app2.lifespan():
164
+ config2 = app2.make(ConfigRepository).unwrap()
165
+ assert config2.get("app.name").unwrap() == "TestApp"
166
+
167
+ assert app1 is not app2
168
+
169
+
170
+ class TestIsolation:
171
+ @pytest.mark.asyncio
172
+ async def test_isolation_test_one(
173
+ self,
174
+ application: Application,
175
+ ) -> None:
176
+ config = application.make(ConfigRepository).unwrap()
177
+
178
+ original_name = config.get("app.name").unwrap()
179
+ assert original_name == "TestApp"
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_isolation_test_two(
183
+ self,
184
+ application: Application,
185
+ ) -> None:
186
+ config = application.make(ConfigRepository).unwrap()
187
+
188
+ assert config.get("app.name").unwrap() == "TestApp"
@@ -0,0 +1,135 @@
1
+ """Test the testing fixtures themselves."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from neva.arch import Application
8
+ from neva.config import ConfigRepository
9
+
10
+
11
+ class TestTestConfig:
12
+ """Test the test_config fixture."""
13
+
14
+ def test_creates_config_directory(
15
+ self,
16
+ test_config: Path,
17
+ ) -> None:
18
+ """Test that test_config creates a config directory."""
19
+ assert test_config.exists()
20
+ assert test_config.is_dir()
21
+ assert test_config.name == "config"
22
+
23
+ def test_creates_app_config_file(
24
+ self,
25
+ test_config: Path,
26
+ ) -> None:
27
+ """Test that app.py config file is created."""
28
+ app_config = test_config / "app.py"
29
+ assert app_config.exists()
30
+ assert app_config.is_file()
31
+
32
+ content = app_config.read_text()
33
+ assert "TestApp" in content
34
+ assert "debug" in content
35
+ assert "testing" in content
36
+
37
+ def test_creates_providers_config_file(
38
+ self,
39
+ test_config: Path,
40
+ ) -> None:
41
+ """Test that providers.py config file is created."""
42
+ providers_config = test_config / "providers.py"
43
+ assert providers_config.exists()
44
+ assert providers_config.is_file()
45
+
46
+ content = providers_config.read_text()
47
+ assert "providers" in content
48
+
49
+
50
+ class TestAppFixture:
51
+ @pytest.mark.asyncio
52
+ async def test_app_is_application_instance(
53
+ self,
54
+ application: Application,
55
+ ) -> None:
56
+ assert isinstance(application, Application)
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_app_config_is_loaded(
60
+ self,
61
+ application: Application,
62
+ ) -> None:
63
+ config_result = application.make(ConfigRepository)
64
+ assert config_result.is_ok
65
+
66
+ config = config_result.unwrap()
67
+ assert config is not None
68
+
69
+ assert config.get("app.name").unwrap() == "TestApp"
70
+ assert config.get("app.debug").unwrap() is True
71
+ assert config.get("app.environment").unwrap() == "testing"
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_app_is_fresh_per_test_first(
75
+ self,
76
+ application: Application,
77
+ ) -> None:
78
+ type(application).__test_marker = "first_test"
79
+ assert type(application).__test_marker == "first_test"
80
+
81
+ @pytest.mark.asyncio
82
+ async def test_app_is_fresh_per_test_second(
83
+ self,
84
+ application: Application,
85
+ ) -> None:
86
+ assert not hasattr(type(application), "__test_marker")
87
+
88
+ @pytest.mark.asyncio
89
+ async def test_app_can_resolve_services(
90
+ self,
91
+ application: Application,
92
+ ) -> None:
93
+ config_result = application.make(ConfigRepository)
94
+ assert config_result.is_ok
95
+
96
+ config = config_result.unwrap()
97
+ assert config is not None
98
+ assert isinstance(config, ConfigRepository)
99
+
100
+ @pytest.mark.asyncio
101
+ async def test_multiple_resolutions_return_same_instance(
102
+ self,
103
+ application: Application,
104
+ ) -> None:
105
+ config1 = application.make(ConfigRepository).unwrap()
106
+ config2 = application.make(ConfigRepository).unwrap()
107
+
108
+ assert config1 is config2
109
+
110
+
111
+ class TestFixtureIntegration:
112
+ @pytest.mark.asyncio
113
+ async def test_test_config_and_app_work_together(
114
+ self,
115
+ test_config: Path,
116
+ application: Application,
117
+ ) -> None:
118
+ config = application.make(ConfigRepository).unwrap()
119
+
120
+ assert config.get("app.name").unwrap() == "TestApp"
121
+
122
+ app_config_file = test_config / "app.py"
123
+ assert app_config_file.exists()
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_can_customize_config_in_test(self, test_config: Path) -> None:
127
+ _ = (test_config / "custom.py").write_text(
128
+ """config = {"custom_key": "custom_value"}"""
129
+ )
130
+
131
+ app = Application(config_path=test_config)
132
+
133
+ async with app.lifespan():
134
+ config = app.make(ConfigRepository).unwrap()
135
+ assert config.get("custom.custom_key").unwrap() == "custom_value"
@@ -0,0 +1,110 @@
1
+ """Test the TestCase base class."""
2
+
3
+ from pathlib import Path
4
+ import pytest
5
+
6
+ from neva import UnwrapError
7
+ from neva.arch import Application
8
+ from neva.config import ConfigRepository
9
+ from neva.testing import TestCase
10
+
11
+
12
+ class TestTestCase(TestCase):
13
+ @pytest.mark.asyncio
14
+ async def test_app_is_injected(self) -> None:
15
+ assert hasattr(self, "app")
16
+ assert isinstance(self.app, Application)
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_make_resolves_services(self) -> None:
20
+ config = self.app.make(ConfigRepository).unwrap()
21
+
22
+ assert config is not None
23
+ assert isinstance(config, ConfigRepository)
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_make_unwraps_result(self) -> None:
27
+ config = self.app.make(ConfigRepository).unwrap()
28
+
29
+ name = config.get("app.name").unwrap()
30
+ assert name == "TestApp"
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_make_raises_on_error(self) -> None:
34
+ class NonExistentService:
35
+ pass
36
+
37
+ with pytest.raises(UnwrapError):
38
+ _ = self.app.make(NonExistentService).unwrap()
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_app_isolation_between_tests_first(self) -> None:
42
+ self.app.__test_marker = "first" # type: ignore
43
+ assert self.app.__test_marker == "first" # type: ignore
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_app_isolation_between_tests_second(self) -> None:
47
+ assert not hasattr(self.app, "__test_marker")
48
+
49
+
50
+ class TestTestCaseWithCustomConfig(TestCase):
51
+ @pytest.fixture
52
+ def test_config(self, tmp_path: Path) -> Path:
53
+ config_dir = tmp_path / "config"
54
+ config_dir.mkdir()
55
+
56
+ _ = (config_dir / "app.py").write_text(
57
+ """
58
+ config = {
59
+ "name": "CustomTestApp",
60
+ "custom_feature": True,
61
+ }
62
+ """
63
+ )
64
+
65
+ _ = (config_dir / "providers.py").write_text('config = {"providers": []}')
66
+
67
+ return config_dir
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_uses_custom_config(self) -> None:
71
+ config = self.app.make(ConfigRepository).unwrap()
72
+
73
+ assert config.get("app.name").unwrap() == "CustomTestApp"
74
+ assert config.get("app.custom_feature").unwrap() is True
75
+
76
+
77
+ class TestTestCaseInheritance:
78
+ class CustomTestCase(TestCase):
79
+ def get_app_name(self) -> str:
80
+ config = self.app.make(ConfigRepository).unwrap()
81
+ return config.get("app.name").unwrap()
82
+
83
+ def is_debug_mode(self) -> bool:
84
+ config = self.app.make(ConfigRepository).unwrap()
85
+ return config.get("app.debug").unwrap()
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_custom_helpers_work(self) -> None:
89
+ test_case = self.CustomTestCase()
90
+
91
+ from neva.arch import Application
92
+ from pathlib import Path
93
+ from tempfile import TemporaryDirectory
94
+
95
+ with TemporaryDirectory() as tmp:
96
+ config_dir = Path(tmp) / "config"
97
+ config_dir.mkdir()
98
+ _ = (config_dir / "app.py").write_text(
99
+ """config = {"name": "TestApp", "debug":"""
100
+ + """ True, "environment": "testing"}"""
101
+ )
102
+ _ = (config_dir / "providers.py").write_text('config = {"providers": []}')
103
+
104
+ app = Application(config_path=config_dir)
105
+ async with app.lifespan():
106
+ test_case.app = app
107
+
108
+ # Use custom helpers
109
+ assert test_case.get_app_name() == "TestApp"
110
+ assert test_case.is_debug_mode() is True