Skip to content

test(e2e): module edit/delete + cross-tenant isolation - #43

Merged
simonorzel26 merged 3 commits into
mainfrom
feat/e2e-edit-delete
Jul 29, 2026
Merged

test(e2e): module edit/delete + cross-tenant isolation#43
simonorzel26 merged 3 commits into
mainfrom
feat/e2e-edit-delete

Conversation

@simonorzel26

Copy link
Copy Markdown
Contributor

Replaces #42 (auto-closed when its stacked base branch merged). Two review-driven coverage specs, now based on main.

Module edit + delete (e2e/l2/edit-delete.spec.ts)

The l2 sweep was create-only, so CrudPage's update/delete paths (the PR #25 bug class in the edit dialog, silent corruption on update) were untested. Drives both through the real UI on risks + policies: open a created row's edit form, change the title, assert the new value in UI and Postgres and the old value gone; then delete via the row button (accepting window.confirm) and assert gone from UI and Postgres. Shared CrudPage flow, so two modules prove the mechanism. Product hooks: data-testid=row-edit/row-delete on the two pages' row buttons.

Cross-tenant isolation (e2e/l3/cross-tenant.spec.ts)

The highest-severity gap: the suite ran as one company, so a dropped companyId filter in any ownership guard was undetectable. Provisions a real second tenant (Rival GmbH + an asset), then from the Dev GmbH session fires asset.list/update/delete at Rival's asset over the real tRPC endpoint and asserts in Postgres that Rival's data is untouched. Non-vacuous: the list attack returns Dev GmbH's own seeded asset (proves it ran authenticated) and the mutation endpoints answer (not 404), so a dropped tenant filter in list, update, or delete each fails a distinct assertion.

Verified

Full suite 82 passed + 1 skip; typecheck clean; rebased onto main (carries the #40 fix).

Note (not fixed): PoliciesPage logs MISSING_MESSAGE for a status value that isn't a known i18n key — an arbitrary status string breaks that row's translated render. Product follow-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01De1NY1HDUJkkwetVKTbYtH

The l2 sweep only ever created records, so CrudPage's onUpdate/onDelete
paths were untested — the PR #25 "form cannot submit" bug class in the
edit dialog, and silent-corruption on update, had zero coverage.

New e2e/l2/edit-delete.spec.ts drives both through the real UI on two
representative CrudPage modules (risks, policies): open the created row's
edit form via a scoped row-edit button, change the title, save, assert the
new value in UI and Postgres and the old value gone; then create another,
delete it via the row-delete button (accepting the window.confirm), and
assert it is gone from UI and Postgres. The CrudPage flow (row action ->
SchemaForm defaultValues -> update/delete -> refresh) is shared by all
twelve modules, so two cover the mechanism.

Product hooks: data-testid row-edit / row-delete on the RisksPage and
PoliciesPage row action buttons (icon-only, previously unselectable).

Full suite 81 passed + 1 skip; typecheck clean.

Note surfaced (not fixed): PoliciesPage logs MISSING_MESSAGE for a status
value that is not a known i18n key — an arbitrary status string breaks the
row's translated render (same "string column with app-level enum" class
as the numeric-500 finding).
…s data

The suite ran as a single seeded company, so a dropped companyId filter in
any ownership guard was structurally undetectable — the company-ending
failure mode for a multi-tenant compliance product had zero coverage.

New e2e/l3/cross-tenant.spec.ts provisions a real second tenant (Rival
GmbH + an asset) via SQL, then from the authenticated Dev GmbH session
attacks Rival's asset over the real tRPC endpoint: asset.list, asset.update
and asset.delete by Rival's id. Ground truth is Postgres after the attack:
Rival's asset must still exist with its original name.

Non-vacuity is asserted so this cannot pass on a 404 or an unauthenticated
no-op: asset.list returns the attacker's OWN seeded asset (Customer
Database), and the update/delete endpoints answer (not 404). A dropped
tenant filter in list/update/delete each fails a distinct assertion.

Full suite 82 passed + 1 skip; typecheck clean.
Cross-tenant: add a positive control. The list attack was self-proving
(returns the attacker's own asset), but update/delete only asserted the
endpoint was not 404 — which cannot tell "tenant .where blocked it" from
"the request never reached the resolver" (envelope/validation/activation
break, or a 500 from any cause). The same session now updates and deletes
its OWN asset first and asserts the DB actually changed, so the mutation
path is proven live; a future break that silently stops the request fails
here loudly instead of green-lighting the cross-tenant no-op.

Edit spec: assert the edited row exists (>= 1) rather than exactly 1, so a
reused local server (no per-run DB reset) with a prior run's edited row
does not spuriously fail. Persistence is still proven; the old-title-gone
check stays in the UI assertion.

6 specs pass; typecheck clean.
@simonorzel26
simonorzel26 merged commit a19be40 into main Jul 29, 2026
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