2 Verification
Ulysia edited this page 2026-10-06 16:54:30 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Mirrored from VERIFICATION.md at commit f31ec6a. Edit the file in the repository: this page is regenerated from it, and edits made here are overwritten.

Verification status

In production use since 2026-08-31. Most of the bundle is exercised daily; the gaps below are specific and named.

tsc passing says the types line up. It says nothing about whether Gotenberg renders a page or Authentik's payloads match what we parse — so this file tracks what has actually been observed working, and what has not.

Two other things check what type-checking cannot, and neither replaces this file: scripts/check-upstream.sh (upstream contract, run in CI on every push) and di-check.ts (DI graph, needs a running container — see below).

This file lists what to check first and where reality is most likely to diverge. Delete entries as they're confirmed.


Confirmed in production

Reported working by the operator of the live instance on 2026-08-31, with the specific evidence noted. Recorded rather than deleted, because "we checked this and it works" is more useful than silence.

  • Audit logging persists. The highest-priority unknown, now closed: SELECT count(*) FROM audit returns rows, and the contents are sensible — logouts, API key creation and deletion. So AuditEeService does win the AUDIT_SERVICE binding over core's NoopAuditModule, and the "last global module wins" assumption in app.module.ts holds in practice.
  • SCIM against real Authentik. This was the riskiest thing in the bundle — the only part written from a spec rather than reverse-engineered from an existing client contract, with PATCH payload shapes the likeliest divergence. Users, groups and group sync all work.
  • SCIM deprovisioning kills live sessions. Unassigning a user ends their session on the next request, and reassigning restores access. This was the explicit end-to-end test this file used to ask for.
  • Externally-synced groups are locked in the UI (patch 0008). SCIM-synced groups show the badge, cannot be renamed or deleted directly, and members cannot be removed by hand — only through the IdP.
  • OIDC SSO, including the local-account merge paths.
  • Group-driven workspace roles (group-role/, patch 0009).
  • Bases / Kanban. All six stages, in daily use. This had been the largest outstanding risk in the bundle: every query, the realtime bridge and the queue processor were untested against a real database, and the specific worries were jsonb_set_many merge semantics on cell patches, the ?|/@> operators against real jsonb, and whether BaseRealtimeBridge resolves BaseWsService through ModuleRef. All of it holds.
  • MCP over OAuth, with a read+write grant (27 Sept 2026). A real client (Claude) completed discovery, dynamic registration, consent and the token exchange, and every tool that existed then was exercised against the live instance: page create/read/append/rename, reorder and reparent via move_page, threaded comments, attachments on a real page, and the base read/row tools. That pass found one bug — list_comments doubled the space at every mark boundary — fixed in c626d9f. Two parts of the OAuth flow are still unexercised and stay under "Check these first".

Check these first

1. The base schema and bulk tools — NEWEST CODE

create_base, create_property, update_property, delete_property, create_base_rows, upsert_base_rows, create_base_view, update_base_view and delete_base. Unit-tested (mcp/base-schema-tools.spec.ts, with negative controls for option-rename id stability, noon pinning, case sensitive keys and duplicate-key refusal), and the DI constructor tokens were probed, but none of it has run against a real database. In likelihood-of-breaking order:

  • The server boots. These add a provider and two BaseModule exports. A DI failure is a boot failure — a 502, not an error page. If it will not start, run node dist/ee/di-check.js in the container.
  • An open tab sees the change. Everything goes through the base services, which broadcast websocket events. With the base open in a browser, create a property and rename an option over MCP: both should appear without a reload.
  • clearedCells is right. Remove an option used by a known number of rows and compare the reported count to what the grid shows afterwards.
  • A date-only column shows the right day. Write 2026-03-01 and an instant like 2026-03-01T23:30:00Z to an includeTime: false column, and check both read as 1 March in the browser — ideally from a machine in a negative UTC offset, which is where the midnight bug lived.
  • Upsert paging. The unit test proves pagination is followed with a stub; against a base above 200 rows, confirm a key on a later page updates rather than duplicates.
  • A view built over MCP renders, and survives the UI. Create one with visibleProperties, a sort and a filter, open it in the browser, drag a column, then get_base: the columns reported should match what the browser shows, before and after the drag.
  • trash_page lands in the trash and restores. Trash a test base, check the space trash and the audit log (PAGE_TRASHED), restore it, and confirm its rows are back.
  • get_page on a row page resolves people. Use a row that is not the base's first: names used to come back empty for every other row.

1b. The two OAuth paths a read+write grant cannot test

  • Scope enforcement. Approve read only, then confirm the client sees no write tools and that a write is refused. Then check a @OAuthScope('write') core route also refuses that token — that path runs through core's guard reading user.oauth.scopes, not our code.
  • Revocation. Revoke from the authorized-apps panel and confirm the client fails on its next call, not an hour later.
SELECT c.name, g.scopes, g.last_used_at FROM oauth_grants g
  JOIN oauth_clients c ON c.id = g.client_id;

2. PDF export end to end

Failure modes, in likelihood order:

  • Gotenberg can't reach APP_URL — see DEPLOYMENT.md. Symptom: Gotenberg returns a non-200 and the job fails with its response body.
  • waitForExpression never satisfied — pdf-export.service.ts waits on document.querySelector('.ProseMirror') !== null. If that isn't the right selector for the rendered editor, you get a blank or partial PDF rather than an error. Check a real output before trusting it.
  • Gotenberg v8 route paths (/forms/chromium/convert/url) are from memory, not verified against the pinned image.

3. Tika extraction

PUT /tika with Accept: text/plain / text/html is long-stable API, but verify against the pinned tag. Check that a scanned PDF returns empty (and is stored as '', not NULL, so backfill doesn't retry it forever) rather than erroring.

4. MFA login flow

Should be testable standalone with no external service: enable MFA in account settings, log out, log back in. Confirm the pending-login mfaToken cookie path works — /mfa/setup, /mfa/enable, /mfa/verify and /mfa/validate-access deliberately carry no JwtAuthGuard, since forced setup happens before a session exists.


Running SQL against the instance

Several checks here are a single query. From the host, not from inside a container:

docker compose exec db psql -U docmost -d docmost

If that reports role "docmost" does not exist, the role is not what the compose file says: POSTGRES_USER is only honoured when the data directory is first initialised, and is silently ignored on every later start. The credentials the app actually uses are the authoritative ones —

docker compose exec docmost printenv DATABASE_URL

— and psql -U postgres -c '\du' lists every role in the cluster.

Check which container you are in first. A container-management TUI started from this directory can attach you to an unrelated Postgres container elsewhere on the same host. That reports role "docmost" does not exist too, which reads like a credentials problem and is not one.

Checking the DI graph

tsc cannot catch a missing Nest provider — it's a runtime module-graph problem. Run this before any deploy:

docker compose exec -w /app/apps/server docmost node dist/ee/di-check.js

It builds the real application (Fastify adapter, init() but never listen()), forcing every provider to resolve, then verifies each of core's scattered require() + moduleRef.get() hooks into this bundle actually resolves — the failure that produced a 500 on every API-key request while the build was green.

Needs the app's normal environment (postgres, redis), which is why it runs in the container. An earlier version used createApplicationContext and claimed to need neither; that was wrong twice over. CollaborationModule's onModuleInit calls httpAdapter.getHttpServer(), which is null without an HTTP adapter, so init always failed — and the old catch-all reported that as "infrastructure, expected" and exited 0. The hook checks never ran. Any build failure now exits non-zero.

init() starts BullMQ workers for the few seconds it lives, so run it while the instance is idle rather than mid-import.

Expect a skip line for anything genuinely not bundled (typesense), and ok for everything else. Modules without a scattered hook (page-permission, scim, bases) are covered by the graph check rather than listed.

Run it against the compiled output, never through tsx — the TS loader resolves circular imports differently and reports false failures in core (UserService ... undefined at runtime) that don't happen in production.

This exists because the first real deploy died on exactly this: DocxImportService injected AttachmentService, but AttachmentModule is neither @Global() nor exports it. Fixed by using AttachmentRepo from the global DatabaseModule.

Verified in isolation

Run as standalone scripts against the real engine, not mocked. Each caught at least one genuine bug:

  • Filter SQL — compiled a representative filter and read the output. Confirmed jsonb's ?| survives parameterisation without colliding with the driver's placeholders, LIKE metacharacters in user input are escaped, and unknown properties drop out of both filter and sort.
  • Timezones — resolved presets across UTC, Europe/Prague, America/Los_Angeles and Pacific/Kiritimati at a fixed instant. thisMonth in Prague correctly starts in CET and ends in CEST; the spring-forward day is 23 hours.
  • Formulas — compiled and evaluated nested formulas in dependency order, confirmed transitive affectedFormulas, DIV_BY_ZERO producing an error cell, and cycle rejection. Caught that FormulaParseError exposes errors not issues, and that evalOrder() includes non-formula dependencies.
  • Type conversion — walked the conversion matrix. Caught that converting out of a choice column emitted option ids rather than labels.

Known unknowns

  • The base schema tools have not run against a database. See "Check these first", item 1. The OAuth flow itself is verified for a read+write grant; read-only scope enforcement and revocation are not (item 1b).
  • Confluence import is deliberately dropped, not missing — see the status table in README.md. Core keeps the hook (ee/confluence-import/confluence-import.service), so an attempted import logs an error and imports nothing. That is the intended degradation, not a gap to close.
  • Filter-only attachment search returns nothing. Upstream made SearchDTO.query optional in 917195b2 so a search can be filters-only. Page search honours that; searchAttachments still returns an empty list when there is no query text, which is what it did before.
  • Page verification QMS transitions were inferred from the client's conditional rendering (status === "draft" gates submit, etc.), not from a spec. The shape is unambiguous but edge behaviour is our reading — e.g. whether re-submitting an already-approved page should be allowed.
  • DOCX export image fallback returns a 1×1 transparent PNG when an image can't be resolved, because the serializer has no "skip this node" concept. It logs a warning; it does not fail loudly.
  • /sso/providers and /sso/info return oidcClientSecret in plaintext to the admin UI. TODO marked in sso/services/sso.service.ts.
  • Row list pagination is offset-based, so a concurrent insert can shift a row across a page boundary. A known design tradeoff rather than an unknown: keyset over arbitrary nullable jsonb sort expressions was judged not worth the complexity. Bases is in production and this has not been reported, which is expected — it needs a concurrent insert during paging to show at all.
  • getSsoProviderById() in the client is buggy — it accepts a providerId argument and never sends it. The server tolerates this by falling back to "the workspace's only provider". That breaks with a second IdP; fix the client then.

Fixed during the build loop

Recorded because they're all the same category — environment-specific typing, not logic — and the next batch will probably be too:

  • ActorType imported from the wrong module (it re-exports from common/events/audit-events, not integrations/audit/audit.service).
  • catch (err) types as unknown; core uses catch (err: any) where it inspects the error.
  • @types/node v24 makes Buffer generic, so it no longer satisfies fetch's BodyInit — wrap as a Uint8Array view.
  • spaces has no icon column; it's logo.
  • .where(sql\…`, 'in', array)` types operands against the sql expression, not a column — express as one raw predicate instead.