Repository navigation
fix(auth): keep auth tests off the application database - #310
merttoprak1 wants to merge 2 commits into
Conversation
The auth integration tests created and dropped the schema on the engine from DATABASE_URL. When that URL pointed at a local database, teardown dropped 23 of its 24 tables. Both modules now use the in-memory client and engine fixtures from tests/conftest.py, like the other integration tests. A new autouse fixture fails any test that writes to the application database. Closes #299
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
lucaosti
left a comment
There was a problem hiding this comment.
Thanks, this fixes #299: I ran the auth tests against a real SQLite file as DATABASE_URL and all 23 tables survive (54 passed). Reusing the client/engine fixtures and documenting the before/after is exactly right.
One change before merging:
- The guard detects the write after it has happened.
_recordonly collects statements and the assertion runs at teardown, so the statement still reaches the developer's database. I tried a probe test that runsCREATE TABLE probethroughSessionLocal: it failed at teardown, but the table was created in the real file. Raising inside thebefore_cursor_executelistener blocks the statement and points at the line that issued it, which is what #299 needs.
Nit:
- The prefix check misses
WITH ... INSERT(my probe inserted a row and passed),REPLACEandTRUNCATE. Matching the verbs after an optional leadingWITHwould cover them.
The guard collected write statements and asserted at teardown, so the statement still reached the developer's database first. The listener now raises inside before_cursor_execute, which runs before cursor.execute: the statement never reaches the database, and the error points at the line that issued it. The match also covers a leading WITH clause, REPLACE and TRUNCATE. Refs #299
|
Thanks, both points were right. Fixed in the second commit. Blocking. The listener now raises Nit. The match now accepts an optional leading Limits. This is a check on SQL text, so it is not a hard boundary:
The full fix for these limits is to set Full suite on this branch: 1087 passed, coverage 85.27%, |
lucaosti
left a comment
There was a problem hiding this comment.
Thanks Mert, this does exactly what #299 needed. I reran the probe against a real SQLite file: CREATE TABLE through SessionLocal now fails at the statement and the table is not created. WITH ... INSERT, REPLACE, TRUNCATE and INSERT OR REPLACE are blocked too, while reads and the in-memory fixtures work as before, and the full suite (1087 tests) leaves the file byte-identical.
One optional nit: a statement starting with a SQL comment (/* x */ INSERT or -- x\nINSERT) still gets through. SQLAlchemy never generates that, so it only matters for hand-written text() SQL; stripping leading comments before the match would close it. Approving.
What changed and why
test_auth_api.pyandtest_auth_security.pycalledcreate_allanddrop_allonapp.db.session.engine. That engine usesDATABASE_URL. WhenDATABASE_URLpoints at a local database, every auth test drops its tables in teardown. This happened during #132.clientandenginefixtures fromtests/conftest.py. These fixtures use an in-memory SQLite database, like the other integration tests. The three seeding tests pass thatenginetoseed_test_users.tests/conftest.pygets an autouse fixture,_application_database_is_untouched. It listens on the application engine withbefore_cursor_executeand raises before a write statement executes:INSERT,UPDATE,DELETE,CREATE,DROP,ALTER,REPLACEorTRUNCATE, with or without a leadingWITHclause. The statement never reaches the database. This is the regression test. It also covers a future test that writes throughSessionLocalorget_db_context, which theget_dboverride does not reach.No new dependency. The guard uses the SQLAlchemy event API.
Closes #299
How it was verified
I set up a real local database with
python -m app.db.init_db(24 tables, two dev accounts) and setDATABASE_URLto it.masteralembic_version)master+ guardTest tried to write to the application database: INSERT INTO app_user ...uv run --no-sync mypy app: no issues.Watch for
SELECT ... INTO,MERGE,COPY,VACUUM,PRAGMAwrites,setvalorEXPLAIN ANALYZE. SQLAlchemy does not generate these. If application code catches the error, the write is still blocked, but the test can stay green./healthcallsget_db()directly instead of throughDepends, so the override does not apply. As a result,test_health_reports_database_connected_when_it_ischecks the real database withSELECT 1. It is read-only, so I left it out of this PR.