simple-module-test 0.0.32__tar.gz → 0.0.33__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.32
3
+ Version: 0.0.33
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.32
29
- Requires-Dist: simple-module-db==0.0.32
30
- Requires-Dist: simple-module-hosting==0.0.32
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
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.32"
3
+ version = "0.0.33"
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.32",
29
- "simple_module_db==0.0.32",
30
- "simple_module_hosting==0.0.32",
28
+ "simple_module_core==0.0.33",
29
+ "simple_module_db==0.0.33",
30
+ "simple_module_hosting==0.0.33",
31
31
  "sqlalchemy>=2.0",
32
32
  ]
33
33
 
@@ -118,17 +118,24 @@ def _ensure_models_imported() -> list:
118
118
 
119
119
 
120
120
  @lru_cache(maxsize=1)
121
- def _alembic_head() -> str | None:
122
- """Cached head revision — cannot change within a pytest run."""
123
- from simple_module_hosting.migrations import resolve_head_revision
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
124
131
 
125
- return resolve_head_revision()
132
+ return resolve_head_revisions()
126
133
 
127
134
 
128
135
  async def _create_all_tables(engine) -> None:
129
136
  """Create all module tables in a single connection.
130
137
 
131
- Also stamps the alembic_version table at head so the app's startup
138
+ Also stamps the alembic_version table at heads so the app's startup
132
139
  migration check (``check_migrations``) treats the test DB as current.
133
140
  Without the stamp the check would raise because ``create_all`` doesn't
134
141
  touch alembic_version.
@@ -136,7 +143,7 @@ async def _create_all_tables(engine) -> None:
136
143
  from sqlalchemy import text
137
144
 
138
145
  bases = _ensure_models_imported()
139
- head = _alembic_head()
146
+ heads = _alembic_heads()
140
147
 
141
148
  async with engine.begin() as conn:
142
149
 
@@ -146,7 +153,7 @@ async def _create_all_tables(engine) -> None:
146
153
 
147
154
  await conn.run_sync(_sync_create_all)
148
155
 
149
- if head:
156
+ if heads:
150
157
  await conn.execute(
151
158
  text(
152
159
  "CREATE TABLE IF NOT EXISTS alembic_version "
@@ -154,10 +161,11 @@ async def _create_all_tables(engine) -> None:
154
161
  )
155
162
  )
156
163
  await conn.execute(text("DELETE FROM alembic_version"))
157
- await conn.execute(
158
- text("INSERT INTO alembic_version (version_num) VALUES (:v)"),
159
- {"v": head},
160
- )
164
+ for head in heads:
165
+ await conn.execute(
166
+ text("INSERT INTO alembic_version (version_num) VALUES (:v)"),
167
+ {"v": head},
168
+ )
161
169
 
162
170
 
163
171
  @pytest.fixture
@@ -169,14 +177,47 @@ async def db_session(db_state: DatabaseState) -> AsyncGenerator[AsyncSession, No
169
177
  yield session
170
178
 
171
179
 
172
- @pytest.fixture
173
- async def app(settings: Settings):
174
- """Create a FastAPI app with tables pre-created and lifespan triggered."""
180
+ SETUP_ADMIN_EMAIL = "admin@test"
181
+ SETUP_ADMIN_PASSWORD = "test-password"
182
+
183
+
184
+ async def _seed_setup_admin(application) -> None:
185
+ """Make the ``app`` fixture model a *configured* install.
186
+
187
+ ``SetupMiddleware`` redirects every request to ``/setup`` until an
188
+ administrator exists. That is right for a fresh deployment and wrong for a
189
+ suite asserting on ordinary pages — without this, every test using ``app``
190
+ gets a 302.
191
+
192
+ Seeds the same admin ``authenticated_client`` uses, so that fixture's own
193
+ ``create_admin`` call stays idempotent rather than creating a second one.
194
+
195
+ Skipped when the ``users`` module isn't installed: an install with no
196
+ local-accounts provider registers no setup step and is never gated, so
197
+ there is nothing to satisfy. Use ``setup_pending_app`` to exercise the gate.
198
+ """
199
+ try:
200
+ from users.bootstrap import create_admin
201
+ except ImportError:
202
+ return
203
+
204
+ async with application.state.sm.db.session_factory() as session:
205
+ await create_admin(
206
+ session,
207
+ email=SETUP_ADMIN_EMAIL,
208
+ password=SETUP_ADMIN_PASSWORD,
209
+ full_name="Test Admin",
210
+ )
211
+
212
+
213
+ async def _build_app(settings: Settings, *, seed_admin: bool):
175
214
  from simple_module_hosting.app_builder import create_app
176
215
 
177
216
  application = create_app(settings)
178
217
 
179
218
  await _create_all_tables(application.state.sm.db.engine)
219
+ if seed_admin:
220
+ await _seed_setup_admin(application)
180
221
 
181
222
  # Trigger lifespan startup so app.state.migration is populated
182
223
  ctx = application.router.lifespan_context(application)
@@ -188,6 +229,23 @@ async def app(settings: Settings):
188
229
  await ctx.__aexit__(None, None, None)
189
230
 
190
231
 
232
+ @pytest.fixture
233
+ async def app(settings: Settings):
234
+ """A configured app: tables created, an admin seeded, lifespan triggered."""
235
+ async for application in _build_app(settings, seed_admin=True):
236
+ yield application
237
+
238
+
239
+ @pytest.fixture
240
+ async def setup_pending_app(settings: Settings):
241
+ """An app with no administrator, so the first-run setup gate is engaged.
242
+
243
+ The counterpart to ``app``: use this to assert on setup-mode behaviour.
244
+ """
245
+ async for application in _build_app(settings, seed_admin=False):
246
+ yield application
247
+
248
+
191
249
  @pytest.fixture
192
250
  async def client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
193
251
  """Unauthenticated async HTTP client."""
@@ -211,10 +269,12 @@ async def authenticated_client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
211
269
  from users.bootstrap import create_admin
212
270
 
213
271
  async with app.state.sm.db.session_factory() as session:
272
+ # Idempotent: the `app` fixture already seeded this same admin so the
273
+ # setup gate would release. This resolves its id for the cookie.
214
274
  result = await create_admin(
215
275
  session,
216
- email="admin@test",
217
- password="test-password",
276
+ email=SETUP_ADMIN_EMAIL,
277
+ password=SETUP_ADMIN_PASSWORD,
218
278
  full_name="Test Admin",
219
279
  )
220
280
  user_id = str(result.user.id)
@@ -29,6 +29,7 @@ from simple_module_test.fixtures import ( # noqa: F401
29
29
  engine,
30
30
  pinned_auth_provider,
31
31
  settings,
32
+ setup_pending_app,
32
33
  )
33
34
 
34
35