Home›Certora›Test run
Certora · Test run · run 2026-11-07

Certora Test run, as shipped in the download

423 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-11-07 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 neutralized (got '\'=SUM(A1)', want '\'=SUM(A1)')
  PASS  +1 neutralized (got '\'+1', want '\'+1')
  PASS  -CMD neutralized (got '\'-2+3+cmd', want '\'-2+3+cmd')
  PASS  @cmd neutralized (got '\'@cmd', want '\'@cmd')
  PASS  leading TAB neutralized (got '\'	x', want '\'	x')
  PASS  cmd payload neutralized (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 organization, 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 organization, the app, the role, the link and the 7-day expiry
  PASS  a line break in the organization 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 (v4.1.3)
  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: anonymize 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  license: every font ships the SIL OFL text for its own family beside it
  PASS  license: 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 (42 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 (14)
  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)
  PASS  security page: the backup card says the JSON backup leaves out webhook URLs too (ruling 1, 2026-09-25)
  PASS  security page: the backup card says every credential is redacted, not only the SMTP password
  PASS  3.2: linkedin_link_carries_the_certificate
  PASS  3.2: linkedin_uses_the_company_id_when_set
  PASS  3.2: linkedin_press_is_counted
  PASS  3.2: linkedin_not_offered_for_revoked_or_expired
  PASS  3.2: verify_page_offers_linkedin_for_valid_only
  PASS  3.2: page_lookup_counts_the_certificate
  PASS  3.2: public_api_lookup_counts_too
  PASS  3.2: api_certificate_carries_the_counts
  PASS  3.2: a_miss_is_one_row_counted
  PASS  3.2: a_near_miss_is_flagged_a_far_one_is_not
  PASS  3.2: report_totals_add_up
  PASS  3.2: misses_are_kept_90_days
  PASS  3.2: verifications_page_renders_for_the_team
  PASS  3.2: badges_are_off_until_switched_on
  PASS  3.2: badges_need_a_contact_email
  PASS  3.2: sharing_settings_saved_clean
  PASS  3.2: assertion_follows_open_badges_2
  PASS  3.2: a_revoked_badge_answers_410
  PASS  3.2: badge_class_and_issuer_carry_what_the_spec_asks
  PASS  3.2: a_deleted_template_keeps_its_badge
  PASS  3.2: no_email_no_badge
  PASS  3.2: assertion_is_served_as_json
  PASS  3.2: badge_image_is_escaped_well_formed_svg
  PASS  3.2: verify_page_shows_the_badge
  PASS  3.2: link_resolves_its_email_only
  PASS  3.2: page_lists_their_certificates_only
  PASS  3.2: an_expired_link_stops
  PASS  3.2: asking_answers_the_same_either_way
  PASS  3.2: a_link_only_for_a_holder
  PASS  3.2: the_link_is_emailed_through_your_smtp
  PASS  3.2: one_email_per_address_per_ten_minutes
  PASS  3.2: asking_is_audited_without_the_address
  PASS  3.2: the_team_can_make_the_link
  PASS  3.2: email_offers_linkedin_and_their_page
  PASS  3.2: erasure_clears_the_certificate_copy_and_links
  PASS  3.2: link_hashes_are_redacted_from_backups
  PASS  3.3 several templates: main_first_then_extras
  PASS  3.3 several templates: no_template_twice
  PASS  3.3 several templates: an_inactive_extra_is_left_out
  PASS  3.3 several templates: extras_need_a_main_template
  PASS  3.3 several templates: a_run_stays_at_2000_certificates
  PASS  3.3 several templates: form_offers_the_other_templates
  PASS  3.3 several templates: preview_counts_people_times_templates
  PASS  3.3 several templates: preview_counts_existing_per_template
  PASS  3.3 several templates: each_person_gets_one_per_template
  PASS  3.3 several templates: each_certificate_has_its_own_code
  PASS  3.3 several templates: each_is_merged_from_its_own_template
  PASS  3.3 several templates: the_flash_says_two_templates_each
  PASS  3.3 several templates: one_template_issues_as_before
  PASS  3.3 several templates: the_batch_is_audited_per_template
  PASS  api roles: POST /api/certificates refuses every role without the permission (403) and admits the rest

————————————————————————————————————————————————————
  PASS  3.3 import dry run: the_upload_reaches_the_dry_run
  PASS  3.3 import dry run: the_dry_run_writes_nothing
  PASS  3.3 import dry run: the_dry_run_counts_only_what_will_import
  PASS  3.3 import dry run: the_dry_run_names_why
  PASS  3.3 import dry run: the_import_brings_in_exactly_what_the_dry_run_said

== 4.0: the Update button is wired ==
  PASS  updates: the release public key is pinned
  PASS  updates: Database::migrate() creates the updater table (in code, not in a comment)
  SKIP  updates: the state row (this suite keeps no open database in its own process; update_e2e.sh installs, migrates and updates for real)
  PASS  updates: index.php clears stale compiled code before anything else loads
  PASS  updates: index.php finishes or rolls back an update before any controller loads, with this app's canary
  PASS  updates: route get /updates is updates_page()
  PASS  updates: route post /updates/key is updates_key_save()
  PASS  updates: route post /updates/check is updates_check()
  PASS  updates: route post /updates/apply is updates_apply()
  PASS  updates: updates_page() opens with the restore page's admin gate
  PASS  updates: updates_key_save() opens with the restore page's admin gate
  PASS  updates: updates_check() opens with the restore page's admin gate
  PASS  updates: updates_apply() opens with the restore page's admin gate
  PASS  updates: every POST checks the CSRF token
  PASS  updates: the page is told why updates are off here (Updater::whyOff, F5 A1 pass)
  PASS  updates: the menu links to the page
  PASS  updates: bin/update.php refuses the web and finishes in a fresh process
  PASS  updates: where updates are off (Cloud, the demo, a missing extension) the page says why and offers no button
  PASS  updates: with a key and a newer signed release the page offers "Back up and update"

== 4.0 A2: sample data, backups, health ==
  PASS  A2: the owner's-install checks ran to the end
  PASS  A2: an owner's own install (no Cloud config, no demo marker)
  PASS  A2: a fresh install's sample tables are all empty, so the installer's sample data can go in
  PASS  A2: switched off: an install without "Start with sample data" gets none
  PASS  A2: first request: the marker is used once and the sample business is there
  PASS  A2: every one of the owner's settings is theirs, not the sample's
  PASS  A2: the sample logins cannot sign in: no password matches, and switched off where the product can
  PASS  A2: the owner's own login is untouched
  PASS  A2: the load is in the audit log
  PASS  A2: a second marker on a Certora with records seeds nothing and changes nothing
  PASS  A2: real use: one of the owner's own rows among the sample
  PASS  A2: remove: the owner's own row stays, and a sample row it points at is kept
  PASS  A2: remove: the report says what went, and what was kept because the owner's records use it
  PASS  A2: remove: the rest of the sample is gone
  PASS  A2: remove: every kept sample row keeps all its own parts
  PASS  A2: remove: no row is left pointing at a row that was removed
  PASS  A2: remove: the sample logins are gone but the ones kept rows use, the owner stays, and nothing counts as sample any more
  PASS  A2: remove: the sample's files are gone from disk (0 written; one a row that stays names stays with it)
  PASS  A2: remove: the owner's own uploaded file is untouched
  PASS  A2: "Load demo data" on a Certora with records refuses and deletes nothing
  PASS  A2: backup: written, sent to the bucket (a signed PUT), and read back
  PASS  A2: backup: the bucket secret is not in the backup it protects
  PASS  A2: backup: under the standard keep count (14) every backup is kept
  PASS  A2: switched off: with automatic backups off, a backup is never due however old the last one
  PASS  A2: the backup lines for Backup & restore: last backup fine, offsite fine, read-back fine
  PASS  A2: an offsite endpoint must be https for an owner (plain http only in tests)
  PASS  A2: all 25 owner's-install checks reported
  PASS  A2: Database::migrate() creates the sample-data and backup tables (in code, not in a comment)
  PASS  A2: index.php loads the three cores and the wiring
  PASS  A2: the first request seeds the installer's sample, and an owner's install backs itself up when due
  PASS  A2: route post /sample-data/remove is sample_data_remove()
  PASS  A2: route post /restore/backups is backups_save()
  PASS  A2: route post /restore/backup-now is backup_now()
  PASS  A2: route get /health is health_page()
  PASS  A2: sample_data_remove() opens with the restore page's admin gate
  PASS  A2: backups_save() opens with the restore page's admin gate
  PASS  A2: backup_now() opens with the restore page's admin gate
  PASS  A2: health_page() opens with the restore page's admin gate
  PASS  A2: every POST checks the CSRF token
  PASS  A2: off an owner's install (Cloud, the demo) the backup settings and "Back up now" refuse in words
  PASS  A2: the installer offers sample data, ticked, and only leaves the first-run marker (with every table's row count)
  PASS  A2: the first run is strictly once (a marker that cannot be removed seeds nothing), and treats the installer's rows as its defaults only while nothing has changed
  PASS  A2: the scheduled-backup URL runs the same backup (offsite copy and read-back included)
  PASS  A2: "Load demo data" outside the public demo goes through SampleData (only into an empty Certora, removable)
  PASS  A2: the menu links to Health
  PASS  A2: the sample-data banner shows only to an admin, only while sample data is there, with its Remove button, and stays put (nested: the UI kit toasts a bare .main > .alert away)
  PASS  A2: Backup & restore shows the automatic backups, "Back up now" and the bucket settings (none on Cloud)
  SKIP  A2: the Health page render (this suite loads no view helpers in its own process; the owner's-install check and update_e2e.sh run the real app)
  PASS  erase planting · sqlite: the subject is created and a marker is planted in all 7 personal columns
  PASS  erase planting · sqlite: the app's own erase runs through its route and reports no failure
  PASS  erase planting · sqlite: no planted marker survives anywhere (21 tables dumped), outside the 1 column(s) kept by ruling (possible legal records)
  PASS  erase planting · sqlite: the erase page (/gdpr) says what an erase keeps: "Kept: the audit trail's line that this recipient was added, …"
  PASS  erase planting · sqlite: MANUAL.md says the same: "Kept: the audit trail's line that this recipient was added, …"
ALL 423 TESTS PASSED (0 failed)

← Back to Certora · Manual · Quickstart · API