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