regstack 0.2.4__tar.gz → 0.2.5__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.
- {regstack-0.2.4 → regstack-0.2.5}/CHANGELOG.md +17 -0
- {regstack-0.2.4 → regstack-0.2.5}/PKG-INFO +1 -1
- {regstack-0.2.4 → regstack-0.2.5}/docs/changelog.md +31 -0
- {regstack-0.2.4 → regstack-0.2.5}/pyproject.toml +23 -1
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/migrations/__init__.py +18 -12
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/doctor.py +2 -2
- regstack-0.2.5/src/regstack/version.py +1 -0
- {regstack-0.2.4 → regstack-0.2.5}/tasks.py +42 -0
- regstack-0.2.5/tests/unit/test_cli_doctor.py +164 -0
- regstack-0.2.5/tests/unit/test_cli_init.py +150 -0
- {regstack-0.2.4 → regstack-0.2.5}/uv.lock +1 -1
- regstack-0.2.4/src/regstack/version.py +0 -1
- {regstack-0.2.4 → regstack-0.2.5}/.github/workflows/publish.yml +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/.github/workflows/test.yml +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/.gitignore +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/.python-version +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/.readthedocs.yaml +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/CLAUDE.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/LICENSE +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/NOTICE +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/README.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/SECURITY.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/_static/.gitkeep +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/_templates/.gitkeep +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/api.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/architecture.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/cli.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/conf.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/configuration.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/embedding.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/index.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/quickstart.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/security.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/docs/theming.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/_common/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/_common/app.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/mongo/README.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/mongo/main.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/mongo/regstack.toml +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/postgres/README.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/postgres/main.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/postgres/regstack.toml +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/sqlite/README.md +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/sqlite/main.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/regstack.toml.example +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/app.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/clock.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/password.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/base.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/factory.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/protocols.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/__main__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/admin.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/init.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/config/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/config/loader.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/config/schema.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/config/secrets.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/base.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/composer.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/console.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/factory.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/ses.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/smtp.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/hooks/events.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/models/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/models/user.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/account.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/admin.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/login.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/logout.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/password.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/phone.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/register.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/routers/verify.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/sms/base.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/sms/factory.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/sms/null.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/sms/sns.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/pages.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/conftest.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_account_management.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_admin_router.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_indexes.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_mfa.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/integration/test_verification.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/__init__.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_cli.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_jwt.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_lockout.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_password.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_sms.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.2.4 → regstack-0.2.5}/tests/unit/test_ui_env.py +0 -0
|
@@ -5,6 +5,23 @@ authoritative copy lives at
|
|
|
5
5
|
[`docs/changelog.md`](docs/changelog.md) and is rendered into the
|
|
6
6
|
Sphinx docs.
|
|
7
7
|
|
|
8
|
+
## 0.2.5 — 2026-04-28
|
|
9
|
+
|
|
10
|
+
Bug fix + tooling.
|
|
11
|
+
|
|
12
|
+
- **Fix:** `regstack doctor` against a SQL backend crashed with
|
|
13
|
+
`asyncio.run() cannot be called from a running event loop`. The
|
|
14
|
+
schema check called `regstack.backends.sql.migrations.current()`,
|
|
15
|
+
which used `asyncio.run()` internally — invalid inside doctor's own
|
|
16
|
+
`asyncio.run`. Added `current_async()` and switched the doctor
|
|
17
|
+
command to use it. Sync `current()` is preserved for the migrate
|
|
18
|
+
CLI.
|
|
19
|
+
- **New:** `inv coverage [--no-html] [--fail-under=N]` runs the full
|
|
20
|
+
three-backend matrix under coverage and writes term + HTML reports.
|
|
21
|
+
Branch coverage is on by default.
|
|
22
|
+
- Test coverage uplift on the CLI: `cli/init.py` 14% → 88%,
|
|
23
|
+
`cli/doctor.py` 61% → 87%. Total: **85% → 87.1%**.
|
|
24
|
+
|
|
8
25
|
## 0.2.4 — 2026-04-28
|
|
9
26
|
|
|
10
27
|
**Breaking** — back-compat shims removed:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/regstack
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/regstack
|
|
@@ -3,6 +3,37 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Versions follow
|
|
4
4
|
[Semantic Versioning](https://semver.org/) once `1.0.0` ships.
|
|
5
5
|
|
|
6
|
+
## 0.2.5 — 2026-04-28
|
|
7
|
+
|
|
8
|
+
**Bug fix + tooling.**
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- ``regstack doctor`` against a SQL backend crashed with
|
|
13
|
+
``asyncio.run() cannot be called from a running event loop``. The
|
|
14
|
+
schema check called
|
|
15
|
+
``regstack.backends.sql.migrations.current()``, which used
|
|
16
|
+
``asyncio.run()`` internally — invalid inside doctor's own
|
|
17
|
+
``asyncio.run``. Added ``current_async()`` and switched the doctor
|
|
18
|
+
command to use it. Sync ``current()`` is preserved for the migrate
|
|
19
|
+
CLI (which runs outside an event loop).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- ``inv coverage [--no-html] [--fail-under=N]`` — runs the full
|
|
24
|
+
three-backend matrix under coverage, combines per-pytest-xdist-worker
|
|
25
|
+
``.coverage`` files, prints the term-with-missing report, and writes
|
|
26
|
+
``htmlcov/``. Branch coverage is on by default.
|
|
27
|
+
- ``[tool.coverage.*]`` config in ``pyproject.toml``.
|
|
28
|
+
- ``tests/unit/test_cli_init.py`` — six tests driving the
|
|
29
|
+
``regstack init`` wizard via ``CliRunner(input=...)``. Lifts
|
|
30
|
+
``cli/init.py`` from 14% → 88%.
|
|
31
|
+
- ``tests/unit/test_cli_doctor.py`` — four tests for the SQLite
|
|
32
|
+
``regstack doctor`` paths. Lifts ``cli/doctor.py`` from 61% → 87%.
|
|
33
|
+
|
|
34
|
+
Total line coverage on the full backend matrix: **85% → 87.1%**
|
|
35
|
+
(branch coverage is also newly enabled).
|
|
36
|
+
|
|
6
37
|
## 0.2.4 — 2026-04-28
|
|
7
38
|
|
|
8
39
|
**Breaking** — every back-compat shim left over from the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regstack"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.5"
|
|
4
4
|
description = "Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -119,3 +119,25 @@ python_version = "3.11"
|
|
|
119
119
|
strict = true
|
|
120
120
|
warn_unreachable = true
|
|
121
121
|
plugins = ["pydantic.mypy"]
|
|
122
|
+
|
|
123
|
+
[tool.coverage.run]
|
|
124
|
+
source = ["src/regstack"]
|
|
125
|
+
branch = true
|
|
126
|
+
parallel = true
|
|
127
|
+
# `concurrency = ["thread"]` is implicit; pytest-xdist workers each write
|
|
128
|
+
# their own .coverage.<host>.<pid>.<token> file and `coverage combine`
|
|
129
|
+
# folds them together — see the `coverage` invoke task.
|
|
130
|
+
|
|
131
|
+
[tool.coverage.report]
|
|
132
|
+
show_missing = true
|
|
133
|
+
skip_covered = false
|
|
134
|
+
precision = 1
|
|
135
|
+
exclude_lines = [
|
|
136
|
+
"pragma: no cover",
|
|
137
|
+
"if TYPE_CHECKING:",
|
|
138
|
+
"raise NotImplementedError",
|
|
139
|
+
"\\.\\.\\.$", # body-less Protocol methods
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
[tool.coverage.html]
|
|
143
|
+
directory = "htmlcov"
|
|
@@ -71,23 +71,29 @@ async def upgrade_async(database_url: str, revision: str = "head") -> None:
|
|
|
71
71
|
|
|
72
72
|
def current(database_url: str) -> str | None:
|
|
73
73
|
"""Return the current revision recorded in alembic_version, or None
|
|
74
|
-
if the table doesn't exist (i.e. fresh DB).
|
|
75
|
-
engine = create_async_engine(database_url)
|
|
76
|
-
|
|
77
|
-
async def _read() -> str | None:
|
|
78
|
-
async with engine.connect() as conn:
|
|
79
|
-
return await conn.run_sync(_current_sync)
|
|
74
|
+
if the table doesn't exist (i.e. fresh DB).
|
|
80
75
|
|
|
76
|
+
Synchronous; do NOT call from inside a running event loop —
|
|
77
|
+
use :func:`current_async` instead.
|
|
78
|
+
"""
|
|
81
79
|
import asyncio
|
|
82
80
|
|
|
81
|
+
return asyncio.run(_current_async_impl(database_url))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def current_async(database_url: str) -> str | None:
|
|
85
|
+
"""Async variant of :func:`current` — safe to call from inside an
|
|
86
|
+
already-running event loop (e.g. ``regstack doctor``)."""
|
|
87
|
+
return await _current_async_impl(database_url)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _current_async_impl(database_url: str) -> str | None:
|
|
91
|
+
engine = create_async_engine(database_url)
|
|
83
92
|
try:
|
|
84
|
-
|
|
93
|
+
async with engine.connect() as conn:
|
|
94
|
+
return await conn.run_sync(_current_sync)
|
|
85
95
|
finally:
|
|
86
|
-
|
|
87
|
-
async def _close() -> None:
|
|
88
|
-
await engine.dispose()
|
|
89
|
-
|
|
90
|
-
asyncio.run(_close())
|
|
96
|
+
await engine.dispose()
|
|
91
97
|
|
|
92
98
|
|
|
93
99
|
def _current_sync(connection) -> str | None:
|
|
@@ -127,10 +127,10 @@ async def _check_schema(config) -> CheckResult:
|
|
|
127
127
|
)
|
|
128
128
|
return CheckResult("schema", True, "core indexes present")
|
|
129
129
|
# SQL backends: compare alembic_version to the bundled head.
|
|
130
|
-
from regstack.backends.sql.migrations import
|
|
130
|
+
from regstack.backends.sql.migrations import current_async, head_revision
|
|
131
131
|
|
|
132
132
|
url = config.database_url.get_secret_value()
|
|
133
|
-
live =
|
|
133
|
+
live = await current_async(url)
|
|
134
134
|
head = head_revision()
|
|
135
135
|
if live is None:
|
|
136
136
|
return CheckResult(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.5"
|
|
@@ -95,6 +95,48 @@ def test_serial(c: Context, k: str = "") -> None:
|
|
|
95
95
|
c.run(cmd, pty=True)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
@task
|
|
99
|
+
def coverage(
|
|
100
|
+
c: Context,
|
|
101
|
+
pg_url: str = _DEFAULT_PG_URL,
|
|
102
|
+
html: bool = True,
|
|
103
|
+
fail_under: int = 0,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Run the full backend matrix under coverage.
|
|
106
|
+
|
|
107
|
+
Combines per-worker .coverage.* files (pytest-xdist runs one
|
|
108
|
+
coverage instance per worker; settings.parallel = true plus
|
|
109
|
+
`coverage combine` glues them back together) and prints the
|
|
110
|
+
line-coverage report. With --html (default), also writes an
|
|
111
|
+
HTML report under ``htmlcov/``.
|
|
112
|
+
|
|
113
|
+
Set ``--fail-under=N`` to make the task exit non-zero when total
|
|
114
|
+
line coverage drops below N percent — useful in CI.
|
|
115
|
+
"""
|
|
116
|
+
# Wipe stale coverage state so partial reruns don't leave double-counted
|
|
117
|
+
# data files behind.
|
|
118
|
+
c.run("uv run coverage erase", pty=True, warn=True)
|
|
119
|
+
env = {
|
|
120
|
+
"REGSTACK_TEST_BACKENDS": "sqlite,mongo,postgres",
|
|
121
|
+
"REGSTACK_TEST_POSTGRES_URL": pg_url,
|
|
122
|
+
# pytest-cov picks settings up from [tool.coverage.*] in pyproject.toml.
|
|
123
|
+
"COVERAGE_PROCESS_START": "pyproject.toml",
|
|
124
|
+
}
|
|
125
|
+
c.run(
|
|
126
|
+
"uv run python -m pytest -n auto --cov=src/regstack --cov-report=",
|
|
127
|
+
pty=True,
|
|
128
|
+
env=env,
|
|
129
|
+
)
|
|
130
|
+
c.run("uv run coverage combine", pty=True, warn=True)
|
|
131
|
+
report_cmd = "uv run coverage report"
|
|
132
|
+
if fail_under > 0:
|
|
133
|
+
report_cmd += f" --fail-under={fail_under}"
|
|
134
|
+
c.run(report_cmd, pty=True)
|
|
135
|
+
if html:
|
|
136
|
+
c.run("uv run coverage html", pty=True)
|
|
137
|
+
print("\nHTML report: htmlcov/index.html")
|
|
138
|
+
|
|
139
|
+
|
|
98
140
|
@task
|
|
99
141
|
def e2e(c: Context) -> None:
|
|
100
142
|
"""Run Playwright end-to-end suite."""
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tests for the ``regstack doctor`` CLI command.
|
|
2
|
+
|
|
3
|
+
Targets the SQLite branch (no infrastructure required) and exercises:
|
|
4
|
+
|
|
5
|
+
- the JWT-secret quality check (missing / too short / present)
|
|
6
|
+
- the backend ``ping`` path
|
|
7
|
+
- the schema check before and after ``install_schema``
|
|
8
|
+
- the email-factory check
|
|
9
|
+
- the ``--send-test-email`` path against the console backend
|
|
10
|
+
- the ``--check-dns`` path with a domain we expect to fail
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import os
|
|
17
|
+
import secrets
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
from click.testing import CliRunner
|
|
22
|
+
|
|
23
|
+
from regstack.cli.__main__ import cli
|
|
24
|
+
from regstack.cli._runtime import open_regstack
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write_sqlite_config(
|
|
28
|
+
tmp_path: Path,
|
|
29
|
+
*,
|
|
30
|
+
from_address: str = "noreply@example.com",
|
|
31
|
+
) -> tuple[Path, Path]:
|
|
32
|
+
sqlite_path = tmp_path / "doctor.db"
|
|
33
|
+
cfg = tmp_path / "regstack.toml"
|
|
34
|
+
cfg.write_text(
|
|
35
|
+
f"""\
|
|
36
|
+
app_name = "doctor-test"
|
|
37
|
+
base_url = "http://localhost:8000"
|
|
38
|
+
database_url = "sqlite+aiosqlite:///{sqlite_path}"
|
|
39
|
+
|
|
40
|
+
jwt_ttl_seconds = 7200
|
|
41
|
+
require_verification = false
|
|
42
|
+
allow_registration = true
|
|
43
|
+
|
|
44
|
+
[email]
|
|
45
|
+
backend = "console"
|
|
46
|
+
from_address = "{from_address}"
|
|
47
|
+
from_name = "doctor-test"
|
|
48
|
+
|
|
49
|
+
[sms]
|
|
50
|
+
backend = "null"
|
|
51
|
+
"""
|
|
52
|
+
)
|
|
53
|
+
return cfg, sqlite_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def doctor_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, str]:
|
|
58
|
+
"""Strip ambient REGSTACK_* vars, then set a known JWT secret + DB URL.
|
|
59
|
+
|
|
60
|
+
Returns ``(config_path, jwt_secret)``.
|
|
61
|
+
"""
|
|
62
|
+
for var in list(os.environ):
|
|
63
|
+
if var.startswith("REGSTACK_"):
|
|
64
|
+
monkeypatch.delenv(var, raising=False)
|
|
65
|
+
secret = secrets.token_urlsafe(64)
|
|
66
|
+
cfg_path, sqlite_path = _write_sqlite_config(tmp_path)
|
|
67
|
+
monkeypatch.setenv("REGSTACK_JWT_SECRET", secret)
|
|
68
|
+
monkeypatch.setenv("REGSTACK_DATABASE_URL", f"sqlite+aiosqlite:///{sqlite_path}")
|
|
69
|
+
return cfg_path, secret
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_doctor_reports_schema_missing_then_present(
|
|
73
|
+
doctor_env: tuple[Path, str],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Pre-install: doctor reports schema missing. Post-install: green."""
|
|
76
|
+
cfg_path, _ = doctor_env
|
|
77
|
+
runner = CliRunner()
|
|
78
|
+
|
|
79
|
+
# Pre-install: schema check fails (alembic_version table missing).
|
|
80
|
+
result = runner.invoke(cli, ["doctor", "--config", str(cfg_path)])
|
|
81
|
+
assert result.exit_code >= 1
|
|
82
|
+
assert "schema" in result.output
|
|
83
|
+
assert "alembic_version table missing" in result.output
|
|
84
|
+
assert "jwt secret" in result.output
|
|
85
|
+
assert "email backend" in result.output
|
|
86
|
+
|
|
87
|
+
# Install schema, then re-run.
|
|
88
|
+
async def _install() -> None:
|
|
89
|
+
async with open_regstack(cfg_path) as rs:
|
|
90
|
+
await rs.install_schema()
|
|
91
|
+
|
|
92
|
+
asyncio.run(_install())
|
|
93
|
+
|
|
94
|
+
result = runner.invoke(cli, ["doctor", "--config", str(cfg_path)])
|
|
95
|
+
assert result.exit_code == 0, result.output
|
|
96
|
+
assert "at head" in result.output
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_doctor_flags_short_jwt_secret(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
100
|
+
for var in list(os.environ):
|
|
101
|
+
if var.startswith("REGSTACK_"):
|
|
102
|
+
monkeypatch.delenv(var, raising=False)
|
|
103
|
+
cfg_path, sqlite_path = _write_sqlite_config(tmp_path)
|
|
104
|
+
monkeypatch.setenv("REGSTACK_JWT_SECRET", "too-short")
|
|
105
|
+
monkeypatch.setenv("REGSTACK_DATABASE_URL", f"sqlite+aiosqlite:///{sqlite_path}")
|
|
106
|
+
|
|
107
|
+
runner = CliRunner()
|
|
108
|
+
result = runner.invoke(cli, ["doctor", "--config", str(cfg_path)])
|
|
109
|
+
assert result.exit_code >= 1
|
|
110
|
+
assert "jwt secret" in result.output
|
|
111
|
+
assert "too short" in result.output
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_doctor_send_test_email_via_console(doctor_env: tuple[Path, str]) -> None:
|
|
115
|
+
"""--send-test-email succeeds against the console backend (it just prints)."""
|
|
116
|
+
cfg_path, _ = doctor_env
|
|
117
|
+
|
|
118
|
+
# Bring schema up so the schema check passes — otherwise the failed
|
|
119
|
+
# schema check makes doctor exit non-zero and we can't observe the
|
|
120
|
+
# send-test-email check independently.
|
|
121
|
+
async def _install() -> None:
|
|
122
|
+
async with open_regstack(cfg_path) as rs:
|
|
123
|
+
await rs.install_schema()
|
|
124
|
+
|
|
125
|
+
asyncio.run(_install())
|
|
126
|
+
|
|
127
|
+
runner = CliRunner()
|
|
128
|
+
result = runner.invoke(
|
|
129
|
+
cli,
|
|
130
|
+
[
|
|
131
|
+
"doctor",
|
|
132
|
+
"--config",
|
|
133
|
+
str(cfg_path),
|
|
134
|
+
"--send-test-email",
|
|
135
|
+
"probe@example.com",
|
|
136
|
+
],
|
|
137
|
+
)
|
|
138
|
+
assert result.exit_code == 0, result.output
|
|
139
|
+
assert "email send" in result.output
|
|
140
|
+
assert "probe@example.com" in result.output
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_doctor_check_dns_runs_lookups(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
144
|
+
"""--check-dns runs SPF/DKIM/MX lookups for the sender domain.
|
|
145
|
+
|
|
146
|
+
We use ``example.com`` as the sender — it's RFC 2606 reserved so it
|
|
147
|
+
accepts no traffic, which is fine here: we only care that doctor
|
|
148
|
+
actually invokes the dig probes (the labels appear in output) and
|
|
149
|
+
handles whatever response comes back without crashing.
|
|
150
|
+
"""
|
|
151
|
+
for var in list(os.environ):
|
|
152
|
+
if var.startswith("REGSTACK_"):
|
|
153
|
+
monkeypatch.delenv(var, raising=False)
|
|
154
|
+
cfg_path, sqlite_path = _write_sqlite_config(tmp_path, from_address="probe@example.com")
|
|
155
|
+
monkeypatch.setenv("REGSTACK_JWT_SECRET", secrets.token_urlsafe(64))
|
|
156
|
+
monkeypatch.setenv("REGSTACK_DATABASE_URL", f"sqlite+aiosqlite:///{sqlite_path}")
|
|
157
|
+
|
|
158
|
+
runner = CliRunner()
|
|
159
|
+
result = runner.invoke(cli, ["doctor", "--config", str(cfg_path), "--check-dns"])
|
|
160
|
+
# The schema check fails (we didn't install it), so exit code ≥ 1 —
|
|
161
|
+
# we just care that the DNS check labels are present in output.
|
|
162
|
+
assert "dns mx" in result.output
|
|
163
|
+
assert "dns spf" in result.output
|
|
164
|
+
assert "dns dmarc" in result.output
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Tests for the ``regstack init`` wizard.
|
|
2
|
+
|
|
3
|
+
The wizard is interactive (Click prompts) so we drive it via
|
|
4
|
+
``CliRunner(input=...)`` and assert on the files it writes. SQLite is
|
|
5
|
+
used throughout so these tests need no external services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import stat
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from click.testing import CliRunner
|
|
14
|
+
|
|
15
|
+
from regstack.cli.__main__ import cli
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _accept_all(num_prompts: int) -> str:
|
|
19
|
+
"""Hit Enter ``num_prompts`` times — every prompt has a default."""
|
|
20
|
+
return "\n" * num_prompts
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Number of prompts on each happy path. If the wizard grows another
|
|
24
|
+
# question these counts need updating; the test failure points to it.
|
|
25
|
+
_SQLITE_HAPPY_PATH_PROMPTS = 17
|
|
26
|
+
_SMTP_PATH_PROMPTS = 17 + 4 # smtp host/port/starttls/user/pass replace 0
|
|
27
|
+
_SES_PATH_PROMPTS = 17 + 1
|
|
28
|
+
_MONGO_PATH_PROMPTS = 17 + 1
|
|
29
|
+
_POSTGRES_PATH_PROMPTS = 17
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_init_writes_sqlite_config_with_defaults(tmp_path: Path) -> None:
|
|
33
|
+
runner = CliRunner()
|
|
34
|
+
result = runner.invoke(
|
|
35
|
+
cli,
|
|
36
|
+
["init", "--target", str(tmp_path)],
|
|
37
|
+
input=_accept_all(_SQLITE_HAPPY_PATH_PROMPTS),
|
|
38
|
+
)
|
|
39
|
+
assert result.exit_code == 0, result.output
|
|
40
|
+
|
|
41
|
+
cfg = tmp_path / "regstack.toml"
|
|
42
|
+
secrets = tmp_path / "regstack.secrets.env"
|
|
43
|
+
assert cfg.exists()
|
|
44
|
+
assert secrets.exists()
|
|
45
|
+
|
|
46
|
+
cfg_text = cfg.read_text()
|
|
47
|
+
assert 'app_name = "MyApp"' in cfg_text
|
|
48
|
+
assert 'base_url = "http://localhost:8000"' in cfg_text
|
|
49
|
+
assert "[email]" in cfg_text
|
|
50
|
+
assert 'backend = "console"' in cfg_text
|
|
51
|
+
# SQLite default writes no mongodb_database (it's omitted on non-mongo paths)
|
|
52
|
+
assert "mongodb_database" not in cfg_text
|
|
53
|
+
|
|
54
|
+
secrets_text = secrets.read_text()
|
|
55
|
+
assert "REGSTACK_JWT_SECRET=" in secrets_text
|
|
56
|
+
assert "REGSTACK_DATABASE_URL=sqlite+aiosqlite:///./myapp.sqlite" in secrets_text
|
|
57
|
+
|
|
58
|
+
# Mode 0600 — secrets file should not be world-readable.
|
|
59
|
+
mode = stat.S_IMODE(secrets.stat().st_mode)
|
|
60
|
+
assert mode == 0o600, oct(mode)
|
|
61
|
+
|
|
62
|
+
assert "Wrote " in result.output
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_init_postgres_backend(tmp_path: Path) -> None:
|
|
66
|
+
runner = CliRunner()
|
|
67
|
+
# 17 prompts, but the database-backend prompt picks postgres → triggers
|
|
68
|
+
# the Postgres URL prompt (which adds 1) but skips the SQLite-path
|
|
69
|
+
# prompt the SQLite branch added. Net = 17.
|
|
70
|
+
# Prompt sequence to reach postgres branch: defaults until the
|
|
71
|
+
# backend prompt (5 defaults), then "postgres", then defaults.
|
|
72
|
+
# We feed: 4x default, "postgres", 12x default.
|
|
73
|
+
inputs = "\n\n\n\n" + "postgres\n" + "\n" * 12
|
|
74
|
+
result = runner.invoke(cli, ["init", "--target", str(tmp_path)], input=inputs)
|
|
75
|
+
assert result.exit_code == 0, result.output
|
|
76
|
+
|
|
77
|
+
secrets_text = (tmp_path / "regstack.secrets.env").read_text()
|
|
78
|
+
assert "REGSTACK_DATABASE_URL=postgresql+asyncpg://postgres@localhost/myapp" in secrets_text
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_init_mongo_backend(tmp_path: Path) -> None:
|
|
82
|
+
runner = CliRunner()
|
|
83
|
+
# 4x default, "mongo", then default host, default db, then 11x default.
|
|
84
|
+
inputs = "\n\n\n\n" + "mongo\n" + "\n\n" + "\n" * 11
|
|
85
|
+
result = runner.invoke(cli, ["init", "--target", str(tmp_path)], input=inputs)
|
|
86
|
+
assert result.exit_code == 0, result.output
|
|
87
|
+
|
|
88
|
+
cfg_text = (tmp_path / "regstack.toml").read_text()
|
|
89
|
+
assert 'mongodb_database = "myapp"' in cfg_text
|
|
90
|
+
|
|
91
|
+
secrets_text = (tmp_path / "regstack.secrets.env").read_text()
|
|
92
|
+
assert "REGSTACK_DATABASE_URL=mongodb://localhost:27017/myapp" in secrets_text
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_init_refuses_to_overwrite_without_force(tmp_path: Path) -> None:
|
|
96
|
+
(tmp_path / "regstack.toml").write_text("# existing\n")
|
|
97
|
+
runner = CliRunner()
|
|
98
|
+
# The overwrite confirm prompt defaults to "no" (abort=True).
|
|
99
|
+
# Sending "n\n" rejects the prompt -> Click aborts with exit 1.
|
|
100
|
+
result = runner.invoke(cli, ["init", "--target", str(tmp_path)], input="n\n")
|
|
101
|
+
assert result.exit_code == 1
|
|
102
|
+
assert "Overwrite?" in result.output
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_init_force_overwrites(tmp_path: Path) -> None:
|
|
106
|
+
(tmp_path / "regstack.toml").write_text("# existing\n")
|
|
107
|
+
runner = CliRunner()
|
|
108
|
+
result = runner.invoke(
|
|
109
|
+
cli,
|
|
110
|
+
["init", "--target", str(tmp_path), "--force"],
|
|
111
|
+
input=_accept_all(_SQLITE_HAPPY_PATH_PROMPTS),
|
|
112
|
+
)
|
|
113
|
+
assert result.exit_code == 0, result.output
|
|
114
|
+
cfg_text = (tmp_path / "regstack.toml").read_text()
|
|
115
|
+
assert "# existing" not in cfg_text
|
|
116
|
+
assert 'app_name = "MyApp"' in cfg_text
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_init_smtp_backend_records_smtp_settings(tmp_path: Path) -> None:
|
|
120
|
+
"""Pick SMTP at the email prompt → wizard asks for host/port/starttls/user/pass."""
|
|
121
|
+
# Sequence: app/base/cookie/proxy (4 defaults), backend default (sqlite),
|
|
122
|
+
# sqlite path default, jwt/jwt-ttl/transport (3 defaults),
|
|
123
|
+
# email backend = "smtp", from-address default, from-name default,
|
|
124
|
+
# smtp host = "mail.example.com", smtp port default (587),
|
|
125
|
+
# smtp starttls default (yes), smtp user = "u", smtp pass = "p",
|
|
126
|
+
# then SMS no, admin no, ui no, register yes, verify no.
|
|
127
|
+
inputs = (
|
|
128
|
+
"\n" * 4 # app, base, cookie, proxy
|
|
129
|
+
+ "\n" # backend = sqlite
|
|
130
|
+
+ "\n" # sqlite path
|
|
131
|
+
+ "\n\n\n" # jwt secret auto, jwt ttl, transport
|
|
132
|
+
+ "smtp\n" # email backend
|
|
133
|
+
+ "\n\n" # from-address, from-name
|
|
134
|
+
+ "mail.example.com\n" # smtp host
|
|
135
|
+
+ "\n\n" # smtp port, starttls
|
|
136
|
+
+ "u\np\n" # smtp user, pass
|
|
137
|
+
+ "\n\n\n\n\n" # sms, admin, ui, register, verify
|
|
138
|
+
)
|
|
139
|
+
runner = CliRunner()
|
|
140
|
+
result = runner.invoke(cli, ["init", "--target", str(tmp_path)], input=inputs)
|
|
141
|
+
assert result.exit_code == 0, result.output
|
|
142
|
+
|
|
143
|
+
cfg_text = (tmp_path / "regstack.toml").read_text()
|
|
144
|
+
assert 'smtp_host = "mail.example.com"' in cfg_text
|
|
145
|
+
assert "smtp_port = 587" in cfg_text
|
|
146
|
+
assert "smtp_starttls = true" in cfg_text
|
|
147
|
+
assert 'smtp_username = "u"' in cfg_text
|
|
148
|
+
|
|
149
|
+
secrets_text = (tmp_path / "regstack.secrets.env").read_text()
|
|
150
|
+
assert "REGSTACK_EMAIL__SMTP_PASSWORD=p" in secrets_text
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/blacklist_repo.py
RENAMED
|
File without changes
|
{regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/mongo/repositories/login_attempt_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.2.4 → regstack-0.2.5}/src/regstack/backends/sql/repositories/login_attempt_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|