Running and testing¶
How to run Dinatos locally and test your changes. Assumes you finished
Environment setup. The project is a Python API
(backend/) plus a Flutter app (frontend/).
Fastest path: just dev¶
cp .env.example .env
# edit .env: set POSTGRES_PASSWORD (no default),
# and DINATOS_ADMIN_EMAIL / DINATOS_ADMIN_PASSWORD so you have an account to log in with
just dev
This starts Postgres in Docker, runs migrations, then runs the backend
(http://127.0.0.1:8000) and the frontend. Open http://127.0.0.1:8081 and
log in with the admin credentials you set.
Ctrl+C stops the backend and frontend. Postgres keeps running.
just docker downstops the containers and keeps the data.just dev-cleanremoves the containers and deletes the Postgres data volume. Read Upgrades and backups before using it on data you care about.
Running pieces separately¶
Backend only¶
Pick one.
Against a Postgres you run yourself:
docker run -d --name dinatos-pg -e POSTGRES_USER=dinatos -e POSTGRES_PASSWORD=dinatos \
-e POSTGRES_DB=dinatos -p 5432:5432 postgres:16-alpine
just backend migrate
just backend run # http://127.0.0.1:8000, interactive API docs at /docs
just backend run reads backend/.env and DINATOS_DATABASE_URL. The
default is postgresql+asyncpg://dinatos:dinatos@localhost:5432/dinatos.
In Docker (backend, web app and Postgres together):
just docker up # http://127.0.0.1:8000; migrations run on start
just docker logs
just docker down # keeps the data volume
This builds the Flutter web app too, so it is slow for backend-only edits.
Prefer just backend run for those. See
Deploying with Docker Compose.
Frontend only¶
cd frontend
just install
just run # flutter run -d web-server --web-port 8081, with hot reload
Open http://127.0.0.1:8081. The Server URL field on the login screen
(default http://127.0.0.1:8000) tells the app which backend to use. Point it
at a just dev backend to hot-reload the frontend alone. See
Getting started.
Creating an account for testing¶
DINATOS_ADMIN_EMAIL and DINATOS_ADMIN_PASSWORD create an admin account on
first start. Without them, the first account registered on a fresh instance
becomes admin:
curl -X POST http://127.0.0.1:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "at-least-8-characters"}'
token=$(curl -X POST http://127.0.0.1:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "at-least-8-characters"}' \
| jq -r .access_token)
curl http://127.0.0.1:8000/exercises -H "Authorization: Bearer $token"
Every endpoint except /health and /auth/* needs that bearer token. See
Authentication.
Testing¶
Command |
Tests |
Needs |
|---|---|---|
|
Lint, backend and MCP server tests, docs build (what CI’s |
Python, |
|
Backend tests only |
Python, |
|
Format, |
Flutter SDK |
|
Database migrations |
Docker |
|
The real web build in a browser |
Docker, Flutter SDK |
just check leaves out the frontend and Docker-based suites because they are
heavy. CI runs each as its own job; see CI and releases.
Backend tests¶
They run on in-memory sqlite. No Postgres needed.
The shared
clientfixture (backend/tests/conftest.py) registers a throwaway user and returns an already logged-inAsyncClient. Use theanonymous_clientfixture to test unauthenticated behavior.Test file names must be unique across
backend/tests/: there are no__init__.pyfiles, so twotest_auth.pyfiles fail collection with “import file mismatch”. Henceapi/test_auth.pyandservices/test_auth_service.pydiffer in name.A new database table must be added to
BACKED_UP_TABLESinbackend/src/dinatos_backend/services/backup.py(or toEXCLUDED_TABLESwith a reason), orjust testfails. Also add user-owned fields to theExported*models inschemas/backup.pyand toservices/user_export.py. See Backup and data export.Coverage needs
concurrency = ["greenlet"]in[tool.coverage.run](backend/pyproject.toml). SQLAlchemy’s async engine switches throughgreenlet, and without that setting coverage silently stops counting a function’s lines after its firstawait db.... If coverage drops sharply after you touch DB code, check this setting before assuming a regression.
Migration tests¶
A migration is a versioned script (Alembic) that changes the database schema.
just backend test-migrations runs pytest-alembic
against a throwaway Postgres (started by testcontainers). It checks:
a single head revision;
a clean upgrade from empty to head;
that the models match what the migrations produce;
test_up_down_consistency, which upgrades and downgrades every migration one at a time.
The last check matters most. It caught a downgrade() that left a Postgres
ENUM type behind and broke the next upgrade(). Sqlite cannot reproduce that,
so it needs real Postgres. The suite is outside backend/tests/ and not part
of just check; CI runs it as the migrations job.
To create a migration after changing a model (needs a running Postgres):
just backend revision "add widget table"
just backend check-migrations # fails if models and migrations diverge
Alembic’s own config lives in backend/pyproject.toml under [tool.alembic].
backend/alembic.ini exists only for logging sections. Do not add an
[alembic] section to it: a key in both places lets the ini value win
silently.
End-to-end tests¶
just e2e test builds the web app, starts a throwaway Postgres and a backend
serving that build, then drives it in Chromium with Playwright. Escape hatches
for iterating on the tests alone:
Variable |
Effect |
|---|---|
|
Skip the Flutter build |
|
Skip Docker |
|
Use this browser when Playwright’s own can’t be downloaded (“Executable doesn’t exist”) |
Gotchas when writing browser tests. Flutter web paints to a <canvas>, so
Playwright finds elements through Flutter’s accessibility tree:
Launch Chromium with
channel="chromium". The default headless shell lacks the WebGL support Flutter needs and renders a permanently blank page with no error. Behind a TLS-intercepting proxy, add--ignore-certificate-errorslocally; never commit it.Use
get_by_role(...)locators, not pixel coordinates. Roles: text fieldstextbox, checkboxescheckbox, buttons and FABsbutton, menu itemsmenuitem, bottom-bar tabstab, aCardis agroupnamed after its label. Icon-only buttons need atooltip:to have a name at all.Type with
press_sequentially(), notfill(). Flutter mirrors its own controller state back onto the<input>, sofill()can be overwritten with an empty value before Flutter sees it. Select-all and delete first if the field may already hold text.The wide layout’s
NavigationRailis missing from the accessibility tree, so e2e tests press it by coordinates. The bottom bar is fine.A click through the accessibility tree reaches the app as a semantic tap, not pointer events. A
Listener/onPointerUpnever sees it.Playwright refuses to click an element another element covers (such as a modal tour’s scrim). To test “blocked”, expect a short-timeout click to time out; do not use
force=True.The build uses
--no-web-resources-cdn. Without it Flutter fetches CanvasKit from Google’s CDN and, with no internet in the browser, the page never renders.A password-manager extension fills fields with synthetic
input/changeevents, which Flutter web ignores by default. Any new form that should work with one needs theWebAutofillSemantics(fields: ...)wrapper (fields are matched by label).e2e/test_autofill.pyreproduces the extension’s behavior.
Things only a real browser or device shows¶
flutter test and flutter analyze cannot catch these, so load the built app
when you change them:
CORS.
flutter testmakes no cross-origin request. A missing policy shows up only as a failed preflight in a browser. To check a web change:flutter build web --release, servebuild/web/, open it against a running backend.Android release permission.
flutter createaddsINTERNETonly to the debug and profile manifests. A release APK without it infrontend/android/app/src/main/AndroidManifest.xmlinstalls fine, then every request fails as “server not reachable”.just android check-apkverifies the permission on a built APK with no emulator.Keyring on Linux desktop. A locked or missing keyring makes
flutter_secure_storagethrow on every call.TokenStorageandServerUrlStorage(frontend/lib/core/) catch that on purpose. Keep the catches.
Pitfalls when editing build files¶
Dockerfile. The final
uv sync --locked --no-devneeds--no-editable. Otherwise the runtime image’s venv points at a build-stage path that does not exist, andimport dinatos_backendfails at container start, even thoughdocker buildsucceeds. Always run the built image against Postgres. Behind a TLS-intercepting proxy you may need to pass its CA bundle in (PIP_CERT,SSL_CERT_FILE); never commit that.Seed data for global tables (such as
exercises) needs a fresh database per run. A second “Bench Press” violates the unique constraint.screenshots/generate.pyuses a disposable Postgres for this reason.flutter create --platforms=...on this project overwritesfrontend/.metadata’s migration list, dropping the other platforms. Diff.metadataandanalysis_options.yamlafterwards and restore.Auth. Sessions are server-side rows (
AuthSession) with opaque random tokens, not JWTs, so logout truly revokes a session. Do not reintroduce JWT signing without reading Authentication. Passwords use Argon2 only; never a fast hash such as SHA-256. SHA-256 is correct for session tokens and CSV content hashes, which are high-entropy.
Next: CI and releases.