Naar de inhoud
Documentatie doorbladeren

De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.

Besloten

Testing strategy

What is tested where: Postgres rules with pgTAP, edge functions with Deno, app logic with unit tests, screens with UI tests, and the full flow end to end.

Bijgewerkt: 29 October 2026

The pyramid

LayerToolRunsOwns
Postgres schema, RLS, functionspgTAP in supabase/tests/*.sqlCI and before db pushEvery visibility rule, every function guard, every trigger
Edge functionsDeno tests beside handler.tsCIPush fan-out, deletion, export, e-mail code verification, with injected fakes
App domain logicJUnit/Truth + XCTestCI, pre-commitValidation, formatting, snapshot diffing, sync reconciliation buckets, view-model state
ScreensCompose UI tests + XCUITest, semantics onlyCI (emulator/simulator)Each screen renders each state; accessibility acceptance
End to endrun-e2e.ps1 (Android; the XCUITest port follows when iOS resumes), against local Supabase with seed.sqlbefore a merge into develop (smoke), before a phase is delivered and before a release (full)The whole functional design, scenario by scenario: see End-to-end test process
WebsitevitestCIi18n parity, no literal store URLs outside StoreBadges, sitemap consistency

The visibility matrix (pgTAP)

build_snapshot is the most important function in the system and gets a table-driven test: three profiles (Anna, Ben, Cara), each with names, two phones, an e-mail, an address in fields mode, a birthday with hidden year and a photo, three circles each; every pairwise connection in every circle; assertions that each detail appears in exactly the snapshots whose circle is in its circle_ids, that the year appears only where year_circle_ids allows, that address fields follow field_scopes, and that a non-connected profile receives null. Then: move Ben to another circle → Anna’s snapshot changes and a circle_changed notification exists for Ben only if his view changed; delete a circle with members → error; remove a detail → hash changes, contact_updated emitted once per affected connection; block → snapshot removed and search returns nothing.

RLS is tested by set local role authenticated; set local request.jwt.claims = '{"sub": "<uuid>"}' and asserting row counts on every table for owner, connected stranger and unrelated stranger.

The pgTAP suite

Sixteen files, 803 assertions, run with npx supabase test db (which drives pg_prove). Each file wraps itself in begin … rollback, so tests leave nothing behind and file order does not matter.

FileWhat it pins
01_schema_and_profiles.sqlAn account row appears for every auth user; a new profile arrives with its default circles and system detail types; the main profile cannot be deleted by hand; nobody reads another account’s rows
02_rls_ownership.sqlThe ownership matrix: one row of Anna’s in every profile-scoped table, each asked the same three questions as owner, as a connected contact and as a stranger
03_details_constraints.sqlA detail may only point at circles and types of its own profile; the default name drives display_name; verification is never self-awarded; four photos per profile under a path derived from the row’s own ids
04_snapshot_matrix.sqlThe visibility matrix above, asked of every pair and every circle, plus the year rule, the field-level address rule and the null for a non-connected profile
05_invitations.sqlsend_invitation and accept_invitation refuse everything they should, and when they say yes both sides of the relationship are complete in one transaction
06_search.sqlTokens are derived from exactly the details that may be found, and search_profiles refuses malformed tokens, applies the AND rule over name words, and excludes opted-out, blocked and departed profiles
07_blocks_and_removal.sqlEnding a relationship ends both rows and tells nobody (A102): no notification for either side, after a removal, a block or a profile deletion
08_account_lifecycle.sqlDeletion leaves nothing anywhere, tells every contact exactly once, keeps the audit row; the export is complete; the disposable-domain hook refuses a sign-up
09_storage_policies.sqlcan_view_photo matches the table rule exactly, including the whole-name match, and the push trigger survives a missing Vault entry
10_notifications_rate_limits.sqlConnections and invitations are function-only; a notification’s kind is the database’s word; a circle with members refuses to disappear; rate limiting, audit and coalescing behave
11_profile_rules.sqlThe five detail limits hold at their boundary under a row lock; a year circle is always a day-and-month circle; delete_profile refuses the main profile and a stranger, and tells every connected profile exactly once; replace_detail_type moves every detail and refuses a system type
12_phase4_contacts.sqlThe invitation reminder is the sender’s, once, and only after seven days; the circle preview is the owner’s and equals what a member of that circle already holds; the name tokeniser matches its golden vector word for word; the expiry sweep is on the hourly schedule
13_invitation_parties.sqlEach side of a pending invitation learns the other’s default name and avatar path and nothing else; a third account and an id that is not an invitation get the same empty answer; two hundred ids are a page and more than that is refused
14_sign_in_email.sqlThe address an account signs in with is verified by the server wherever it is written: on insert, on an edit that turns another address into it, and on a managed profile of the same account; the same address on somebody else’s profile proves nothing; a searchable sign-in address is findable through search_profiles without a code
15_prune_notifications.sqlA notification older than ninety days is deleted, one that is one day short of it is not; the function returns the count it removed; nobody but the nightly job can call it; the schedule exists exactly once
16_phase5_services.sqlA service and its links are the owner’s alone and are writable only in the columns the client owns; ten services is the limit; a link belongs to its service’s profile; removing a contact keeps the address-book entry, lets the contact go and moves updated_at, while an entry that was never written goes with it; cross_reference takes two hundred hashed tokens and no names, echoes the token that matched, applies the same four exclusions as search and refuses the thirty-first call in an hour; a synced circle that is deleted leaves circle_ids instead of wedging the row; a link beside an orphaned entry is refused as a duplicate and adopted by an update instead; delete_account_data takes both tables with it

Saymail’s web/supabase/tests are plain plpgsql scripts that raise on a failed expectation. Incico uses pgTAP through the CLI instead: plan(), is(), throws_ok() and results_eq() give a count and a TAP report, so a file that stops early is a failure rather than a short run.

Edge functions have 132 Deno tests beside every handler and every shared module, run with:

deno test --allow-env --allow-net=127.0.0.1 supabase/functions

.github/workflows/backend.yml runs both suites on every push and pull request touching supabase/**, after deno lint and deno fmt --check, and finishes with a supabase db reset so a broken seed.sql fails the build.

The v1 Playwright scenarios, ported

  1. Register three users, complete onboarding, add and remove each detail type.
  2. Circle CRUD: cancel discards, duplicate names get “(2)”, delete needs confirmation and is blocked while members exist, rename inline, toggle per-detail circles.
  3. Send invitations with circle and relation.
  4. Accept and decline; verify field-level visibility immediately; move a contact between circles.
  5. Nickname editing and cancel semantics.
  6. Changing a circle’s shared details propagates to the other party’s view.
  7. Connect a service (device contacts on the emulator), pick circles, run sync, verify buckets, auto-numbered service names.
  8. Pending-sync badges only on services whose circles include the changed detail.
  9. Account deletion cascades and removes the user from other people’s lists.

The nine are part of the larger catalogue in End-to-end test process, which also covers sign-in, the profile editors, notifications, settings, offline behaviour, tablets, languages and accessibility. Each scenario exists as a Compose instrumented test; the XCUITest port follows when iOS resumes, sharing the IDs and step names.

Golden vectors

docs/vectors/phone-format.json and address-format.json exist and are loaded by both test suites (Gradle resources.srcDir("../../../docs/vectors"), Xcode folder reference), so +31612345678 formats identically on both platforms and a Dutch address renders as one line the same way. Both are generated or checked rather than hand-kept: tools/gen-phone-vectors.mjs produces every phone expectation from libphonenumber-js, and tools/vectors-check.mjs recomputes every address display from the rule in mobile/design/address-formats.json and fails CI on a mismatch. docs/vectors/search-tokens.json and avatar-colour.json joined them in phase 4: the first pins the three rules a device must apply before it calls search_profiles (a device that hashes differently finds nobody), the second pins the initials-avatar colour, so the same person is never orange on one screen and green on another. Both are recomputed by tools/vectors-check.mjs, and the search tokens are asserted a second time against the SQL functions by supabase/tests/12_phase4_contacts.sql. snapshot-hash.json and circle-dots.json follow with the features that need them.

Naming and structure

Action_WithCondition_ShouldResult, Arrange-Act-Assert with comments, one test class per source class, builders in Builders/ beside the tests, files under 400 lines, both success and failure paths. Fakes over mocks on iOS; MockK on Android where a fake would be bigger than the test.

Release gate

A release requires: CI green on main; E2E suites green on the emulator and on the simulator; manual test.md items checked; supabase db diff empty against the staging project after deploy.