docs: add testing strategy, migration workflow, error catalog, and security policy - #597
Merged
codebestia merged 2 commits intoAug 31, 2026
Merged
Conversation
|
@Deb-Auth Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Adds three documents covering gaps in the contributor-facing documentation, plus the matching rows in the documentation index. docs/testing.md - cross-app testing philosophy and conventions: - States the no-external-services rule explicitly, with the reasoning and the approved substitute for each dependency (ioredis-mock for Redis, a hand-built db mock for Postgres, LocalDiskObjectStore for S3/MinIO, fake-indexeddb and the web setup shims, pytest-mock fixtures for the AI agent, and the Soroban Env harness for contracts). - Documents the per-app runners and commands: vitest for backend and web, pytest for the AI agent, cargo test for contracts, plus the include and exclude patterns that decide which files are collected. - Explains the established Drizzle mocking pattern, including why the module under test is imported with a dynamic import after the mocks, and why .values() sometimes has to be both thenable and expose .returning(): the message insert chains .returning() for the generated id while the envelope insert only awaits .values(), so a stub supporting one shape silently records nothing for the other. - Documents that socket handlers must be driven through the enveloped dispatch event rather than a raw socket.on listener, since there is no raw listener and grabbing one bypasses envelope validation, the auth gate, and eventId idempotency. - Notes the shared in-process rate-limit counters that leak between tests, with the reset hooks for that and the other module-level state. apps/backend/docs/migrations.md - drizzle-kit migration workflow: - Establishes schema.ts as the source of truth and migrations as generated output, never hand-written first, with the one documented exception. - Documents the drizzle/ layout and meta/_journal.json as the file that decides what actually runs, and that a .sql file missing from it is silently skipped. - Documents the merge hazard concretely against the incident already in this repository's history: seven colliding 0001_* files and a journal entry with duplicate JSON keys, which left nine migrations unlisted and forced a squash back to a single baseline. - Gives the recommended conflict-resolution procedure - merge schema.ts, take the base branch's drizzle/ wholesale, drop your own migration, and regenerate - plus the checks that catch a collision before it lands. - Notes that drizzle/meta/ is Prettier-ignored because it is generated output. SECURITY.md - vulnerability disclosure policy: - Gives GitHub private vulnerability reporting as the private channel, with acknowledgement, triage, update, and fix windows. - States explicitly that vulnerabilities must not be reported through public issues, pull requests, or discussions, and why. - Defines scope across the backend, web client crypto, contracts, AI agent, and supply chain, with an explicit out-of-scope list and testing rules. - Cross-links docs/threat-model.md so reporters can tell an accepted residual metadata risk from a real finding. - Adds a contract-specific path: only token_transfer is upgradeable, so a bug in group_treasury or proposals cannot be fixed by a redeploy and funds already in a vulnerable instance may be unrecoverable. Closes codebestia#545 Closes codebestia#546 Closes codebestia#548
Deb-Auth
force-pushed
the
docs/testing-migrations-security
branch
from
August 31, 2026 00:02
921ecff to
5ed1162
Compare
Adds apps/backend/docs/contracts-error-catalog.md, cataloguing every error shape the backend returns across both transports, plus the matching rows in the documentation index. Contents: - One REST table, deduplicated to a row per distinct (status, error) pair, listing the extra fields each carries and every route that emits it. Middleware-level errors are split into their own table since they can accompany any authenticated route. - The dynamic REST statuses that do not appear as literal res.status calls: the validateMessagePayload results behind POST /messages, and the checkEnvelopeProtocols 400/409 pair with its violations array. - Rate-limit responses, which do not have one shape: the standard rateLimit() middleware sets RateLimit-Limit/Remaining/Reset plus Retry-After, the upload byte quota sets only Retry-After, and the group-invite throttle sets no headers at all. Documents the safe client rule for reading a backoff from any of the three. - Socket errors, keyed by the emitted event value, including the condition-valued ones: device_set_mismatch, protocol_mismatch, rate_limited, envelope_too_large, device_revoked, and payload_too_large. Notes that rate_limited carries the throttled event in limitedEvent rather than event, and that envelope_too_large is a code value rather than an event value. - The two distinct socket error payload shapes: the dispatcher wraps its rejections in the standard event envelope, while the security middleware and every messaging handler emit a bare payload. Includes the normalising snippet a client needs to handle both. - Handshake failures, which arrive on connect_error as a plain Error rather than as an error event, so a client listening only on error sees nothing. - A retryable/terminal breakdown, including the cases where a naive retry is actively wrong: an auth nonce is single-use so a 401 there means restarting from the challenge, a message retry must reuse the same messageId to stay idempotent, and an MLS epoch conflict must be rebuilt against the returned currentEpoch. Two findings surfaced while cataloguing, documented rather than changed: there is no global Express error handler, so an exception escaping a route returns Express's HTML page instead of a JSON body; and auditLogsRouter is defined and tested but never mounted in app.ts, so its two errors are currently unreachable over HTTP. Closes codebestia#555
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds four contributor-facing documents that were missing from the repository, plus the matching rows in
docs/README.md(the index requires every new.mdfile to be listed).Documentation only — no source, schema, config, or workflow changes.
docs/testing.md— testing strategy and conventions (#545)The cross-app testing philosophy and the conventions a contributor has to follow.
ioredis-mockfor Redis (andnullfor the deliberate Redis-is-down fallbacks), a hand-builtdbmock for Postgres,LocalDiskObjectStorefor S3/MinIO,fake-indexeddbplus theapps/web/src/test/setup.tsshims for the web suite, thepytest-mockfixtures inconftest.pyfor the AI agent, andEnv::default()for contracts.pnpm db:migratestep, not for the tests, and thatsecurity-ci.yml— which runs backend tests with no containers at all — is the standing proof the suite is service-free.cargo testfor contracts, with the config file for each and theinclude/excludepatterns that decide which files get collected (including whydist/is excluded, and that the webincludesilently skips.tsxtest files).../db/index.js,../db/schema.js, anddrizzle-ormtogether, and why the module under test is imported withawait import(...)after the mocks rather than a static import..values()sometimes has to be both thenable and expose.returning(): Drizzle's insert builder is itself awaitable and chainable. The message insert calls.returning()because it needs the generatedidandcreatedAt; the envelope batch insert just awaits.values(...). A stub returning{ returning }alone makes the awaited call resolve to a plain object and silently record nothing — the test passes while asserting on an insert that never happened. A stub returning only a promise makes.returning()throw. Includes the canonical dual-shape stub frome2ee.integration.test.ts, plus two cautions about the recording side effect firing twice.dispatchevent, never a rawsocket.on(type, ...)listener — there is no raw listener to grab, and a test written that way bypasses envelope validation, the auth gate, andeventIdidempotency, which is exactly the surface those checks protect. Gives thedispatchEventhelper and the four things to preserve when copying it (uniqueeventId, currenttimestamp,socket.authset first, register through the real registrar).services/rateLimiter.tskeeps alocalCountersmap used whenever Redis is unavailable — which, in a suite that mocksredistonull, is always. Several tests hitting the same endpoint as the same subject share one budget, so a test passes alone and returns429in file order. DocumentsclearLocalRateLimitCounters()/resetRateLimitBucket(bucket)and the equivalent reset hooks for the prekey low-watermark latches and the presence offline-broadcast set.apps/backend/docs/migrations.md— database migration workflow (#546)schema.tsis the source of truth; migrations are generated from it and never hand-written first, because drizzle-kit diffs against the snapshot rather than the database and will re-emit or undo a change it has no record of. The one legitimate exception (backfills,CREATE INDEX CONCURRENTLY) is to edit the generated file, not to add an ungenerated one.pnpm db:generate→ review the emitted SQL (renames that arrive as drop-plus-add,NOT NULLwithout a default, type changes needingUSING) →pnpm db:migrate, with a warning aboutdb:push.drizzle/layout: the.sqlfiles,meta/_journal.json, and the per-migration snapshots. Calls out that_journal.jsondecides what actually runs —db:migratereads the journal, not the directory listing, so a.sqlfile with no journal entry is silently skipped with no error or warning.d60b648,drizzle/held seven distinct0001_*.sqlfiles, and the journal'sidx: 1entry contained three duplicatewhen/tagkey pairs inside one object. Duplicate JSON keys are not an error — last one wins — so that entry ran exactly one migration. Thirteen.sqlfiles, four journal entries: nine migrations silently skipped, and the history had to be squashed back to a single0000_lean_scrambler.sqlbaseline.schema.tson its own terms, take the base branch'sdrizzle/wholesale, delete your own branch's migration and snapshot, regenerate, review, and verify against a scratch database. Plus five mechanical checks that catch a collision before it lands.drizzle/meta/is Prettier-ignored because it is generated output that drizzle-kit rewrites in its own format on the next generate.SECURITY.md— vulnerability disclosure policy (#548)For an E2EE product with on-chain funds, a finder currently has no private option at all.
/security/advisories/new), with a maintainer-contact fallback — and response windows: acknowledgement in 3 business days, triage in 10, updates every 14 days, fix or documented mitigation for a confirmed high-severity issue within 90 days of triage.upgrade), plus the AI agent and supply chain — with an explicit out-of-scope list and testing rules.docs/threat-model.mdso reporters can distinguish a documented, accepted residual metadata risk (social graph, traffic analysis, presence patterns, device fingerprinting) from a real finding — and says plainly that content, key material, or session state being observable when the threat model says otherwise is exactly what we want to hear about.token_transferhas anupgradeentrypoint, whilegroup_treasuryandproposalsexpose none at all, so fixing either means deploying a new contract, migrating state, and repointing every consumer. Funds already in a vulnerable instance may be unrecoverable. Reproduce inEnv::default()or on testnet — never against a live treasury — and the strongest proof of concept is a failing#[test].apps/backend/docs/contracts-error-catalog.md— error code and response catalog (#555)Every error shape the backend returns, across both transports, in one place. There was previously nowhere to look up what a client can actually receive.
(status, error)pair, giving the extra fields each carries and every route that emits it. Middleware errors (requireAuth,validate,transportSecurity,rateLimit) are split into their own table since they can accompany any authenticated route. Also covers the statuses that never appear as a literalres.status(...)call: thevalidateMessagePayloadresults behindPOST /messages, and thecheckEnvelopeProtocols400/409pair with itsviolations[]shape.eventvalue, including the non-obvious ones the issue calls out —device_set_mismatch(envelopes missing for the sender's own sibling devices),protocol_mismatch,rate_limited, andenvelope_too_large— plusdevice_revokedandpayload_too_largefrom the connection middleware. Two traps are documented explicitly:rate_limitedcarries the throttled event inlimitedEvent, notevent, andenvelope_too_largeis acodevalue that arrives inside a normalsend_messagepayload rather than aneventvalue of its own.{ eventId, type, timestamp, payload }), while the security middleware and every messaging handler emit a bare{ event, message, code? }. A client must handle both; the doc includes the normalising snippet. It also notes that three of the four dispatcher errors carry noeventfield at all, so handlers must not key on its presence.errorevents — they reject during the Socket.IO handshake and surface onconnect_erroras a plainError. A client listening only onerrorsees nothing for a bad token, a revoked device, or a blocked origin.rateLimit()middleware setsRateLimit-Limit/Remaining/ResetplusRetry-After; the upload byte quota sets onlyRetry-After; the group-invite throttle sets no headers at all. The doc gives the safe client rule for reading a backoff from any of the three, and notes thatRateLimit-*are also set on successful responses so a client can back off before being rejected.401there means restarting fromPOST /auth/challengerather than resubmitting; a message retry must reuse the samemessageIdor a committed-but-failed-looking send duplicates the message; and an MLSCommit epoch conflictmust be rebuilt against thecurrentEpochreturned in the response.Two findings surfaced while cataloguing. Both are documented rather than changed, since this PR is documentation only:
500, not{ error }— so a client that assumes every non-2xx body is JSON will throw while parsing.auditLogsRouteris never mounted. It is defined insrc/routes/auditLogs.tsand exercised inauditLog.test.ts, butapp.tsdoes not mount it, so its two errors are currently unreachable over HTTP. Listed in the catalog and flagged rather than silently presented as live.Type of change
Checklist
Verification
npx prettier --checkpasses on all four changed files. The repo-widepnpm format:checkreports a large number of pre-existing failures on files this PR does not touch; none of them are from this branch.res.status(...).json(...)call acrosssrc/routes/andsrc/middleware/(180 call sites, deduplicated to 120 distinct pairs) and everysocket.emit('error', ...)/createEnvelope('error', ...)site was parsed, then each was mapped back to its enclosing route. Two parser artefacts were hand-corrected against the source, and the dynamic-status call sites were expanded manually..values()/.returning()dual shape againste2ee.integration.test.ts, the dispatch-only handler path againstsocket/dispatcher.ts, the reset hooks againstservices/rateLimiter.ts,services/prekeyLowSignal.ts, andservices/presence.ts, the journal incident againstgit show d60b648^:apps/backend/drizzle/meta/_journal.json, and the contract upgradeability claims againstcontracts/docs/concepts-upgrades.md.docs/testing.mdwas verified empirically by runningpnpm --filter backend teston a machine with Docker stopped and no Postgres, Redis, or MinIO running.docs/README.mdwas edited by hand rather than reformatted, so its diff is 8 added rows and nothing else.Notes for the maintainer
SECURITY.mdneeds one repository setting to work. GitHub private vulnerability reporting must be enabled (Settings → Code security and analysis → Private vulnerability reporting), otherwise the advisory URL in the policy 404s for reporters. If you would prefer a security email address as the channel instead, say the word and I will swap it.Closes #545
Closes #546
Closes #548
Closes #555