Ownware
Home›Certora›Test run
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

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →