From backend/:
uv run pytestbackend/pytest.ini sets addopts to include coverage by default,
--cov=. --cov-config=../.coveragerc --cov-report=term-missing --cov-report=html:htmlcov --cov-report=xml:coverage.xml, alongside --strict-markers --tb=short -v. So a plain uv run pytest always computes coverage and writes backend/htmlcov/ and backend/coverage.xml, even for
a single test file.
Scope a run to one app, one file, or a keyword the same way you would with any pytest project:
uv run pytest accounts/tests/ # one app's test package
uv run pytest leads/tests/test_leads_kanban.py # one file
uv run pytest -k "test_org_isolation" # by keyword: matches the cross-tenant
# isolation test present in most API test
# modules (leads, cases, contacts, ...)Coverage instrumentation and the three report formats above add real overhead. For the edit-run-edit loop, drop coverage and stop at the first failure:
uv run pytest --no-cov -x--strict-markers (also set in addopts) means a typo'd custom marker fails the run immediately
rather than being silently ignored. Every custom marker you use has to be declared in pytest.ini's
markers list. It doesn't touch markers a plugin registers itself, like @pytest.mark.django_db
(from pytest-django). Those work without appearing in pytest.ini at all, which is why every
example on this page uses django_db freely alongside the two markers that actually are declared.
pytest.ini declares exactly two custom markers:
markers =
slow: marks tests as slow
postgres_only: marks tests that require PostgreSQLpostgres_only is in active use. Exactly six tests carry it, confirmed by collecting the marker
directly (uv run pytest -m postgres_only --collect-only → 6/2776 tests collected), across four
files: common/tests/test_pat_auth.py (one test), common/tests/test_pat_model.py (one),
common/tests/test_rls_pipeline_tables.py (two), and invoices/tests/test_portal_rls.py (two).
(common/tests/test_pat_integration.py mentions postgres_only only in a comment explaining that
its own tests deliberately don't carry the marker and run on SQLite instead. It contributes zero
of the six.) Four of the six assert RLS policy state directly (pg_policies,
pg_class.relrowsecurity): the two in test_rls_pipeline_tables.py, plus one each in
test_pat_model.py and test_portal_rls.py confirming personal_access_token and
portal_access_token deliberately carry no policy. The other two assert isolation behaviour
under a real, RLS-enforcing role instead of reading policy metadata:
test_pat_auth.py::test_cross_org_pat_cannot_read_other_orgs_leads calls the real /api/leads/
endpoint and checks a foreign org's lead never appears, and
test_portal_rls.py::test_resolution_reads_estimate_that_empty_context_hides exercises the full
portal-token resolution path. slow is declared but,
as of this writing, applied to nothing (grep -rn "pytest.mark.slow" --include='*.py' ., excluding
venv/, returns no matches in application code). It's reserved for future use, not a marker you'll
see filtering anything today.
crm.test_settings (backend/crm/test_settings.py) is the settings module pytest.ini points at,
and its DATABASES is hardcoded to an in-memory SQLite database, not read from an environment
variable, not conditional on anything. Its own module docstring states why: "RLS (Row-Level
Security) is PostgreSQL-only and is skipped on SQLite." That means the default uv run pytest
run never exercises a single RLS policy. Every test runs against a database engine that has no
concept of row security at all. Everything you'd normally rely on RLS to catch, an org-filter bug
included, is invisible to this default suite; only the explicit Model.objects.filter(org=...) in
application code is being tested. See Multi-tenancy and RLS
for why that ORM filter has to be correct on its own merits, independent of RLS.
The postgres_only-marked tests exist to close part of that gap, and they self-skip safely rather
than fail when there's no PostgreSQL connection: each one checks connection.vendor != "postgresql"
and calls pytest.skip(...) if so. Under the SQLite settings this repository ships, that check is
always true, so these six tests always skip. They don't fail, and a green uv run pytest run tells
you nothing about whether they'd pass. One of them, test_portal_rls.py's module docstring,
describes an intended way to actually run them for real, against a non-superuser PostgreSQL role:
RLS_ENFORCE=1, pointed at a crm.test_settings_postgres module. A comment in backend/.env
(gitignored, .gitignore:20, and absent from .env.example, so a fresh clone has neither the
file nor the reference) names the same two pieces. Neither source is authoritative, because that
settings module does not exist anywhere in this checkout as of this writing, so this documented path
does not currently work. If you need to prove RLS isolation holds against real data rather than
just reading the policy SQL, use manage.py manage_rls --test against a real, non-superuser-configured PostgreSQL
database instead. See PostgreSQL and RLS
for exactly what it proves and how to set up that role.
.github/workflows/tests.yml's backend-tests job runs on the same SQLite settings described
above, there's no PostgreSQL service in that job, so postgres_only tests skip there too, and
CI itself has no path that runs them for real. Two steps matter, in this order:
uv run python manage.py makemigrations --check --dry-run
uv run pytest -m "not postgres_only" --tb=short -vThe migration check runs first and fails the build if any model change is missing its migration.
See Pull requests for what that means for your workflow. The
-m "not postgres_only" filter on the test step is redundant with the tests self-skipping, but
makes the exclusion explicit in the command itself rather than relying on every future
postgres_only test remembering its own skip guard.