SurfSense/surfsense_backend/tests
CREDO23 1652e0777b feat(account-deletion): take every owned workspace with the account
With no transfer to offer, refusing the deletion would strand the user: the
409 told them to hand a workspace over using a feature that no longer exists.
So the preflight, the refusal, and the re-check in the task all go, and the
erase now deletes every workspace the account owns, shared or not.

Members of a shared workspace lose it. That is a real consequence for people
who did not click the button, so the dialog states it before the account is
erased rather than leaving them to discover it. A workspace the user merely
belongs to is untouched -- only their membership goes with them.

The tests move with the behaviour: the shared workspace is destroyed and its
colleague's own account survives, and owning one no longer holds the route
back.
2026-08-21 13:43:32 +02:00
..
e2e chore: bumped version to 0.0.31 2026-07-06 21:43:15 -07:00
fixtures test(tiktok): pin live fixtures, trim listing debug scaffolding 2026-07-08 17:53:29 +02:00
integration feat(account-deletion): take every owned workspace with the account 2026-08-21 13:43:32 +02:00
unit test(signup-credit): fold people/<sub> onto the bare subject id 2026-08-21 11:39:29 +02:00
utils feat(artifacts): add backend verification service 2026-08-12 03:04:49 +05:30
__init__.py feat: Add end-to-end tests for document upload pipeline and shared test utilities 2026-02-25 16:39:45 +05:30
conftest.py test: rename SearchSpace -> Workspace across tests + fixtures (Phase 2 Wave F) 2026-06-26 18:36:46 +02:00
README.md docs(tests): document offline hugging face mode 2026-08-12 03:06:09 +05:30

Tests

How the backend test suite is organized and the conventions to follow when adding tests.

Layout: type-first, module-mirrored

Tests are split by type at the top level, and each type mirrors the app/ module tree inside:

tests/
├── conftest.py                  # global fixtures + DATABASE_URL pinning
├── unit/                        # pure logic: no DB, no app, no network
│   └── notifications/
│       ├── api/test_transform.py
│       └── service/
│           ├── messages/test_connector_indexing.py
│           └── test_metadata.py
└── integration/                 # real PostgreSQL (pgvector)
    ├── conftest.py              # async engine, transactional db_session, db_user, ...
    └── notifications/
        ├── conftest.py          # module-scoped fixtures (e.g. transactional client)
        └── test_*_handler.py

To find a feature's tests, look under tests/<type>/<same path as app/>.

Unit vs integration

  • @pytest.mark.unit — pure, fast, no I/O. Test behavior through a public function's inputs/outputs.
  • @pytest.mark.integration — requires a real database. Run with AUTH_TYPE=LOCAL.

Maximize logic covered by unit tests; keep integration tests for what genuinely needs the DB (persistence, SQL filters, scoping, HTTP wiring).

Principles

  • Behavior, not implementation. Assert observable outputs (returned values, persisted rows, HTTP responses), never private helpers. Tests should survive a refactor.
  • Functional core / imperative shell. Put pure decision logic in a side-effect-free module (e.g. app/notifications/service/messages/) so it is unit-testable; keep the persistence shell thin and cover it with a few integration tests.
  • One responsibility per test file, mirroring the slice it covers.
  • Mock only at system boundaries (external APIs, brokers), never internal collaborators. Prefer dependency overrides and the transactional db_session over mocks.

Fixtures

conftest.py is scoped to its directory and below. Keep truly global fixtures in tests/conftest.py; put module-specific fixtures in that module's conftest.py so a DB fixture never loads for a pure unit test.

For API integration tests, override get_async_session and get_auth_context to ride the test's transactional db_session (see tests/integration/notifications/conftest.py): rows seeded in the test and rows read via the endpoint share one transaction that rolls back automatically.

Import mode

The suite uses --import-mode=importlib with pythonpath = ["."] (see pyproject.toml). This lets test files share basenames across modules (e.g. many test_api.py) without __init__.py boilerplate; new test directories do not need an __init__.py.

Running

# fast unit tests
uv run pytest -m unit

# integration (needs Postgres + pgvector)
AUTH_TYPE=LOCAL uv run pytest -m integration

# a single module's tests
uv run pytest tests/unit/notifications

Network-restricted machines

app/config/__init__.py instantiates the embedding model in the Config class body, so importing app.db — which tests/conftest.py does — reaches out to huggingface.co before a single test runs. Behind a proxy or an allowlist that fails the request (rather than refusing the connection) the HF client retries five times and then aborts collection, even when the weights are already cached.

Force the cached copy:

HF_HUB_OFFLINE=1 uv run pytest -m unit

This is the same switch the hermetic E2E container uses (docker/docker-compose.e2e.yml). It only works once the model is in ~/.cache/huggingface; warm it once from a machine with network access (same command as Dockerfile):

uv run python -c "from chonkie import AutoEmbeddings; AutoEmbeddings.get_embeddings('sentence-transformers/all-MiniLM-L6-v2')"