Skip to content

test: add a full automated test suite (and fix six defects it uncovered) - #117

Merged
scolastico merged 9 commits into
mainfrom
feat/full-test-suite
Jul 30, 2026
Merged

test: add a full automated test suite (and fix six defects it uncovered)#117
scolastico merged 9 commits into
mainfrom
feat/full-test-suite

Conversation

@scolastico

Copy link
Copy Markdown
Member

Description

Establishes a complete automated test suite for everything the server currently does, and fixes the defects the tests uncovered.

62 tests across 9 suites, running fully offline (sqlite3 :memory:, S3 stubbed with aws-sdk-client-mock, no live valkey/redis). pnpm test is now part of PR CI.

Test infrastructure

  • jest.config.js + pnpm test / pnpm test:cov
  • test/app.tscreateTestApp() mirrors main.ts bootstrapping (rawBody, body-parser limits) and sandboxes env vars per suite
  • test/esm-fix-mock.ts — deterministic stand-in for the fix-esm loaded modules, whose runtime require hooks do not work under jest
  • test/jest.setup.ts — silences module-global Nest loggers so output stays readable

Coverage

  • Unit: config helpers, CryptoService, getIp (proxy trust, IPv6 stripping, per-request caching)
  • E2E: note create (json + text) / fetch (raw, json, self-destruct) / delete (ip + token auth) / token limits; decryption (query + body, wrong key, length cap, missing note); root redirect, /info, /stats, stats password, instance password guard; request rate limit, failed-request ban, 418 block, limits-disabled bypass; the full file upload lifecycle with every documented error code; both cleanup crons and migrations (up, idempotent, revert)

Deliberately not covered: the swagger UI itself (main.ts-only), fatal-exit startup paths, remote trusted-proxy URL fetching (network), and the maxTokensPerRequest / decryptionRequestMultiplier config options, which no controller references.

Bugs fixed

Each was found by a failing test, not by inspection:

  1. CryptoService.decrypt had its AES arguments swapped — it decrypted the user's key using the note content as the passphrase, so /note/:id/decrypt could never return the plaintext. It also now rejects empty/undecryptable results with 401 instead of returning an empty string.
  2. GET/POST /note/:id/decrypt returned 500 for an unknown idgetDecrypted dereferenced note.content without a null check. Now 404.
  3. The file part counter was written fire-and-forget — missing await on updateFile(id, { part }) made part counting racy.
  4. Stale multipart uploads were never aborted. getUploadFilesLastUpdatedBefore used where("upload_id", "!=", null), which knex compiles to upload_id != NULL — never true in SQL. Abandoned uploads therefore survived until their expiry instead of being cleaned up after uploadPartTimeInMinutes. Now whereNotNull. (getStats's two-arg where(col, null) forms compile correctly; only this query was affected.)
  5. deleteFile never invalidated its cache entry. getFile caches for 30s, so a deleted or aborted upload kept being served from a stale cache entry. Now mirrors deleteNote.
  6. Two status codes disagreed with swagger. POST /note/:id/decrypt returned Nest's default 201 where 200 is documented, and PUT /file/upload/:id returned 200 where 204 is documented. Both now match the documented contract.

Additional Notes

tsconfig.json gains "types": ["node", "jest"] — TypeScript 6 no longer auto-includes @types/jest globals, so the spec files would not compile without it.

Note for local development: the prebuilt sqlite3 binary requires glibc 2.38. On older distros it fails to load and needs npm_config_build_from_source=true pnpm rebuild sqlite3. CI's ubuntu-latest runner is unaffected.

Checklist

  • I have tested my changes locally
  • I have updated the documentation if needed
  • This PR follows the project's coding style

…and honor documented status codes

- await the part counter update so sequential part requests are deterministic
- invalidate the file-<id> cache in deleteFile, mirroring deleteNote, so a
  deleted upload is no longer served from a stale cache entry
- POST /note/:id/decrypt now returns the documented 200 instead of Nest's
  default 201
- PUT /file/upload/:id now returns the documented 204 instead of 200

Adds file transfer e2e coverage for the full upload lifecycle.
…nce e2e tests

The stale-upload cleanup used where("upload_id", "!=", null), which knex
compiles to "upload_id != NULL" — never true in SQL — so abandoned
multipart uploads were never aborted before their expiry.
@scolastico
scolastico merged commit 66b45ff into main Jul 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant