Skip to content

Accept invitations into a cross-product organisation, and stop the DB test flake - #44

Merged
rafiki270 merged 4 commits into
mainfrom
claude/db-test-startup-flake
Sep 5, 2026
Merged

Accept invitations into a cross-product organisation, and stop the DB test flake#44
rafiki270 merged 4 commits into
mainfrom
claude/db-test-startup-flake

Conversation

@rafiki270

Copy link
Copy Markdown
Contributor

Summary

Three fixes, found while verifying a live invitation end to end against production.

  • Cross-product acceptance (team-invite.service.acceptance.ts) — the function states that an organisation is usable from every UOA product, then filtered the invitee's memberships by the inviting product's domain. A cross-product invitee holds no membership row until acceptance creates one, so every such acceptance was refused with a bare 401 and rendered as "Invitation unavailable". Both gates now use the existing client/product-aware variants; the ACTIVE-membership tombstone rule is unchanged and the relaxation is gated on the inviting domain resolving to a single active product service. This is the security-relevant change in this PR and deserves the closest look.
  • Admin connection on acceptance (email-registration-link.ts) — consuming an invitation writes a domain role and two memberships, which row-level security refuses to the app role. The path added for existing accounts ran on the wrong connection and returned a 500 in production. Its regression test moved from the superuser-backed createTestDb to createRlsTestDb, and now reproduces that 500 exactly when the connection is wrong.
  • DB test flake (vitest.config.ts + 14 test files) — fourteen provisioning beforeAll hooks passed their own timeout, overriding the configured 180s. The two set to 30s could not finish: createTestDb backs off ~31s across six startup retries, so the hook was killed mid-retry. That took API tests (Postgres) red at random. A unit test now names any hook that reintroduces one.

Testing

  • pnpm --filter @uoa/api test and the full Postgres integration suite pass locally, including the RLS-backed invitation regression.
  • The cross-product acceptance test is the one thing not verified locally: Docker died on the machine after it was written, so CI's Postgres is its first real run.
  • The invitation flow was walked end to end against production — invitation created through Nessie, email received, landing page, Accept — which is how the 500 and then the 401 were found.

🤖 Generated with Claude Code

Ondrej Rafaj and others added 4 commits September 5, 2026 21:14
…accepted

`acceptTeamInviteWithinTransaction` states its own invariant plainly: one
organisation is usable from every UOA-integrated product, so the org's origin
domain is not an acceptance predicate. The scope gate at the end of the same
function then contradicted it, filtering the invitee's memberships by
`org.domain = config.domain`. A cross-product invitee can never satisfy that:
they hold no membership row anywhere until this call creates one, and the org
belongs to whichever product founded it. Acceptance was refused with a bare
401, surfacing as "Invitation unavailable" on the mail-bound flow.

This is the live failure behind the invitations that never landed: the
organisation was founded through one product and invited into through
another.

Both gates now use the client/product-aware variants that already exist for
exactly this. The tombstone rule is untouched — exact ACTIVE organisation and
team membership is still required — and the domain relaxation applies only
where the inviting domain resolves to a single active product service.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`clientDomain.create` takes `label`, not a client secret hash. CI caught it;
Docker had died locally before the test could run once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`billing_app_keys_purpose_origins_check` requires them for
CUSTOMER_LIFECYCLE; dropping the field from the copied fixture broke the
insert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-product membership and the single-product policy are read on the
BYPASSRLS client, which is resolved once from the ambient environment and so
never sees a test's isolated schema. Acceptance now takes them as optional
dependencies; production callers pass nothing and behave exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rafiki270
rafiki270 merged commit b09dee7 into main Sep 5, 2026
2 checks passed
@rafiki270
rafiki270 deleted the claude/db-test-startup-flake branch September 5, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant