Certora · Test run · run 2026-09-25
Certora Test run, as shipped in the download
295 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 with php tests/run.php. Run it yourself after you download.
== 1. Cert::merge — merge-token substitution ==
PASS recipient token (got 'Hi Maya Rivera!', want 'Hi Maya Rivera!')
PASS all five tokens (got 'Maya Rivera/Project Management/Jul 6, 2026/Northwind Institute/ABCD1234WXYZ', want 'Maya Rivera/Project Management/Jul 6, 2026/Northwind Institute/ABCD1234WXYZ')
PASS repeated token (got 'Maya Rivera & Maya Rivera', want 'Maya Rivera & Maya Rivera')
PASS unknown token kept (got 'Score: {score}', want 'Score: {score}')
PASS no tokens untouched (got 'Plain body text.', want 'Plain body text.')
PASS no re-expansion of injected token (got '{code}=x ZZZ', want '{code}=x ZZZ')
PASS empty value for missing var (got '[]', want '[]')
PASS tokensUsed detects set (got array (
0 => 'recipient',
1 => 'course',
2 => 'date',
), want array (
0 => 'recipient',
1 => 'course',
2 => 'date',
))
PASS tokensUsed empty (got array (
), want array (
))
== 2. Cert::newCode — shape, alphabet, length ==
PASS code length is 12 (got 12, want 12)
PASS code uses only the safe alphabet
PASS code excludes ambiguous 0/O/1/I/L
PASS isValidCodeShape true for a fresh code
PASS isValidCodeShape false for lowercase
PASS isValidCodeShape false for empty
PASS isValidCodeShape false for symbols
== 3. Cert::newUniqueCode — no collisions across a batch of 1000 ==
PASS 1000 issued codes are all unique
PASS exactly 1000 distinct codes recorded (got 1000, want 1000)
PASS newUniqueCode returns a valid code after retries
== 4. Cert::normalizeCode + verification-lookup semantics ==
PASS dashes stripped + uppercased (got 'ABCD2345WXYZ', want 'ABCD2345WXYZ')
PASS spaces stripped (got 'ABCD2345WXYZ', want 'ABCD2345WXYZ')
PASS out-of-alphabet chars dropped (got 'ABCDEFGH', want 'ABCDEFGH')
PASS ambiguous 0/1/I/O/L dropped (got 'ABC', want 'ABC')
PASS empty stays empty (got '', want '')
PASS displayCode groups by four (got 'ABCD-2345-WXYZ', want 'ABCD-2345-WXYZ')
PASS valid code (with dashes) -> found (got 'Maya Rivera', want 'Maya Rivera')
PASS wrong code -> not found (null) (got NULL, want NULL)
PASS empty input -> not found (null) (got NULL, want NULL)
== 5. Cert::throttle — fixed-window per-IP rate limit ==
PASS first hit opens a window (got array (
0 => true,
1 => 1,
), want array (
0 => true,
1 => 1,
))
PASS 2nd hit allowed (got array (
0 => true,
1 => 2,
), want array (
0 => true,
1 => 2,
))
PASS 3rd hit at limit allowed (got array (
0 => true,
1 => 3,
), want array (
0 => true,
1 => 3,
))
PASS 4th hit blocked (got array (
0 => false,
1 => 4,
), want array (
0 => false,
1 => 4,
))
PASS elapsed window resets (got array (
0 => true,
1 => 1,
2 => 1000061,
), want array (
0 => true,
1 => 1,
2 => 1000061,
))
== 6. Cert::normalizeOrientation ==
PASS landscape passes (got 'landscape', want 'landscape')
PASS portrait passes (got 'portrait', want 'portrait')
PASS junk -> landscape (got 'landscape', want 'landscape')
== 7. Cert::csvCell — numeric-aware formula-injection guard ==
PASS =SUM neutralised (got '\'=SUM(A1)', want '\'=SUM(A1)')
PASS +1 neutralised (got '\'+1', want '\'+1')
PASS -CMD neutralised (got '\'-2+3+cmd', want '\'-2+3+cmd')
PASS @cmd neutralised (got '\'@cmd', want '\'@cmd')
PASS leading TAB neutralised (got '\' x', want '\' x')
PASS cmd payload neutralised (got '\'=cmd|calc', want '\'=cmd|calc')
PASS CR payload quoted+prefixed (got '"\'
X"', want '"\'
X"')
PASS -45.00 kept numeric (got '-45.00', want '-45.00')
PASS 1250 kept numeric (got '1250', want '1250')
PASS plain name untouched (got 'Maya Rivera', want 'Maya Rivera')
PASS mid-string = untouched (got 'a=b', want 'a=b')
PASS comma/quote RFC-4180 quoted (got '"Doe, ""Jr"""', want '"Doe, ""Jr"""')
== 8. Cert::toCsv — header + rows, RFC-4180, CRLF ==
PASS header row (got 'Code,Recipient,Course', want 'Code,Recipient,Course')
PASS row count (header+2) (got 3, want 3)
PASS CRLF line endings
PASS comma field quoted
PASS formula code defused
== 8b. Cert::expiryDate — month arithmetic, CLAMPED at month ends ==
PASS plain +12 months (got '2027-08-04', want '2027-08-04')
PASS plain +1 month (got '2026-04-15', want '2026-04-15')
PASS 0 months = never expires (got NULL, want NULL)
PASS negative = never expires (got NULL, want NULL)
PASS Jan 31 + 1mo clamps to Feb 28 (got '2026-02-28', want '2026-02-28')
PASS Jan 31 + 1mo leap year -> Feb 29 (got '2028-02-29', want '2028-02-29')
PASS Aug 31 + 6mo clamps to Feb 28 (got '2027-02-28', want '2027-02-28')
PASS Feb 29 + 12mo clamps to Feb 28 (got '2029-02-28', want '2029-02-28')
PASS Dec 31 + 2mo clamps to Feb 28 (got '2027-02-28', want '2027-02-28')
PASS year rollover Dec + 1mo (got '2027-01-15', want '2027-01-15')
PASS +24 months across years (got '2028-05-10', want '2028-05-10')
PASS +120 months (10y) (got '2036-08-04', want '2036-08-04')
== 8c. Cert::expiryStatus / certStatus — boundary + precedence ==
PASS null = never expires (got 'none', want 'none')
PASS empty = never expires (got 'none', want 'none')
PASS expires TODAY -> still valid (got 'valid', want 'valid')
PASS expires tomorrow -> valid (got 'valid', want 'valid')
PASS expired yesterday -> expired (got 'expired', want 'expired')
PASS certStatus valid (got 'valid', want 'valid')
PASS certStatus expired (got 'expired', want 'expired')
PASS certStatus never-expires (got 'valid', want 'valid')
PASS revoked beats valid (got 'revoked', want 'revoked')
PASS revoked beats expired (got 'revoked', want 'revoked')
PASS revoked, never-expires (got 'revoked', want 'revoked')
== 9. csrf_valid: empty-token bypass rejected (hash_equals('','') is TRUE) ==
PASS empty stored + empty given -> REJECTED
PASS empty stored + any given -> REJECTED
PASS matching token -> accepted
PASS mismatched token -> rejected
== 10. PdfCert::render — valid plain-text PDF document ==
PASS starts with %PDF header
PASS ends with %%EOF
PASS has xref table
PASS has a page object
PASS non-trivial length
PASS long line wraps to 2+ chunks
== 11. PdfCert::renderCertificate — valid certificate page, both orientations ==
PASS rendered body carries the recipient
PASS rendered body carries the merged code
PASS landscape starts with %PDF
PASS landscape ends with %%EOF
PASS landscape MediaBox 792x612
PASS landscape draws a border rect
PASS landscape has a page object
PASS landscape non-trivial length
PASS portrait MediaBox 612x792
PASS portrait starts with %PDF
PASS portrait ends with %%EOF
PASS portrait draws a border rect
PASS unicode title still valid PDF
== 11b. every certificate line prints at the size asked for (no 1 pt text) ==
PASS the signatory title prints at 10 pt (got '10', want '10')
PASS the issue / expiry line prints at 10 pt, with both dates readable (got '10', want '10')
PASS the title (24), body (13), signatory (13), header (12) and verify line (9) keep their sizes (got array (
0 => '24',
1 => '13',
2 => '13',
3 => '12',
4 => '9',
), want array (
0 => '24',
1 => '13',
2 => '13',
3 => '12',
4 => '9',
))
PASS no line anywhere on the page is set below 9 pt
PASS font sizes format without eating whole-number zeros (got array (
0 => '10',
1 => '20',
2 => '9',
3 => '10.5',
4 => '24',
), want array (
0 => '10',
1 => '20',
2 => '9',
3 => '10.5',
4 => '24',
))
PASS no made-up verify domain is written by the demo seed or the demo reset
PASS the demo seed leaves the owner's verify_url alone
PASS the hosted demo takes its public verify address from CERTORA_DEMO_VERIFY_URL, http(s) only
== The verify link as a QR code on the certificate ==
PASS qr: qr_decodes_back_to_the_payload
PASS qr: qr_reed_solomon_parity_is_valid
PASS qr: qr_format_block_names_the_real_mask
PASS qr: qr_covers_versions_1_to_10
PASS qr: qr_refuses_an_over_long_payload
PASS qr: golden matrices identical to Assetora's proven encoder
PASS landscape: the QR drawn on the page is exactly the encoder's matrix for the verify link
PASS landscape: no footer text runs into the QR code
PASS portrait: the QR drawn on the page is exactly the encoder's matrix for the verify link
PASS portrait: no footer text runs into the QR code
PASS no verify link, no QR code
PASS only an http(s) link becomes a QR code
PASS text first: a verify line too long to sit beside the code keeps its full size, and the code is left off
PASS the certificate PDF passes the QR link only for a standing certificate with an http(s) verify address
PASS index.php loads the QR encoder
== Load demo data keeps the owner's settings ==
PASS the seed check ran cleanly (no .demo-mode marker in the tree)
PASS loading demo data on a real install leaves the owner's organisation, issuer and verify address as they were
PASS ...and still loads the demo records
== Invitations by email through your own SMTP ==
PASS the invite check ran cleanly
PASS the email names the organisation, the app, the role, the link and the 7-day expiry
PASS a line break in the organisation name cannot add a mail header
PASS with no SMTP server set, the box is not offered
PASS ...and a send is refused with a reason, not attempted
PASS with SMTP set, the box is offered
PASS an unreachable mail server comes back as a reported failure, never an exception
PASS a real send goes out through the owner's SMTP server
PASS the invitee's message carries their address, the link and the expiry
PASS the invite handler emails only when "Email the link" is ticked, and still shows the link once
PASS the public demo never sends an invitation
PASS the invite form offers "Email the link", ticked only when SMTP is set
== Backups and restore are for an administrator only ==
PASS the backup guard check ran cleanly
PASS a signed-in viewer is refused backup_json (403)
PASS a signed-in registrar is refused backup_json (403)
PASS a signed-in viewer is refused backup_sqlite (403)
PASS a signed-in registrar is refused backup_sqlite (403)
PASS a signed-in viewer is refused restore_page (403)
PASS a signed-in registrar is refused restore_page (403)
PASS a signed-in viewer is refused restore_upload (403)
PASS a signed-in registrar is refused restore_upload (403)
PASS a signed-in viewer is refused restore_commit (403)
PASS a signed-in registrar is refused restore_commit (403)
PASS a signed-in viewer is refused backup_token_mint (403)
PASS a signed-in registrar is refused backup_token_mint (403)
PASS an administrator still gets the backup download
PASS the scheduled backup answers to its token only, even for a signed-in administrator
PASS the Security page shows the Backups card only to someone allowed to download it
== PWA ==
PASS manifest.json is valid JSON
PASS manifest has required fields
PASS manifest URLs are RELATIVE (subpath-install safe)
PASS manifest icons exist on disk (192 + 512 + apple-touch)
PASS sw.js never caches non-GET
PASS sw.js never caches page navigations (auth HTML can not enter cache storage)
PASS sw.js cache name carries the version (old caches purged on activate)
PASS offline fallback page ships
PASS every app layout links the manifest + registers the SW
-- Own It 2.0 --
PASS v2: tables
PASS v2: totp_cols
PASS v2: smtp_cols
PASS v2: mint
PASS v2: revoke
PASS v2: totp
PASS v2: recovery
PASS v2: delivery
PASS v2: rate
PASS v2: csv_dry
PASS v2: backup_tables
PASS v2: cert_serializer
PASS v2: routes registered (api, 2fa, security, backup, import, healthz)
PASS v2: public verify endpoint is registered and key-free
PASS v2: 2FA intercepts login + login is rate-limited
PASS v2: notifications never fatal
PASS v2: API reuses the UI core (_issue_certificate) instead of re-deriving issuance
PASS v2: public verify reuses the page throttle (no unthrottled code oracle)
PASS v2: batch endpoint validates the WHOLE batch before issuing any certificate
PASS v2: no ternary key re-reads in ANY controller (PHP 8 warning hygiene, PLAN item 10)
PASS v2: dark palette + zero hardcoded white backgrounds
PASS v2: theme.js + qr.js ship and layout loads theme.js + a toggle
PASS v2: API.md ships + sw cache carries the CURRENT version (v3.1.5)
PASS v2: cli-server base guard present (extension-bearing routes work under php -S)
-- Own It 3.0 --
PASS v3: every copied core is byte-identical to the shared core
PASS v3: the certificate renderer is PdfCert, and the core Pdf is deliberately absent
PASS v3: index.php calls Database::migrate() after the schemaExists guard
PASS v3: Database::migrate() ensures the settings singleton
PASS v3: the installer writes its settings even though the row already exists
PASS v3: v3_mcp_spec() provides every key Mcp::handle() reads
PASS v3: GET /mcp reaches the transport hint WITHOUT the key check in front of it
PASS v3: issue_certificates allowlists its recipients array
PASS v3: a recipients element that is not an id is REFUSED, never coerced to recipient #1
PASS v3: /backup.sqlite is carved out of the blanket .sqlite deny (dev router AND .htaccess)
PASS v3: every MCP tool carries a role guard (verify is the one documented exception)
PASS v3: the batch writer is shared, never a second issue path
PASS v3: status and expiry come from Cert::, never re-derived
PASS v3: every admin-surface handler is permission-gated, not merely login-gated
PASS v3: purging a certificate is not reachable through certificate.*
PASS v3: the role ladder means what the Users screen says it means
PASS v3: every POST route that mutates is audited
PASS v3: the audit CSV reads the same rows the screen does, with the core's writer
PASS v3: the shareable backup redacts credentials, not just the SMTP password
PASS v3: password_hash and token_hash are in the redaction list
PASS v3: restore reports what it actually wrote
PASS v3: the restore page states the JSON-vs-.sqlite trade-off
PASS v3: anonymise refuses while a certificate can still be relied upon
PASS v3: the accent AND the AA-safe ink are published to BOTH theme selectors
PASS v3: no rule hardcodes the light value of a token the dark theme redefines
PASS v3: dark mode reads its label and button ink from tokens, not literals
PASS v3: the uploaded logo is RENDERED, with a URL /media can serve
PASS v3: the brand accent reaches the certificate PDF itself
PASS v3: the calendar link is RENDERED once after minting, not just stored
PASS v3: every view gets the variables it reads from its controller
PASS v3: deploy kit ships
PASS v3: the deploy healthcheck is the frozen template's, not a hand-written substitute
PASS v3: no placeholder survived the fill
PASS v3: README documents the 3.0 release
PASS v3: API.md documents the MCP surface
PASS v3: no 2.0.0 version string survives outside the tests
PASS v3: no REST handler writes on the bare key guard
PASS nav: every sidebar link is highlighted by the page it opens
PASS nav: no page highlights a sidebar item that does not exist
PASS ip: a direct visitor's forged headers are ignored
PASS ip: behind a trusted proxy the forwarded visitor is used
PASS ip: CF-Connecting-IP wins over the X-Forwarded-For chain
PASS ip: the chain is read right-to-left, past trusted hops
PASS ip: a Cloudflare edge is not a trusted proxy by default
PASS ip: CIDR maths (v4 boundaries + v6, no cross-family match)
== UI kit: batch preview (what this issue run will produce, before it produces it) ==
PASS preview: routed, permission-gated and CSRF-checked like the issue path itself
PASS preview: writes NOTHING — no insert, no issue call, no code minted
PASS preview: renders through the SAME merge the issuer uses (Cert::merge over Cert::vars)
PASS preview: the code it shows cannot be mistaken for a real one
PASS preview: counts recipients who already hold a live certificate from this template
PASS preview/engine: {recipient} merges to the real name (got 'Awarded to Ada Lovelace', want 'Awarded to Ada Lovelace')
PASS preview/engine: an unknown token is left alone rather than blanked (got 'Hello {nope}', want 'Hello {nope}')
PASS preview/engine: tokensUsed reports what a template actually merges
== v3.1: an image ON the certificate ==
PASS seal: a plain 8-bit RGB PNG is carried into the PDF without re-encoding
PASS seal: a JPEG is embedded as DCTDecode with its own dimensions
PASS seal: a transparent PNG is refused, and says why
PASS seal: a palette PNG is refused, and says why
PASS seal: an interlaced PNG is refused, and says why
PASS seal: a PDF or a text file is refused outright
PASS seal: a wide image is scaled to fit its box, never stretched (got array (
0 => 150.0,
1 => 37.5,
), want array (
0 => 150.0,
1 => 37.5,
))
PASS seal: a tall image is bounded by the box height instead (got array (
0 => 15.0,
1 => 60.0,
), want array (
0 => 15.0,
1 => 60.0,
))
PASS seal: a certificate WITHOUT a seal is unchanged (no XObject appears)
PASS seal: a certificate WITH one carries a real image XObject the page references
PASS seal: the sealed PDF still ends with a valid trailer and xref
== v3.1: certificates reach their recipients ==
PASS delivery: batch, single and the recipient link are all routed
PASS delivery: sending needs certificate.issue and a CSRF token
PASS delivery: a batch is bounded and reports what is left
PASS delivery: the progress list carries a first name only — never an address
PASS delivery: the public PDF route is token-only, refuses short tokens and never asks for a login
PASS delivery: one PDF builder serves the download, the email link and the public copy
PASS delivery: OFF by default — an operator who never asked sends nothing (got 'Certificate emails are switched off in Settings.', want 'Certificate emails are switched off in Settings.')
PASS delivery: demo installs never reach a credential (got 'Demo mode — no mail leaves this server.', want 'Demo mode — no mail leaves this server.')
PASS delivery: a revoked certificate is never emailed (got 'This certificate is revoked.', want 'This certificate is revoked.')
PASS delivery: a recipient with no address is a no-send, not an error (got 'This recipient has no email address.', want 'This recipient has no email address.')
PASS delivery: the batch never emails the same certificate twice (got 'Already emailed.', want 'Already emailed.')
PASS delivery: an operator CAN re-send one on purpose (got '', want '')
PASS delivery: the email carries the code and the private link, and no other recipient
PASS scope: api_keys gains a scope column defaulting to full — an upgrade never loosens
PASS scope: requireKey reads the key scope and refuses a write on a read-only key
PASS scope: mintKey persists the chosen scope, and anything but read is full access
PASS scope: /mcp hands the key scope to the core, which refuses write tools per tool
PASS scope: the key form lets an administrator mint a read-only key
PASS licence: every font ships the SIL OFL text for its own family beside it
PASS licence: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause
-- F2b: the demo card, the webhook buttons and events, the JSON backup --
PASS demo card: /verify says what the page is ("this is the public page anyone checks a certificate on"), not "signed in as the admin"
PASS demo card: /verify/ABCD-2345-EFGH says what the page is ("this is the public page anyone checks a certificate on"), not "signed in as the admin"
PASS demo card: /certificates keeps the admin wording
PASS demo card: /generate keeps the admin wording
PASS demo card: /login keeps the admin wording
PASS demo card: below 600px the script starts it as the small "Own it" pill unless the visitor opened it this session
PASS demo card: output without </body> (JSON, CSV, a PDF) passes through untouched
PASS forms: every POST form on a screen reaches a registered route (33 checked)
PASS webhooks: the Settings page answers and lists every event it offers
PASS webhooks: the Add, Pause, Resume and Delete buttons each reach a route and do what they say
PASS webhooks: Delete also removes that webhook's delivery log, as its confirmation says
PASS webhooks: a paused webhook is sent nothing
PASS webhooks: certificate.issued is sent by the action where it happens [302]
PASS webhooks: certificate.revoked is sent by the action where it happens [302]
PASS webhooks: every event the Settings page offers is sent somewhere
PASS webhooks: issuing from the screen announces each certificate, and so does /mcp
PASS webhooks: bulk revoke announces each certificate it revokes, never one already revoked
PASS webhooks: the revocation carries the certificate, marked revoked
PASS backup redaction: a credential was planted in every credential column (10)
PASS backup redaction: /backup.json and the scheduled backup both use the one list, v3_redact()
PASS backup redaction: /backup.json (downloaded by an administrator) carries none of the planted credentials
PASS backup redaction: /backup.json (downloaded by an administrator) shows [REDACTED] in each credential column and leaves none unredacted
PASS backup redaction: the scheduled backup file carries none of the planted credentials
PASS backup redaction: the scheduled backup file shows [REDACTED] in each credential column and leaves none unredacted
PASS backup redaction: the administrator got the download (HTTP 200)
————————————————————————————————————————————————————
ALL 295 TESTS PASSED (0 failed)
← Back to Certora · Manual · Quickstart · API