simple-module-test 0.0.36__tar.gz → 0.0.37__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 (20) hide show
  1. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/.gitignore +1 -0
  2. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/PKG-INFO +4 -4
  3. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/pyproject.toml +4 -4
  4. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/database.py +14 -0
  5. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/plugin.py +73 -0
  6. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/LICENSE +0 -0
  7. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/README.md +0 -0
  8. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/__init__.py +0 -0
  9. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/_schema.py +0 -0
  10. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/app_factory.py +0 -0
  11. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/fake_events.py +0 -0
  12. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/fixtures.py +0 -0
  13. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/redis_server.py +0 -0
  14. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/routes.py +0 -0
  15. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/session_cookie.py +0 -0
  16. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/setup_wizard.py +0 -0
  17. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/simple_module_test/tenant_client.py +0 -0
  18. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/tests/test_app_factory.py +0 -0
  19. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/tests/test_fake_events.py +0 -0
  20. {simple_module_test-0.0.36 → simple_module_test-0.0.37}/tests/test_plugin_fixtures.py +0 -0
@@ -64,3 +64,4 @@ host/client_app/.playwright-cli/*
64
64
  .superpowers/
65
65
  .qa/
66
66
  .verify/
67
+ /openapi.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_test
3
- Version: 0.0.36
3
+ Version: 0.0.37
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.36
29
- Requires-Dist: simple-module-db==0.0.36
30
- Requires-Dist: simple-module-hosting==0.0.36
28
+ Requires-Dist: simple-module-core==0.0.37
29
+ Requires-Dist: simple-module-db==0.0.37
30
+ Requires-Dist: simple-module-hosting==0.0.37
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.36"
3
+ version = "0.0.37"
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.36",
29
- "simple_module_db==0.0.36",
30
- "simple_module_hosting==0.0.36",
28
+ "simple_module_core==0.0.37",
29
+ "simple_module_db==0.0.37",
30
+ "simple_module_hosting==0.0.37",
31
31
  "sqlalchemy>=2.0",
32
32
  ]
33
33
 
@@ -26,6 +26,10 @@ def database_url_for_tests() -> str:
26
26
  return os.environ.get(TEST_DATABASE_ENV) or SQLITE_MEMORY
27
27
 
28
28
 
29
+ def database_is_postgres() -> bool:
30
+ return database_url_for_tests().startswith("postgresql")
31
+
32
+
29
33
  def is_sqlite(url: str) -> bool:
30
34
  return url.startswith("sqlite")
31
35
 
@@ -41,6 +45,15 @@ async def reset_schema(engine: Any) -> None:
41
45
  async with engine.begin() as conn:
42
46
  await conn.execute(text("DROP SCHEMA IF EXISTS public CASCADE"))
43
47
  await conn.execute(text("CREATE SCHEMA public"))
48
+ # The drop took the installed extensions with it; modules' models need them.
49
+ await conn.run_sync(_create_declared_extensions)
50
+
51
+
52
+ def _create_declared_extensions(sync_conn: Any) -> None:
53
+ from simple_module_core.discovery import discover_modules
54
+ from simple_module_db.extensions import ensure_db_extensions
55
+
56
+ ensure_db_extensions(sync_conn, discover_modules())
44
57
 
45
58
 
46
59
  # The test the schema was last emptied for. On SQLite every engine is its own
@@ -70,6 +83,7 @@ __all__ = [
70
83
  "SQLITE_MEMORY",
71
84
  "TEST_DATABASE_ENV",
72
85
  "begin_test",
86
+ "database_is_postgres",
73
87
  "database_url_for_tests",
74
88
  "init_db_kwargs",
75
89
  "is_sqlite",
@@ -37,6 +37,36 @@ from simple_module_test.redis_server import redis_server # noqa: F401
37
37
  from simple_module_test.tenant_client import tenant_client # noqa: F401
38
38
 
39
39
 
40
+ def pytest_configure(config: pytest.Config) -> None:
41
+ config.addinivalue_line(
42
+ "markers",
43
+ "postgres: needs a PostgreSQL test database (SM_TEST_DATABASE_URL=postgresql+...)",
44
+ )
45
+ from simple_module_test.database import database_is_postgres
46
+
47
+ # Every Postgres test drops and recreates ``public`` in the one database, so
48
+ # xdist workers would wipe each other's schema mid-test.
49
+ if database_is_postgres() and getattr(config.option, "numprocesses", None):
50
+ raise pytest.UsageError(
51
+ "SM_TEST_DATABASE_URL is Postgres, whose schema every test drops: run "
52
+ "serially with `make test-py-pg` or `-n 0` (`PYTEST_WORKERS=0 make test-py`)"
53
+ )
54
+
55
+
56
+ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
57
+ """Skip ``@pytest.mark.postgres`` tests unless the test database is Postgres."""
58
+ from simple_module_test.database import database_is_postgres
59
+
60
+ if database_is_postgres():
61
+ return
62
+ skip = pytest.mark.skip(
63
+ reason="needs Postgres: set SM_TEST_DATABASE_URL=postgresql+asyncpg://..."
64
+ )
65
+ for item in items:
66
+ if "postgres" in item.keywords:
67
+ item.add_marker(skip)
68
+
69
+
40
70
  def _bootstrap_eager_celery() -> None:
41
71
  """Register a process-wide eager Celery app before any test runs.
42
72
 
@@ -65,6 +95,49 @@ def _bootstrap_eager_celery() -> None:
65
95
  _bootstrap_eager_celery()
66
96
 
67
97
 
98
+ def _cheap_password_hashing() -> None:
99
+ """Make ``PasswordHelper()`` hash with minimal Argon2/bcrypt cost.
100
+
101
+ Every ``app`` fixture seeds an admin, and production-cost Argon2 is ~90ms
102
+ of CPU per hash — over a tenth of each app's setup, paid thousands of times
103
+ a run. The hashes stay real ``$argon2``/``$2b`` strings that verify and
104
+ upgrade through the same code paths; only their work factor drops, which is
105
+ a property of deployment rather than anything a test asserts.
106
+
107
+ Patched at import, not in a fixture: test modules build ``PasswordHelper()``
108
+ at module scope during collection, before any fixture runs. An explicit
109
+ ``password_hash=`` still wins. Skipped when ``fastapi_users`` is absent, or
110
+ when ``SM_TEST_REAL_PASSWORD_HASHING`` is set — the opt-out for a consumer
111
+ suite whose tests depend on production hash parameters.
112
+ """
113
+ if os.environ.get("SM_TEST_REAL_PASSWORD_HASHING"):
114
+ return
115
+ try:
116
+ from fastapi_users.password import PasswordHelper
117
+ from pwdlib import PasswordHash
118
+ from pwdlib.hashers.argon2 import Argon2Hasher
119
+ from pwdlib.hashers.bcrypt import BcryptHasher
120
+ except ImportError:
121
+ return
122
+
123
+ original_init = PasswordHelper.__init__
124
+
125
+ def _init(self, password_hash: PasswordHash | None = None) -> None:
126
+ if password_hash is None:
127
+ password_hash = PasswordHash(
128
+ (
129
+ Argon2Hasher(time_cost=1, memory_cost=8, parallelism=1),
130
+ BcryptHasher(rounds=4),
131
+ )
132
+ )
133
+ original_init(self, password_hash)
134
+
135
+ PasswordHelper.__init__ = _init # type: ignore[method-assign]
136
+
137
+
138
+ _cheap_password_hashing()
139
+
140
+
68
141
  @pytest.fixture
69
142
  def fake_event_bus() -> FakeEventBus:
70
143
  """Fresh recording EventBus for each test."""