Skip to main content
Owner: Backend Engineering Last reviewed: 2026-09-24 Next review: 2026-12-24 Read the certificate lifecycle before running live checks. Use a test Infisical environment with disposable learners and courses, private storage, Redis and test notification delivery.

Automated checks

From server, with Node 24 and pnpm 9.15.4:
The controller suite starts a temporary HTTP server on loopback and substitutes authentication/CASL guards; it checks route protection wiring, validation, envelopes and binary responses. Service/worker tests mock repositories, storage and queues. These tests do not establish live JWT, database locking, storage or email delivery. The renderer tests use real fonts/template and validate reference positions, wrapping, dates, embedding and explicit rejection of unrenderable text.

Live API and persistence

  1. Apply the certificate migration and build the backend. Confirm dist/assets/certificates contains the PDF and three fonts. Start the API, certificate worker and outbox worker with test configuration from Infisical.
  2. Use two distinct learners. Complete a valid enrolled course as learner A. Verify automatic generation produces one certificate and one outbox event; the notification links to /my-certificate?certificateId=....
  3. Call POST /certificate/course/:courseId/issue as A. Expect 200 with metadata. Repeat it, including simultaneous HTTP requests and a queue job. Verify one database row, one issuance event, one ID/date and unchanged PDF SHA-256.
  4. List with page/limit and course filters. Empty results must be valid. Get the certificate metadata; confirm no storage key or other learners’ records leak.
  5. Download with A’s JWT. Expect raw application/pdf, attachment filename and private, no-store. Compare the downloaded SHA-256 with metadata. Repeat the download after changing A’s name/course title; bytes must remain unchanged.
  6. Attempt list/get/download/issue without JWT, with expired JWT and as B. No unauthenticated access; B cannot fetch A’s metadata or PDF (404). Passing A’s user ID/name in the issuance body must never issue as A.
  7. Verify new issuance rejects incomplete/empty courses, pending/rejected/refunded enrollment, expired access and partial course access. Verify fully purchased sections and both supported completion paths can qualify.
  8. Test stale/foreign video/lesson IDs and a nearly complete rounded percentage. Neither may grant a certificate. Progress writes must reject another learner, another course’s content and unpurchased sections.
  9. Simulate Redis unavailability during final progress: saved progress survives; the explicit issue endpoint recovers after infrastructure is healthy. Retry a failed generation job and confirm no duplicate issuance.
  10. Simulate storage upload failure: no certificate/outbox record should commit. Restore storage and retry. Simulate unavailable/corrupted stored content: download returns 503 rather than a broken PDF or a silent regeneration.
  11. Simulate email failure after issuance: the PDF stays downloadable and the outbox retries. Repeated successful issuance requests must not create new emails.
  12. Verify migration up on the target schema and unique index enforcement using concurrent real database transactions. Back up before any rollback test; dropping the table does not remove objects from storage.

Visual acceptance

Generate reproducible reference, long-text and accented-name examples locally:
The preview script does not issue database records or send notifications; its outputs go to server/tmp/certificate-previews by default. Render each generated PDF with Poppler and open it in a browser PDF viewer:
Compare the supplied filled reference with the same name, title, duration and date. Check the exact one-page landscape size, retained artwork, name blue, typography, left alignment and line breaks. Ensure fonts are embedded without renderer warnings and extracted text contains only the actual date, name and course description. Repeat with long names, accents, long course titles, unbroken words, missing duration and a UTC date boundary. No clipping, overlap with the completion sentence, footer or signature rules, or tiny unreadable text is acceptable. Empty names, unsupported scripts/emoji and excessive text must fail explicitly with 422, not issue a blank or corrupted certificate. Name/signature lines remain empty as in the supplied reference. This feature does not add an electronic or handwritten signature.