All checks were successful
CI / Build container images (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 2m57s
CI / Import/export fidelity gate (push) Successful in 46s
CD / Build and push images (push) Successful in 3m16s
CD / Deploy to Test (push) Successful in 8s
CI / Auth e2e pack (push) Successful in 4m8s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 10s
Implements the security core of the plugin system: code-plugin surfaces run in opaque-origin iframes (sandbox="allow-scripts", never allow-same-origin) with a capability-filtered RPC bridge. - api: serve a per-plugin sandbox frame document at /plugins/:id/:version/frame with a CSP that pins every load to the plugin's own asset path (built from APP_BASE_URL, not the request Host, so a Host-rewriting proxy cannot break it) and forbids network access (connect-src 'none'). Plugin assets get Access-Control-Allow-Origin: * so the null-origin frame can load its own module bundle. - web: sandbox-host creates the frame, wires the SDK host bridge over a source-filtered postMessage transport, drives render under a 5 s deadline (hung/failed plugin -> placeholder, never a frozen page), and tears down on unmount. PluginFrame/PluginPreviewPage surface it; the built-in ui.resize handler clamps plugin-requested heights. - plugin-sdk: host bridge reports gate violations via onViolation and registers a gated handler for every v1 method, so an undeclared capability is rejected with capability_not_permitted (not unknown_method). - tests: SDK gate unit test; web sandbox unit tests (opaque origin, source filtering, timeout); and the e2e security pack with a permanent malicious fixture plugin proving no escape (DOM/cookies/storage/fetch/ undeclared capability all blocked) plus well-behaved and hung cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
168 lines
9.2 KiB
Markdown
168 lines
9.2 KiB
Markdown
# End-to-end tests
|
||
|
||
Playwright suites, most local-only — three run in CI/CD:
|
||
|
||
| Suite | Target | Where it runs |
|
||
| ----------------- | --------------------------------- | --------------------------------------------------------------------------- |
|
||
| `smoke.spec.ts` | any deployed stage | CD pipeline against `https://test.dorfteich.cloud` after every deploy |
|
||
| `auth.spec.ts` | full local stack **with Mailpit** | CI job `auth-e2e` on every PR/push; locally against the dev stack |
|
||
| `content.spec.ts` | full local stack | same CI job `auth-e2e` (a second step), right after the auth pack |
|
||
| everything else | full local stack | locally only — `editor`/`sidebar`/`image`/`link`/`markdown`/`trash`.spec.ts |
|
||
|
||
`content.spec.ts` is the M2 content regression pack (issue #32): page
|
||
lifecycle, editor basics, image paste, Markdown round-trip, and trash —
|
||
enough to catch a regression across the whole content model without
|
||
re-running every edge case the feature-specific packs above already cover.
|
||
Its Markdown round-trip test is a real regression pin, not just a smoke
|
||
check: it compares the seeded "Every Element" fixture page's exported
|
||
Markdown byte-for-byte against the checked-in `content-page.md` (see
|
||
"Content fixtures" below) — any schema/serializer change that alters how a
|
||
node round-trips fails it, once the seed has re-run against the changed
|
||
code (build → migrate → seed → test, exactly CI's order).
|
||
|
||
## Running locally
|
||
|
||
```sh
|
||
# 1. Stack: database + Mailpit, api (3001), web dev server (5173)
|
||
docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/compose.dev.yml up -d db mailpit
|
||
DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm --filter @dorfteich/api db:seed
|
||
DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich PORT=3001 pnpm --filter @dorfteich/api start:dev &
|
||
pnpm --filter @dorfteich/web dev &
|
||
|
||
# 2. Tests
|
||
E2E_BASE_URL=http://localhost:5173 E2E_MAILPIT_URL=http://localhost:8025 pnpm --filter @dorfteich/web e2e
|
||
```
|
||
|
||
`auth.spec.ts` skips itself when `E2E_MAILPIT_URL` is unset, so the CD
|
||
smoke run never trips over it.
|
||
|
||
## Fixture matrix
|
||
|
||
Seeded by `pnpm --filter @dorfteich/api db:seed` (idempotent — re-running
|
||
never duplicates). Shared password: `fixture passwort 123`. Fixtures exist
|
||
only on dev machines and disposable CI/Test databases.
|
||
|
||
| Username | State | Purpose |
|
||
| ------------------ | ------------------- | ---------------------------------------------------------------------------------------- |
|
||
| `fixture-admin` | active, Site Admin | admin UI/permissions cases |
|
||
| `fixture-user` | active | regular journeys, settings, sessions |
|
||
| `fixture-editor` | active | second regular account for the collab-permissions pack (reader/editor of another's pond) |
|
||
| `fixture-viewer` | active | signed-in non-member for `authenticated`/`public` access-rule cases (issue #55) |
|
||
| `fixture-outsider` | active | the "foreign user" of the permission matrix — member of nothing (issue #60) |
|
||
| `fixture-pending` | e-mail not verified | unverified-login cases |
|
||
|
||
## Permission matrix (`permission-matrix.spec.ts`, issue #60)
|
||
|
||
The cross-feature permission hardening pack pins the security-relevant
|
||
**subject × surface** combinations against regressions. It is API-level (the
|
||
UI adds nothing over the resolved status code) and enforces the 404-vs-403
|
||
policy: an unauthorized _read_ is 404 (existence hidden), an unauthorized
|
||
_write_ on something readable is 403.
|
||
|
||
- **Subjects:** site admin (`fixture-admin`), pond admin / owner
|
||
(`fixture-user`), editor (`fixture-editor`), the same editor _label-restricted_
|
||
by a `secret`-label deny, reader (`fixture-viewer`), public (anonymous), and
|
||
the foreign user (`fixture-outsider`, a member of nothing).
|
||
- **Surfaces:** page read, edit (collab-token `rw`/`ro`), sidebar list, search,
|
||
versions (history = write), media, and the public HTML endpoint.
|
||
- **Extending it:** a new permission-touching feature adds a surface here (one
|
||
`expect` row per subject) rather than a bespoke test, so the matrix stays the
|
||
one place the policy is pinned. A weakened guard is caught here — verified by
|
||
temporarily loosening a route decorator and watching the pack go red.
|
||
|
||
## Plugin sandbox (`plugins.spec.ts`, issue #73)
|
||
|
||
The sandbox security pack drives the Site-Admin plugin preview page
|
||
(`/admin/plugins/:id/preview`) — the exact sandbox runtime pages embed — with
|
||
three fixture plugins built in `plugin-fixtures.ts` and installed through the
|
||
admin API:
|
||
|
||
- **well-behaved**: answers `render` and resizes its own frame via the declared
|
||
`ui` capability;
|
||
- **malicious** (the permanent security regression asset, ADR 0008): probes the
|
||
parent DOM, cookies, `localStorage`, same-origin and external `fetch`, and an
|
||
undeclared capability — every probe must report `blocked`. A `LEAKED` verdict
|
||
is a sandbox escape and fails the build;
|
||
- **hung**: never answers, so the host's 5 s deadline must collapse it to the
|
||
failure placeholder while the surrounding page stays responsive.
|
||
|
||
Runs in the `auth-e2e` CI job (needs the api's writable `PLUGINS_DIR`, satisfied
|
||
by the default `./data/plugins`).
|
||
|
||
## Attachments (`attachments.spec.ts`, issue #61)
|
||
|
||
Non-image attachments: a page's attachments section uploads an allowlisted
|
||
file, lists it, and inserts it into the document as a download link (verified
|
||
to serve with `Content-Disposition: attachment` + `nosniff`, never inline); a
|
||
disallowed extension is rejected with the localized allowlist error; the Pond
|
||
Admin file manager reports storage usage and flags an orphan (a pond-level
|
||
upload with no embedding page). The SVG sanitize/reject policy is covered at
|
||
the api level in `files.e2e.db.test.ts`.
|
||
|
||
## Import (`import.spec.ts`, issue #64)
|
||
|
||
The sidebar "import document" action: pick a file, upload, watch progress, open
|
||
the new page. `.md` imports directly (the response is already `succeeded`);
|
||
`.docx`/`.odt` poll a conversion job. Cases: a `.docx` corpus fixture (#63)
|
||
opens the converted page, a `.md` opens directly, an unsupported `.txt` shows
|
||
the localized error and creates no page, and two concurrent `.md` imports both
|
||
complete. The **`.docx` case self-skips unless `E2E_PANDOC` is set** — CI's e2e
|
||
stack has no reachable pandoc sidecar (jobs are container-networked; same reason
|
||
the api's real-pandoc fixtures test skips in CI, #63), so it runs locally / on a
|
||
stage. Run it locally with a sidecar reachable at the api's `PANDOC_URL`:
|
||
|
||
```sh
|
||
docker run -d -p 3030:3030 pandoc/core:3.6 server # api PANDOC_URL → this
|
||
E2E_PANDOC=1 E2E_BASE_URL=http://localhost:5990 \
|
||
pnpm --filter @dorfteich/web exec playwright test e2e/import.spec.ts
|
||
```
|
||
|
||
## Export (`export.spec.ts`, issue #65)
|
||
|
||
The pond-settings "Download pond as ZIP" link (a Markdown ZIP of the readable
|
||
pages) and the page-menu office export. The ZIP download needs no sidecar; the
|
||
`.docx` export runs a conversion job and, like the import `.docx` case,
|
||
**self-skips unless `E2E_PANDOC` is set**. The permission-filtered ZIP contents,
|
||
the docx pandoc output, and the 500-page streaming path are covered at the api
|
||
level in `export.service.db.test.ts` (+ `export-markdown.test.ts`).
|
||
|
||
## Content fixtures
|
||
|
||
`db:seed` also creates a **shared** pond `content-fixtures` (owned by
|
||
`fixture-user`) with two pages, for the content regression pack and manual
|
||
QA:
|
||
|
||
- **Every Element** (`every-element`) — every editor schema node and mark
|
||
(issue #24: headings 1–4, all list types, table, blockquote, code block,
|
||
horizontal rule, hard break, and all five marks). Loaded from the
|
||
checked-in `apps/api/prisma/fixtures/content-page.yjs`, a Yjs snapshot
|
||
generated from the human-readable `content-page.md` next to it —
|
||
`content-page.md` is the thing to read or edit; the `.yjs` file is a
|
||
build artifact of it, not source.
|
||
- **Fixture Image** (`fixture-image`) — one real, servable uploaded image
|
||
(the placeholder `fileId` inside the Markdown fixture above is not a
|
||
real attachment; this page's image is).
|
||
|
||
Regenerating after editing `content-page.md`:
|
||
|
||
```sh
|
||
pnpm --filter @dorfteich/api fixtures:regenerate
|
||
```
|
||
|
||
This is deterministic — re-running without editing the Markdown produces a
|
||
byte-identical `.yjs` file (the script pins the Yjs document's `clientID`,
|
||
which is otherwise randomized per `Y.Doc` instance) — and it refuses to
|
||
write a snapshot that isn't a fixed point of the Markdown round-trip
|
||
(`docToMarkdown(markdownToDoc(x)) === x`), so a stale fixture can't get
|
||
checked in silently.
|
||
|
||
## Conventions
|
||
|
||
- New feature packs get their own `<feature>.spec.ts` next to these and
|
||
extend the fixture matrix here (permission matrix arrives with M5,
|
||
issue #60).
|
||
- Use `contextForUser()` from `helpers.ts` for signed-in tests — it logs
|
||
in through the api and hands you a browser context with the session
|
||
cookie, no UI login repetition.
|
||
- Flaky tests are defects (ADR 0014): fix or quarantine immediately.
|