simple-module-test 0.0.17__tar.gz → 0.0.19__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.
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/PKG-INFO +9 -5
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/README.md +5 -1
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/pyproject.toml +4 -4
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/simple_module_test/__init__.py +6 -3
- simple_module_test-0.0.19/simple_module_test/fixtures.py +197 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/simple_module_test/plugin.py +15 -0
- simple_module_test-0.0.19/tests/test_plugin_fixtures.py +62 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/.gitignore +0 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/LICENSE +0 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/simple_module_test/app_factory.py +0 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/simple_module_test/fake_events.py +0 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/simple_module_test/session_cookie.py +0 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/tests/test_app_factory.py +0 -0
- {simple_module_test-0.0.17 → simple_module_test-0.0.19}/tests/test_fake_events.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_test
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Shared pytest fixtures (app, client, db_session, authenticated_client) for writing simple_module module tests
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -25,9 +25,9 @@ Requires-Dist: fastapi>=0.115
|
|
|
25
25
|
Requires-Dist: httpx>=0.27
|
|
26
26
|
Requires-Dist: pytest-asyncio>=0.24
|
|
27
27
|
Requires-Dist: pytest>=8.0
|
|
28
|
-
Requires-Dist: simple-module-core==0.0.
|
|
29
|
-
Requires-Dist: simple-module-db==0.0.
|
|
30
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
28
|
+
Requires-Dist: simple-module-core==0.0.19
|
|
29
|
+
Requires-Dist: simple-module-db==0.0.19
|
|
30
|
+
Requires-Dist: simple-module-hosting==0.0.19
|
|
31
31
|
Requires-Dist: sqlalchemy>=2.0
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
@@ -47,11 +47,15 @@ pip install "simple_module_hosting[dev]"
|
|
|
47
47
|
|
|
48
48
|
## What it provides
|
|
49
49
|
|
|
50
|
+
All fixtures below are auto-registered via the `pytest11` entry point — installing the package is enough.
|
|
51
|
+
|
|
52
|
+
- `fake_event_bus` — a fresh recording `EventBus` per test that captures every `publish`/`publish_nowait` call.
|
|
53
|
+
- `build_test_app` — callable returning a minimal FastAPI app that loads exactly one module.
|
|
50
54
|
- `settings` — a ready-to-use `Settings` instance with an in-memory SQLite database and multi-tenancy enabled.
|
|
51
55
|
- `db_state`, `engine`, `db_session` — fresh `DatabaseState` per test; `db_session` also creates all module tables and stamps `alembic_version` at head so the boot-time migration check passes.
|
|
52
56
|
- `app` — a `create_app(settings)` instance with `lifespan` started and stopped.
|
|
53
57
|
- `client` — an `httpx.AsyncClient` bound to the test app.
|
|
54
|
-
- `authenticated_client` — same but with an admin user seeded and a forged session cookie attached.
|
|
58
|
+
- `authenticated_client` — same but with an admin user seeded and a forged session cookie attached. **Requires the `users` module** to be installed (it seeds the admin via `users.bootstrap`); apps scaffolded by `smpy` include it.
|
|
55
59
|
|
|
56
60
|
## Usage
|
|
57
61
|
|
|
@@ -14,11 +14,15 @@ pip install "simple_module_hosting[dev]"
|
|
|
14
14
|
|
|
15
15
|
## What it provides
|
|
16
16
|
|
|
17
|
+
All fixtures below are auto-registered via the `pytest11` entry point — installing the package is enough.
|
|
18
|
+
|
|
19
|
+
- `fake_event_bus` — a fresh recording `EventBus` per test that captures every `publish`/`publish_nowait` call.
|
|
20
|
+
- `build_test_app` — callable returning a minimal FastAPI app that loads exactly one module.
|
|
17
21
|
- `settings` — a ready-to-use `Settings` instance with an in-memory SQLite database and multi-tenancy enabled.
|
|
18
22
|
- `db_state`, `engine`, `db_session` — fresh `DatabaseState` per test; `db_session` also creates all module tables and stamps `alembic_version` at head so the boot-time migration check passes.
|
|
19
23
|
- `app` — a `create_app(settings)` instance with `lifespan` started and stopped.
|
|
20
24
|
- `client` — an `httpx.AsyncClient` bound to the test app.
|
|
21
|
-
- `authenticated_client` — same but with an admin user seeded and a forged session cookie attached.
|
|
25
|
+
- `authenticated_client` — same but with an admin user seeded and a forged session cookie attached. **Requires the `users` module** to be installed (it seeds the admin via `users.bootstrap`); apps scaffolded by `smpy` include it.
|
|
22
26
|
|
|
23
27
|
## Usage
|
|
24
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_test"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.19"
|
|
4
4
|
description = "Shared pytest fixtures (app, client, db_session, authenticated_client) for writing simple_module module tests"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -25,9 +25,9 @@ dependencies = [
|
|
|
25
25
|
"httpx>=0.27",
|
|
26
26
|
"pytest>=8.0",
|
|
27
27
|
"pytest-asyncio>=0.24",
|
|
28
|
-
"simple_module_core==0.0.
|
|
29
|
-
"simple_module_db==0.0.
|
|
30
|
-
"simple_module_hosting==0.0.
|
|
28
|
+
"simple_module_core==0.0.19",
|
|
29
|
+
"simple_module_db==0.0.19",
|
|
30
|
+
"simple_module_hosting==0.0.19",
|
|
31
31
|
"sqlalchemy>=2.0",
|
|
32
32
|
]
|
|
33
33
|
|
|
@@ -12,9 +12,12 @@ Primary exports:
|
|
|
12
12
|
* :func:`build_test_app` — return a minimal FastAPI app loading exactly
|
|
13
13
|
one module, with an in-memory SQLite DB.
|
|
14
14
|
|
|
15
|
-
The
|
|
16
|
-
``
|
|
17
|
-
|
|
15
|
+
The pytest plugin auto-registers these fixtures (no imports needed):
|
|
16
|
+
``fake_event_bus``, ``build_test_app``, ``settings``, ``db_state``,
|
|
17
|
+
``engine``, ``db_session``, ``app``, ``client``, and ``authenticated_client``.
|
|
18
|
+
See ``simple_module_test.plugin`` (and ``simple_module_test.fixtures``) for
|
|
19
|
+
their definitions. ``authenticated_client`` additionally requires the ``users``
|
|
20
|
+
module to be installed — it seeds an admin via ``users.bootstrap``.
|
|
18
21
|
"""
|
|
19
22
|
|
|
20
23
|
from simple_module_test.app_factory import build_test_app
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Headline app/db/client fixtures shipped by the pytest plugin.
|
|
2
|
+
|
|
3
|
+
These are the fixtures the README advertises — ``settings``, ``db_state``,
|
|
4
|
+
``engine``, ``db_session``, ``app``, ``client``, ``authenticated_client`` —
|
|
5
|
+
made available to any test run in an environment that installs
|
|
6
|
+
``simple_module_test`` (the ``pytest11`` entry point re-exports them from
|
|
7
|
+
:mod:`simple_module_test.plugin`). They build a real ``create_app`` against an
|
|
8
|
+
in-memory SQLite DB with every installed module's tables created and the
|
|
9
|
+
``alembic_version`` row stamped at head, so the boot-time migration check
|
|
10
|
+
passes.
|
|
11
|
+
|
|
12
|
+
Module-scope imports here reference only framework packages
|
|
13
|
+
(``simple_module_core`` / ``simple_module_db`` / ``simple_module_hosting``),
|
|
14
|
+
which are this package's declared dependencies. The one plugin-module coupling
|
|
15
|
+
— ``authenticated_client`` seeding an admin via ``users.bootstrap`` — is a lazy
|
|
16
|
+
import inside the fixture body, so importing this module never requires the
|
|
17
|
+
``users`` module to be installed (mirroring ``plugin._bootstrap_eager_celery``,
|
|
18
|
+
which soft-imports ``background_tasks``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import importlib
|
|
25
|
+
from collections.abc import AsyncGenerator
|
|
26
|
+
from functools import lru_cache
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
import pytest
|
|
30
|
+
from simple_module_core.discovery import discover_modules
|
|
31
|
+
from simple_module_db.base import all_module_bases
|
|
32
|
+
from simple_module_db.session import DatabaseState, init_db
|
|
33
|
+
from simple_module_hosting.settings import Settings
|
|
34
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
35
|
+
|
|
36
|
+
from simple_module_test.session_cookie import forge_session_cookie
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def settings() -> Settings:
|
|
41
|
+
"""Settings configured for testing with in-memory SQLite.
|
|
42
|
+
|
|
43
|
+
Multi-tenancy stays on so the existing ``TenantMiddleware`` tests
|
|
44
|
+
(and the ``X-Tenant-ID`` header paths they rely on) keep working.
|
|
45
|
+
Individual tests that want the tenant middleware absent construct
|
|
46
|
+
their own ``Settings(multi_tenant=False, ...)`` in the test body.
|
|
47
|
+
"""
|
|
48
|
+
return Settings(
|
|
49
|
+
database_url="sqlite+aiosqlite:///:memory:",
|
|
50
|
+
environment="testing",
|
|
51
|
+
secret_key="test-secret-key",
|
|
52
|
+
multi_tenant=True,
|
|
53
|
+
tenant_header="X-Tenant-ID",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
async def db_state() -> AsyncGenerator[DatabaseState, None]:
|
|
59
|
+
"""Create a fresh in-memory DatabaseState with listeners registered."""
|
|
60
|
+
from simple_module_db.listeners import register_listeners
|
|
61
|
+
|
|
62
|
+
state = init_db("sqlite+aiosqlite:///:memory:")
|
|
63
|
+
register_listeners(state)
|
|
64
|
+
yield state
|
|
65
|
+
await state.engine.dispose()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.fixture
|
|
69
|
+
async def engine(db_state: DatabaseState) -> AsyncEngine:
|
|
70
|
+
"""Return the engine from the test DatabaseState."""
|
|
71
|
+
return db_state.engine
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@lru_cache(maxsize=1)
|
|
75
|
+
def _ensure_models_imported() -> list:
|
|
76
|
+
"""Import all module models so all_module_bases is populated (cached)."""
|
|
77
|
+
for mod in discover_modules():
|
|
78
|
+
pkg = type(mod).__module__.split(".")[0]
|
|
79
|
+
with contextlib.suppress(ModuleNotFoundError):
|
|
80
|
+
importlib.import_module(f"{pkg}.models")
|
|
81
|
+
return list(all_module_bases)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@lru_cache(maxsize=1)
|
|
85
|
+
def _alembic_head() -> str | None:
|
|
86
|
+
"""Cached head revision — cannot change within a pytest run."""
|
|
87
|
+
from simple_module_hosting.migrations import resolve_head_revision
|
|
88
|
+
|
|
89
|
+
return resolve_head_revision()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _create_all_tables(engine) -> None:
|
|
93
|
+
"""Create all module tables in a single connection.
|
|
94
|
+
|
|
95
|
+
Also stamps the alembic_version table at head so the app's startup
|
|
96
|
+
migration check (``check_migrations``) treats the test DB as current.
|
|
97
|
+
Without the stamp the check would raise because ``create_all`` doesn't
|
|
98
|
+
touch alembic_version.
|
|
99
|
+
"""
|
|
100
|
+
from sqlalchemy import text
|
|
101
|
+
|
|
102
|
+
bases = _ensure_models_imported()
|
|
103
|
+
head = _alembic_head()
|
|
104
|
+
|
|
105
|
+
async with engine.begin() as conn:
|
|
106
|
+
|
|
107
|
+
def _sync_create_all(sync_conn):
|
|
108
|
+
for base in bases:
|
|
109
|
+
base.metadata.create_all(sync_conn)
|
|
110
|
+
|
|
111
|
+
await conn.run_sync(_sync_create_all)
|
|
112
|
+
|
|
113
|
+
if head:
|
|
114
|
+
await conn.execute(
|
|
115
|
+
text(
|
|
116
|
+
"CREATE TABLE IF NOT EXISTS alembic_version "
|
|
117
|
+
"(version_num VARCHAR(32) NOT NULL PRIMARY KEY)"
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
await conn.execute(text("DELETE FROM alembic_version"))
|
|
121
|
+
await conn.execute(
|
|
122
|
+
text("INSERT INTO alembic_version (version_num) VALUES (:v)"),
|
|
123
|
+
{"v": head},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.fixture
|
|
128
|
+
async def db_session(db_state: DatabaseState) -> AsyncGenerator[AsyncSession, None]:
|
|
129
|
+
"""Yield an async session backed by in-memory SQLite."""
|
|
130
|
+
await _create_all_tables(db_state.engine)
|
|
131
|
+
|
|
132
|
+
async with db_state.session_factory() as session:
|
|
133
|
+
yield session
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.fixture
|
|
137
|
+
async def app(settings: Settings):
|
|
138
|
+
"""Create a FastAPI app with tables pre-created and lifespan triggered."""
|
|
139
|
+
from simple_module_hosting.app_builder import create_app
|
|
140
|
+
|
|
141
|
+
application = create_app(settings)
|
|
142
|
+
|
|
143
|
+
await _create_all_tables(application.state.sm.db.engine)
|
|
144
|
+
|
|
145
|
+
# Trigger lifespan startup so app.state.migration is populated
|
|
146
|
+
ctx = application.router.lifespan_context(application)
|
|
147
|
+
await ctx.__aenter__()
|
|
148
|
+
|
|
149
|
+
yield application
|
|
150
|
+
|
|
151
|
+
# Lifespan shutdown disposes the engine
|
|
152
|
+
await ctx.__aexit__(None, None, None)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.fixture
|
|
156
|
+
async def client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
|
|
157
|
+
"""Unauthenticated async HTTP client."""
|
|
158
|
+
transport = httpx.ASGITransport(app=app)
|
|
159
|
+
async with httpx.AsyncClient(
|
|
160
|
+
transport=transport,
|
|
161
|
+
base_url="http://testserver",
|
|
162
|
+
) as c:
|
|
163
|
+
yield c
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.fixture
|
|
167
|
+
async def authenticated_client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
|
|
168
|
+
"""HTTPX client with a signed session cookie carrying a seeded admin user's id.
|
|
169
|
+
|
|
170
|
+
Requires the ``users`` module to be installed — it seeds the admin via
|
|
171
|
+
``users.bootstrap.create_admin``. The import is deferred to here (not module
|
|
172
|
+
scope) so this plugin imports cleanly without ``users``; a consumer app that
|
|
173
|
+
uses this fixture without ``users`` gets a clear ImportError naming it.
|
|
174
|
+
"""
|
|
175
|
+
from users.bootstrap import create_admin
|
|
176
|
+
|
|
177
|
+
async with app.state.sm.db.session_factory() as session:
|
|
178
|
+
result = await create_admin(
|
|
179
|
+
session,
|
|
180
|
+
email="admin@test",
|
|
181
|
+
password="test-password",
|
|
182
|
+
full_name="Test Admin",
|
|
183
|
+
)
|
|
184
|
+
user_id = str(result.user.id)
|
|
185
|
+
|
|
186
|
+
signed = forge_session_cookie(
|
|
187
|
+
app.state.sm.settings.secret_key,
|
|
188
|
+
{"user_id": user_id},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
transport = httpx.ASGITransport(app=app)
|
|
192
|
+
async with httpx.AsyncClient(
|
|
193
|
+
transport=transport,
|
|
194
|
+
base_url="http://testserver",
|
|
195
|
+
cookies={"session": signed},
|
|
196
|
+
) as c:
|
|
197
|
+
yield c
|
|
@@ -15,6 +15,21 @@ import pytest
|
|
|
15
15
|
from simple_module_test.app_factory import build_test_app as _build_test_app
|
|
16
16
|
from simple_module_test.fake_events import FakeEventBus
|
|
17
17
|
|
|
18
|
+
# Re-export the headline app/db/client fixtures the README advertises. pytest
|
|
19
|
+
# collects fixture-decorated attributes of the plugin module, so importing them
|
|
20
|
+
# here registers them for every test run that installs simple_module_test —
|
|
21
|
+
# without a per-consumer conftest. Treat this surface like a public API: deleting
|
|
22
|
+
# one breaks external modules' suites. See GH #200.
|
|
23
|
+
from simple_module_test.fixtures import ( # noqa: F401
|
|
24
|
+
app,
|
|
25
|
+
authenticated_client,
|
|
26
|
+
client,
|
|
27
|
+
db_session,
|
|
28
|
+
db_state,
|
|
29
|
+
engine,
|
|
30
|
+
settings,
|
|
31
|
+
)
|
|
32
|
+
|
|
18
33
|
|
|
19
34
|
def _bootstrap_eager_celery() -> None:
|
|
20
35
|
"""Register a process-wide eager Celery app before any test runs.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""GH #200: the published ``simple_module_test`` plugin must register the
|
|
2
|
+
headline fixtures its README/``__init__`` advertise — not just ``fake_event_bus``
|
|
3
|
+
+ ``build_test_app``. Before the fix those fixtures lived only in the framework
|
|
4
|
+
repo's root ``conftest.py``, so a consumer that installed the plugin (but wasn't
|
|
5
|
+
the framework repo) got "fixture not found".
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# The fixtures the plugin promises. Treat this set as the public fixture
|
|
11
|
+
# surface — dropping one breaks external modules' suites.
|
|
12
|
+
_HEADLINE_FIXTURES = (
|
|
13
|
+
"fake_event_bus",
|
|
14
|
+
"build_test_app",
|
|
15
|
+
"settings",
|
|
16
|
+
"db_state",
|
|
17
|
+
"engine",
|
|
18
|
+
"db_session",
|
|
19
|
+
"app",
|
|
20
|
+
"client",
|
|
21
|
+
"authenticated_client",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_pytest_fixture(obj) -> bool:
|
|
26
|
+
"""True if ``obj`` was produced by ``@pytest.fixture`` (version-robust).
|
|
27
|
+
|
|
28
|
+
pytest >=8.4 returns a ``FixtureFunctionDefinition`` instance; older
|
|
29
|
+
versions left the function in place with a ``_pytestfixturefunction``
|
|
30
|
+
marker attribute. Cover both so this test doesn't pin a pytest version.
|
|
31
|
+
"""
|
|
32
|
+
return hasattr(obj, "_pytestfixturefunction") or (
|
|
33
|
+
type(obj).__name__ == "FixtureFunctionDefinition"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_plugin_module_exports_every_headline_fixture():
|
|
38
|
+
"""Each advertised fixture is a real ``@pytest.fixture`` on the plugin module."""
|
|
39
|
+
import simple_module_test.plugin as plugin
|
|
40
|
+
|
|
41
|
+
for name in _HEADLINE_FIXTURES:
|
|
42
|
+
obj = getattr(plugin, name, None)
|
|
43
|
+
assert obj is not None, f"{name!r} is not exported by simple_module_test.plugin"
|
|
44
|
+
# A plain function imported by mistake would not satisfy this.
|
|
45
|
+
assert _is_pytest_fixture(obj), f"{name!r} is not a pytest fixture"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_plugin_is_registered_via_pytest11_entry_point(pytestconfig):
|
|
49
|
+
"""pytest discovered the plugin through its ``pytest11`` entry point, so a
|
|
50
|
+
consumer gets the fixtures by installing the package alone."""
|
|
51
|
+
assert pytestconfig.pluginmanager.hasplugin("simple_module_test")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def test_db_session_fixture_injects_without_a_local_conftest(db_session):
|
|
55
|
+
"""``db_session`` resolves in this test dir, which has no conftest defining
|
|
56
|
+
it — it can only come from the installed plugin (root conftest no longer
|
|
57
|
+
provides it). Proves the moved fixture is genuinely shipped, not just
|
|
58
|
+
re-exported on paper."""
|
|
59
|
+
from sqlalchemy import text
|
|
60
|
+
|
|
61
|
+
result = await db_session.execute(text("SELECT 1"))
|
|
62
|
+
assert result.scalar_one() == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_test-0.0.17 → simple_module_test-0.0.19}/simple_module_test/session_cookie.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|