simple-module-test 0.0.33__tar.gz → 0.0.34__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_test
3
- Version: 0.0.33
3
+ Version: 0.0.34
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.33
29
- Requires-Dist: simple-module-db==0.0.33
30
- Requires-Dist: simple-module-hosting==0.0.33
28
+ Requires-Dist: simple-module-core==0.0.34
29
+ Requires-Dist: simple-module-db==0.0.34
30
+ Requires-Dist: simple-module-hosting==0.0.34
31
31
  Requires-Dist: sqlalchemy>=2.0
32
32
  Description-Content-Type: text/markdown
33
33
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_test"
3
- version = "0.0.33"
3
+ version = "0.0.34"
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.33",
29
- "simple_module_db==0.0.33",
30
- "simple_module_hosting==0.0.33",
28
+ "simple_module_core==0.0.34",
29
+ "simple_module_db==0.0.34",
30
+ "simple_module_hosting==0.0.34",
31
31
  "sqlalchemy>=2.0",
32
32
  ]
33
33
 
@@ -0,0 +1,77 @@
1
+ """Schema construction for the plugin's app fixtures.
2
+
3
+ Split out of :mod:`simple_module_test.fixtures` so that module declares
4
+ fixtures and this one holds the machinery underneath them: importing every
5
+ installed module's models, resolving the migration heads, creating the tables
6
+ and stamping ``alembic_version`` so the boot-time migration check passes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import importlib
13
+ from functools import lru_cache
14
+
15
+ from simple_module_core.discovery import discover_modules
16
+ from simple_module_db.base import all_module_bases
17
+
18
+
19
+ @lru_cache(maxsize=1)
20
+ def _ensure_models_imported() -> list:
21
+ """Import all module models so all_module_bases is populated (cached)."""
22
+ for mod in discover_modules():
23
+ pkg = type(mod).__module__.split(".")[0]
24
+ with contextlib.suppress(ModuleNotFoundError):
25
+ importlib.import_module(f"{pkg}.models")
26
+ return list(all_module_bases)
27
+
28
+
29
+ @lru_cache(maxsize=1)
30
+ def _alembic_heads() -> tuple[str, ...]:
31
+ """Cached head revisions — cannot change within a pytest run.
32
+
33
+ Plural: each module's first migration sets its own ``branch_labels``, so
34
+ the history has one head per module and a real upgraded database carries
35
+ an ``alembic_version`` row for each. Stamping only one leaves the rest
36
+ looking un-applied, which now reads as a behind-head schema and puts every
37
+ test app behind the setup gate.
38
+ """
39
+ from simple_module_hosting.migrations import resolve_head_revisions
40
+
41
+ return resolve_head_revisions()
42
+
43
+
44
+ async def _create_all_tables(engine) -> None:
45
+ """Create all module tables in a single connection.
46
+
47
+ Also stamps the alembic_version table at heads so the app's startup
48
+ migration check (``check_migrations``) treats the test DB as current.
49
+ Without the stamp the check would raise because ``create_all`` doesn't
50
+ touch alembic_version.
51
+ """
52
+ from sqlalchemy import text
53
+
54
+ bases = _ensure_models_imported()
55
+ heads = _alembic_heads()
56
+
57
+ async with engine.begin() as conn:
58
+
59
+ def _sync_create_all(sync_conn):
60
+ for base in bases:
61
+ base.metadata.create_all(sync_conn)
62
+
63
+ await conn.run_sync(_sync_create_all)
64
+
65
+ if heads:
66
+ await conn.execute(
67
+ text(
68
+ "CREATE TABLE IF NOT EXISTS alembic_version "
69
+ "(version_num VARCHAR(32) NOT NULL PRIMARY KEY)"
70
+ )
71
+ )
72
+ await conn.execute(text("DELETE FROM alembic_version"))
73
+ for head in heads:
74
+ await conn.execute(
75
+ text("INSERT INTO alembic_version (version_num) VALUES (:v)"),
76
+ {"v": head},
77
+ )
@@ -20,20 +20,17 @@ which soft-imports ``background_tasks``).
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
- import contextlib
24
- import importlib
25
23
  import os
26
24
  from collections.abc import AsyncGenerator, Iterator
27
- from functools import lru_cache
28
25
 
29
26
  import httpx
30
27
  import pytest
31
- from simple_module_core.discovery import DEFAULT_AUTH_PROVIDER, discover_modules
32
- from simple_module_db.base import all_module_bases
28
+ from simple_module_core.discovery import DEFAULT_AUTH_PROVIDER
33
29
  from simple_module_db.session import DatabaseState, init_db
34
30
  from simple_module_hosting.settings import Settings
35
31
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
36
32
 
33
+ from simple_module_test._schema import _create_all_tables
37
34
  from simple_module_test.session_cookie import forge_session_cookie
38
35
 
39
36
  _AUTH_PROVIDER_ENV = "SM_AUTH_PROVIDER"
@@ -107,67 +104,6 @@ async def engine(db_state: DatabaseState) -> AsyncEngine:
107
104
  return db_state.engine
108
105
 
109
106
 
110
- @lru_cache(maxsize=1)
111
- def _ensure_models_imported() -> list:
112
- """Import all module models so all_module_bases is populated (cached)."""
113
- for mod in discover_modules():
114
- pkg = type(mod).__module__.split(".")[0]
115
- with contextlib.suppress(ModuleNotFoundError):
116
- importlib.import_module(f"{pkg}.models")
117
- return list(all_module_bases)
118
-
119
-
120
- @lru_cache(maxsize=1)
121
- def _alembic_heads() -> tuple[str, ...]:
122
- """Cached head revisions — cannot change within a pytest run.
123
-
124
- Plural: each module's first migration sets its own ``branch_labels``, so
125
- the history has one head per module and a real upgraded database carries
126
- an ``alembic_version`` row for each. Stamping only one leaves the rest
127
- looking un-applied, which now reads as a behind-head schema and puts every
128
- test app behind the setup gate.
129
- """
130
- from simple_module_hosting.migrations import resolve_head_revisions
131
-
132
- return resolve_head_revisions()
133
-
134
-
135
- async def _create_all_tables(engine) -> None:
136
- """Create all module tables in a single connection.
137
-
138
- Also stamps the alembic_version table at heads so the app's startup
139
- migration check (``check_migrations``) treats the test DB as current.
140
- Without the stamp the check would raise because ``create_all`` doesn't
141
- touch alembic_version.
142
- """
143
- from sqlalchemy import text
144
-
145
- bases = _ensure_models_imported()
146
- heads = _alembic_heads()
147
-
148
- async with engine.begin() as conn:
149
-
150
- def _sync_create_all(sync_conn):
151
- for base in bases:
152
- base.metadata.create_all(sync_conn)
153
-
154
- await conn.run_sync(_sync_create_all)
155
-
156
- if heads:
157
- await conn.execute(
158
- text(
159
- "CREATE TABLE IF NOT EXISTS alembic_version "
160
- "(version_num VARCHAR(32) NOT NULL PRIMARY KEY)"
161
- )
162
- )
163
- await conn.execute(text("DELETE FROM alembic_version"))
164
- for head in heads:
165
- await conn.execute(
166
- text("INSERT INTO alembic_version (version_num) VALUES (:v)"),
167
- {"v": head},
168
- )
169
-
170
-
171
107
  @pytest.fixture
172
108
  async def db_session(db_state: DatabaseState) -> AsyncGenerator[AsyncSession, None]:
173
109
  """Yield an async session backed by in-memory SQLite."""
@@ -236,12 +172,38 @@ async def app(settings: Settings):
236
172
  yield application
237
173
 
238
174
 
175
+ def _disable_env_bootstrap(monkeypatch: pytest.MonkeyPatch) -> None:
176
+ """Stop ``bootstrap_admin_from_env`` seeding the app being built.
177
+
178
+ ``UsersModule.on_startup`` creates an administrator from
179
+ ``SM_USERS_BOOTSTRAP_*``, read from the environment *and* from a ``.env`` on
180
+ disk. A developer who followed ``.env.example`` has those set, so every app
181
+ this plugin builds gets an admin whether the fixture asked for one or not.
182
+ For ``setup_pending_app`` that is not a nuisance but the opposite of its
183
+ contract: the setup gate releases the moment an administrator exists, so the
184
+ wizard routes 404 and every test using it fails.
185
+
186
+ They fail *locally only* — CI has no ``.env`` — which is the worst shape for
187
+ a fixture to be wrong in, and left the failure looking like test-ordering
188
+ noise rather than a fixture that does not do what it says.
189
+ """
190
+ for key in list(os.environ):
191
+ if key.startswith("SM_USERS_BOOTSTRAP_"):
192
+ monkeypatch.delenv(key, raising=False)
193
+ try:
194
+ from users import bootstrap as bootstrap_module
195
+ except ImportError:
196
+ return # No local-accounts provider installed; nothing bootstraps.
197
+ monkeypatch.setattr(bootstrap_module, "_read_dotenv_bootstrap_vars", dict)
198
+
199
+
239
200
  @pytest.fixture
240
- async def setup_pending_app(settings: Settings):
201
+ async def setup_pending_app(settings: Settings, monkeypatch: pytest.MonkeyPatch):
241
202
  """An app with no administrator, so the first-run setup gate is engaged.
242
203
 
243
204
  The counterpart to ``app``: use this to assert on setup-mode behaviour.
244
205
  """
206
+ _disable_env_bootstrap(monkeypatch)
245
207
  async for application in _build_app(settings, seed_admin=False):
246
208
  yield application
247
209