Ownware
Home›Waiverly›Test run
Waiverly · Test run · run 2026-09-25

Waiverly Test run, as shipped in the download

238 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.

[DB: SQLite]

== 1. Canonical text + content hashing ==
  PASS  CRLF and LF bodies hash identically (got 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0', want 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0')
  PASS  leading/trailing whitespace is canonicalized away (got 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0', want 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0')
  PASS  changing one word changes the hash
  PASS  changing the title changes the hash
  PASS  adding an acknowledgment changes the hash
  PASS  hash is 64 hex chars

== 2. Template versioning (immutable snapshots) ==
  PASS  creating a template publishes version 1
  PASS  metadata-only edit does NOT bump the version
  PASS  metadata edit still saved (minor_age) (got 16, want 16)
  PASS  current version unchanged (got 1, want 1)
  PASS  text edit publishes version 2
  PASS  version 1 snapshot is untouched (immutable) (got 'Original text.', want 'Original text.')
  PASS  version 1 hash still verifies against its own text (got '1898463c2e568c6836bc855d81618633c4a9d351631b6d5e1cbc8dd83f6ba523', want '1898463c2e568c6836bc855d81618633c4a9d351631b6d5e1cbc8dd83f6ba523')

== 3. Age math (minor gate) ==
  PASS  day before 18th birthday -> 17 (got 17, want 17)
  PASS  on the 18th birthday -> 18 (got 18, want 18)
  PASS  day after -> still 18 (got 18, want 18)
  PASS  Feb-29 dob, non-leap Feb 28 -> not yet 18 (got 17, want 17)
  PASS  Feb-29 dob, non-leap Mar 1 -> 18 (got 18, want 18)
  PASS  Feb-29 dob, leap-year Feb 29 -> exactly N (got 20, want 20)
  PASS  born today -> 0 (got 0, want 0)

== 4. Expiry month math (month-end clamping) ==
  PASS  Jan 31 + 1mo -> Feb 28 (non-leap) (got '2026-02-28', want '2026-02-28')
  PASS  Jan 31 + 1mo -> Feb 29 (leap) (got '2024-02-29', want '2024-02-29')
  PASS  Aug 31 + 1mo -> Sep 30 (got '2026-09-30', want '2026-09-30')
  PASS  +12mo -> same date next year (got '2027-07-04', want '2027-07-04')
  PASS  Dec 15 + 2mo rolls the year (got '2027-02-15', want '2027-02-15')
  PASS  Jan 31 + 13mo -> Feb 28 next year (got '2027-02-28', want '2027-02-28')
  PASS  +24mo from Feb 29 clamps (got '2026-02-28', want '2026-02-28')

== 5. Signing: validation + record creation ==
  PASS  adult signs successfully
  PASS  verify code is 10 chars (got 10, want 10)
  PASS  code avoids 0/O/1/I lookalikes
  PASS  adult is not flagged minor (got 0, want 0)
  PASS  expiry = signed date + 6 months (template setting) (got '2027-03-25', want '2027-03-25')
  PASS  record binds to the CURRENT version (v2) (got 2, want 2)
  PASS  fresh record passes integrity
  PASS  status of a fresh record (got 'valid', want 'valid')
  PASS  rejects empty name — “Enter your full name.”
  PASS  rejects bad email — “That email address does not look right.”
  PASS  rejects missing DOB — “Enter your date of birth.”
  PASS  rejects impossible DOB — “Enter your date of birth.”
  PASS  C2R: refuses an array where signer_name belongs — “Please fill in each field with a single value.”
  PASS  C2R: refuses an array where signer_email belongs — “Please fill in each field with a single value.”
  PASS  C2R: refuses an array where signer_dob belongs — “Please fill in each field with a single value.”
  PASS  C2R: refuses an array where the signature belongs — “Please fill in each field with a single value.”
  PASS  C2R: refuses an array where guardian_name belongs — “Please fill in each field with a single value.”
  PASS  C2R: not one of those refusals stored a waiver named "Array"
  PASS  C2R: `acks` is still allowed to be a list (the one legitimate array field)
  PASS  rejects future DOB — “Check the date of birth.”
  PASS  rejects unticked acknowledgment — “Please tick every acknowledgment box.”
  PASS  rejects missing signature — “Please sign in the signature box.”
  PASS  rejects non-PNG payload — “Please sign in the signature box.”
  PASS  oversize-fixture really exceeds the cap
  PASS  rejects oversize signature — “Please sign in the signature box.”
  PASS  rejects inactive template — “This waiver is not accepting signatures.”
  PASS  minor WITHOUT guardian is rejected — “A parent or guardian must complete this waiver for a minor.”
  PASS  minor WITH guardian signs (flagged minor) (got 1, want 1)
  PASS  guardian name stored (got 'Casey Guardian', want 'Casey Guardian')
  PASS  guardian signature stored
  PASS  kiosk source recorded (got 'kiosk', want 'kiosk')
  PASS  adult submission blanks stray guardian fields (got '', want '')

== 6. Derived status: valid / expired / superseded ==
  PASS  valid while before expiry (got 'valid', want 'valid')
  PASS  expired the day AFTER expires_on (got 'expired', want 'expired')
  PASS  still valid ON expires_on itself (got 'valid', want 'valid')
  PASS  never-expires ('' expiry) stays valid decades on (got 'valid', want 'valid')
  PASS  old version + require_resign -> superseded (got 'superseded', want 'superseded')
  PASS  old version + require_resign OFF -> stays valid (got 'valid', want 'valid')
  PASS  superseded beats expired (precedence) (got 'superseded', want 'superseded')
  PASS  publishing v3 supersedes the v2 record
  PASS  superseded record still shows the exact v2 text it signed
  PASS  superseded record still passes integrity

== 7. Tamper evidence ==
  PASS  editing the signer name in the DB flags TAMPERED
  PASS  restoring the original value verifies again
  PASS  swapping the signature image flags TAMPERED
  PASS  original signature verifies again

== 8. Verify-code lookup ==
  PASS  byCode finds the record
  PASS  byCode is case/format tolerant (lowercase + dashes)
  PASS  unknown code -> null
  PASS  all codes unique (got 4, want 4)

== 9. CSV export formula-injection guard ==
  PASS  neutralizes =formula signer name (got '\'=cmd|\'/c calc\'!A1', want '\'=cmd|\'/c calc\'!A1')
  PASS  neutralizes +formula (got '\'+SUM(A1)', want '\'+SUM(A1)')
  PASS  neutralizes @formula (got '\'@A1', want '\'@A1')
  PASS  neutralizes leading-minus text (not numeric) (got '\'-1+1@cmd', want '\'-1+1@cmd')
  PASS  neutralizes leading TAB (got '\'	=1', want '\'	=1')
  PASS  leaves plain negative number untouched (got '-45', want '-45')
  PASS  leaves plain decimal untouched (got '-45.00', want '-45.00')
  PASS  leaves ordinary name untouched (got 'Alex Sample', want 'Alex Sample')
  PASS  leaves date string untouched (got '2026-07-04', want '2026-07-04')
  PASS  leaves empty untouched (got '', want '')

== 10. Regression: validity window covered by record hash + input length caps ==
  PASS  extending expires_on in the DB flags TAMPERED
  PASS  restoring expires_on verifies again
  PASS  backdating signed_on in the DB flags TAMPERED
  PASS  restoring signed_on verifies again
  PASS  rejects over-long phone (would 500 on MySQL strict) — “That phone number is too long.”
  PASS  rejects over-long email — “That email address does not look right.”
  PASS  rejects over-long guardian relation — “The guardian relationship is too long.”
  PASS  normal-length fields still accepted after caps

== 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: backup_tables
  PASS  v2: no_signature_image
  PASS  v2: no_signer_ip
  PASS  v2: has_record_hash
  PASS  v2: routes registered
  PASS  v2: NO signing endpoint (a waiver must be signed by a person, in a browser)
  PASS  v2: integrity check reuses the product core (Waiver::integrityOk)
  PASS  v2: signing fires waiver.signed + notifies, never fatally
  PASS  v2: 2FA intercepts login + rate limit
  PASS  v2: zero clip() dependency
  PASS  v2: no ternary key re-reads (PLAN item 10)
  PASS  v2: dark palette; signature pad pinned light in BOTH themes
  PASS  v2: theme.js on the interactive layouts (print.php deliberately excluded)
  PASS  v3: API.md ships + sw cache and healthz/ping/openapi all report the CURRENT version (3.1.5)
  PASS  v2: cli-server base guard present

-- Own It 3.0 --
  PASS  v3: v3_tables
  PASS  v3: v3_columns
  PASS  v3: upgrade_never_loosens
  PASS  v3: settings_singleton_ensured
  PASS  v3: upgrade_from_2x
  PASS  v3: migrate_called_at_boot
  PASS  v3: roles
  PASS  v3: last_admin_guard
  PASS  v3: waiver_signed
  PASS  v3: signed_text_is_pinned
  PASS  v3: becomes_superseded
  PASS  v3: integrity_still_ok
  PASS  v3: mcp_tools
  PASS  v3: mcp_is_read_only
  PASS  v3: mcp_arg_guard
  PASS  v3: mcp_returns_signed_snapshot
  PASS  v3: mcp_never_returns_signature
  PASS  v3: mcp_get_is_405
  PASS  v3: expiry_data_shared
  PASS  v3: pdf_renders_pinned_text
  PASS  v3: gdpr_blocked_while_valid
  PASS  v3: gdpr_allowed_once_expired
  PASS  v3: gdpr_keeps_the_evidence_skeleton
  PASS  v3: ical_token
  PASS  v3: ical_feed_shape
  PASS  v3: branding_content_decides
  PASS  v3: restore_refuses_other_product
  PASS  v3: audit_redacts
  PASS  v3: routes registered, and LITERALS come before {id} patterns
  PASS  v3: the PDF renders the PINNED snapshot, never the current template
  PASS  v3: GDPR refuses to erase a waiver that can still be relied on
  PASS  v3: the signature image is never exposed by the API or the audit trail
  PASS  v3: expiry is computed in ONE place and reused by the list, the feed and the stats tool
  PASS  v3: re-signing never edits the original waiver
  PASS  v3: core Mcp is byte-identical to the shared core
  PASS  v3: core AuditLog is byte-identical to the shared core
  PASS  v3: core Pdf is byte-identical to the shared core
  PASS  v3: core Ical is byte-identical to the shared core
  PASS  v3: core BackupRestore is byte-identical to the shared core
  PASS  v3: core Branding is byte-identical to the shared core
  PASS  v3: core RolesKit is byte-identical to the shared core
  PASS  v3: core Gdpr is byte-identical to the shared core
  PASS  v3: core Oidc is byte-identical to the shared core
  PASS  v3/C2: the viewer seat is READ-ONLY in the map — it holds nothing that changes the record
  PASS  v3/C2: the legal text and the register are role-gated, not merely signed-in
  PASS  v3/C2: the backup routes hand over the whole register — settings.edit, not requireLogin
  PASS  v3/C2: no route that predates 3.0 is left on a bare requireLogin except the user's own account
  PASS  v3/C2: the sidebar hides Settings from a role the route refuses
  PASS  v3/C2: /backup.sqlite is an app ROUTE and the dev router does not 403 it
  PASS  v3/C2: bin/demo.php can actually run — migrate() calls AuditLog::ensure()
  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: returning-signer check (kiosk hero endpoint) ==
  PASS  returning: exact email+DOB finds the record, case-insensitively
  PASS  returning: reports the engine-computed expiry (got '2027-09-25', want '2027-09-25')
  PASS  returning: answer carries NO name and NO code (nothing to enumerate)
  PASS  returning: right email + wrong DOB stays silent
  PASS  returning: unknown email stays silent
  PASS  returning: malformed email / DOB are refused before the query
  PASS  returning: past the expiry date the engine says expired
  PASS  returning: a text bump flips the answer to superseded (must re-sign)
  PASS  returning: /w/{token}/check + /k/{token}/check routed, CSRF-checked, minimal JSON

== v3.1: the signer gets their own copy ==
  PASS  signer copy: carries the title, the dates and the reference code
  PASS  signer copy: links the PUBLIC verify page, code included
  PASS  signer copy: carries NO signature image, record hash, IP or internal note
  PASS  signer copy: subject names the waiver and the business
  PASS  signer copy: no base URL configured → the email simply omits the link
  PASS  signer copy: OFF by default — an operator who never asked sends nothing (got 'Signer copies are switched off in Settings.', want 'Signer copies are switched off in Settings.')
  PASS  signer copy: demo installs never reach a credential (got 'Demo mode — no mail leaves this server.', want 'Demo mode — no mail leaves this server.')
  PASS  signer copy: unconfigured SMTP refuses in plain words (got 'SMTP is not configured.', want 'SMTP is not configured.')
  PASS  signer copy: a signer who gave no address is not an error, just a no-send (got 'This signer did not give an email address.', want 'This signer did not give an email address.')
  PASS  signer copy: the automatic send does not repeat itself (got 'A copy was already sent for this waiver.', want 'A copy was already sent for this waiver.')
  PASS  signer copy: an operator CAN send it again on purpose (got '', want '')
  PASS  signer copy: the manual route is permission-gated and CSRF-checked
  PASS  signer copy: signing calls it, and a mail failure cannot break the signature
  PASS  signer copy: the SMTP password is write-only — a blank post keeps the stored secret
  PASS  signer copy: a refused SMTP connection returns a reason instead of a fatal error
  PASS  signer copy: the failure is written on the row, not marked as sent
  PASS  signer copy: the failed send leaves the signed record untouched
  PASS  API.md: names the real signing paths (/w/{token}, /k/{token}), not a /sign route that 404s

== F2: walk-in kit — QR poster per template, prefilled share links ==
  PASS  poster: GET /templates/{id}/poster is routed to template_poster, behind template.view
  PASS  poster: the QR carries the Settings public address when one is set (got 'https://waivers.example.com/w/846e092faa0109b1ba3e828e12b6eb87', want 'https://waivers.example.com/w/846e092faa0109b1ba3e828e12b6eb87')
  PASS  poster: renders the share link as a QR (qr.js) plus the address in print
  PASS  poster: the kiosk token is never on it (the kiosk stays on the desk tablet)
  PASS  poster: every template row links to its poster
  PASS  prefill: ?name=&email= on the share link fills both fields, escaped
  PASS  prefill: the kiosk ignores it (the next person must not inherit a name)
  PASS  signer copy: the signing route itself also catches anything the copy throws, so the owner notice still sends
  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
  PASS  forms: every POST form on a screen reaches a registered route (26 checked)
  PASS  demo ribbon: on the public pages it says what the page is, not "signed in as the admin"
  PASS  demo ribbon: below 600px it starts as the small pill unless the visitor opened it
  PASS  backups: every non-admin role gets 403 on every backup, restore and backup-token route
  PASS  backups: an administrator still gets the JSON backup (got 'GATE 200', want 'GATE 200')
  PASS  backup JSON: an administrator gets a backup, with every credential column planted (10)
  PASS  backup JSON: the download carries none of them
  PASS  backup JSON: the scheduled backup carries none of them
  PASS  backup JSON: the download and the scheduled writer read the one redaction list
  PASS  security page: the backup card says every credential is redacted, not only the SMTP password

————————————————————————————————————————————————————
ALL 238 TESTS PASSED (0 failed)

← Back to Waiverly · 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 →