Add the backend test harness: fixtures, factories, and DB isolation #5
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
pyproject.tomlalready declares[tool.pytest.ini_options] testpaths = ["tests"]and dev dependencies on
pytest,pytest-asyncio, andhttpx— but notests/directory exists. There is currently no automated test of any kind in this repo.
This issue lands the harness the other test issues build on.
Scope
Test package layout, database isolation, an API client fixture with authentication
stubbed, and object factories for the core entities.
Implementation notes
backend/tests/withconftest.py.:memory:so WAL mode andPRAGMA foreign_keys=ONbehave as they do in production —testing against different pragma behaviour than production would defeat the purpose.
upgrade head, notBase.metadata.create_all. That way themigrations themselves are exercised on every run and cannot silently drift from the models.
get_dbdependency so the app and the test share one session/transaction.get_current_user/require_roleto inject aUserat achosen
UserRole, so authorization can be tested without a live OAuth provider.Collection,Photo,DateEvidence,DateDecision,ReviewComment,BackgroundJob, andUserwith sensible defaults and keyword overrides.tmp_path-based storage root fixture so file writes never touch the real./storage.Done when
pytestruns green from a clean checkout with no manual setup beyond installing dev depscirca.dbReferences
backend/pyproject.toml(pytest config already present,tests/missing)backend/app/db/session.py,backend/app/auth/session.pydocs/circa-phase1-backlog.mdsection 10 (QA tickets)Blocks: every other backend test issue in this milestone.
Done in
adc6136. CI green.Done when
pytestruns green from a clean checkout with no manual setup beyond installing dev depscirca.dbThe second item was false, and had been for some time. Three suites called
ingest_photo()without redirecting storage, and 257 scan files had accumulated in the realbackend/storage/— the directory that on a deployment holds irreplaceable originals. Nothing failed. The tests passed and quietly wrote to disk, which is the only reason it survived this long.Redirection is now an autouse fixture, so a suite gets isolation whether or not it remembers to ask. That is the same shape as #70's required keyword argument and #87's method-derived write intent: the property holds because forgetting is not expressible, not because everyone remembers.
Verified rather than asserted:
backend/storageempty (it had 257 files; they are deleted);test_harness_isolation.pyasserts the guarantees directly — the configured storage root is outside the repository, writing through the resolved backend leaves the real directory untouched, the schema carries analembic_version.Factories.
factories.pyhad one hash helper; twelve files had each grown their ownmake_photo. It now holds builders for collection, user, album, photo, evidence, decision, comment and job, and the duplicates are gone. Consolidating them surfaced two latent traps rather than just shortening the code:test_authz_matrixused a fixedsha256per status, which #91's partial unique index would have collided on the day someone made two photos of the same status. It worked only because it made exactly one of each.refresh(), which on a write-intent session opens a transaction and holds SQLite's write lock for the rest of the test — the exact pattern #87 documents andconftestwarns about.Two design notes worth recording:
make_decisionrequires an author because the schema does —created_byis NOT NULL on a decision and nullable on evidence. That asymmetry is the schema saying something true: evidence can come from a machine, a conclusion cannot.make_decisiondeliberately does not recompute the projection. A factory that quietly did would hide the driftrebuild_projectionsexists to detect (#79), and a test could then pass against a photo whose stored state no route could produce.One thing I got wrong on the way: the autouse fixture originally created its storage directory, which broke
test_upload_filename_safety— that suite asserts on the exact contents of itstmp_pathto prove a hostile filename wrote nothing it should not have. A fixture that leaves a trace when unused is a fixture other tests can see, so it now letsLocalStoragecreate its own root on first resolution.