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

Warrantora Test run, as shipped in the download

142 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. addMonths — the edge PHP's own date maths gets wrong ==
  PASS  THE ONE THAT MATTERS: 31 Jan + 1 month is 28 Feb, not 3 March (got '2026-02-28', want '2026-02-28')
  PASS  ...and in a leap year it is 29 Feb (got '2028-02-29', want '2028-02-29')
  PASS  31 Aug + 6 months = 28 Feb (a customer does not get three free days) (got '2027-02-28', want '2027-02-28')
  PASS  31 May + 1 = 30 Jun (got '2026-06-30', want '2026-06-30')
  PASS  29 Feb + 12 months lands on 28 Feb (got '2029-02-28', want '2029-02-28')
  PASS  29 Feb + 48 months lands on 29 Feb again (got '2032-02-29', want '2032-02-29')
  PASS  a day that exists in both months is untouched (got '2028-03-15', want '2028-03-15')
  PASS  crossing the year boundary (got '2027-01-31', want '2027-01-31')
  PASS  a five-year term (got '2031-06-10', want '2031-06-10')
  PASS  zero months is the same day (got '2026-06-10', want '2026-06-10')
  PASS  a nonsense date gives nothing back, never a guess (got '', want '')
  PASS  an impossible date is rejected too (got '', want '')
  PASS  daysBetween counts forward (got 30, want 30)
  PASS  ...and backward, signed (got -30, want -30)
  PASS  ...across a leap day (got 2, want 2)

== 2. Expiry is COMPUTED — and says so when it cannot be ==
  PASS  the warranty term is used (got 12, want 12)
  PASS  an item override beats the term (got 30, want 30)
  PASS  THE SUBTLE ONE: an override of 0 means NO COVER, not "unset" (got 0, want 0)
  PASS  an empty-string override is "unset", and falls through (got 12, want 12)
  PASS  no term and no override falls back (got 18, want 18)
  PASS  expiry = sold + term (got '2027-01-15', want '2027-01-15')
  PASS  a five-year term (got '2031-01-15', want '2031-01-15')
  PASS  a term of zero expires the day it was sold (got '2026-01-15', want '2026-01-15')
  PASS  NO SALE DATE means no expiry — never a guess (got '', want '')
  PASS  ...and a malformed one is the same (got '', want '')

== 3. Status and the 30/60/90 bands ==
  PASS  comfortably covered (got 'active', want 'active')
  PASS  inside the soon window (got 'expiring', want 'expiring')
  PASS  the window is the OPERATOR's, not a constant: 90 days widens it (got 'expiring', want 'expiring')
  PASS  ...and the same item is merely active at 30 (got 'active', want 'active')
  PASS  past its date (got 'expired', want 'expired')
  PASS  expiring EXACTLY today still counts as covered (got 'expiring', want 'expiring')
  PASS  the day after is expired (got 'expired', want 'expired')
  PASS  a voided registration is void whatever the dates say (got 'void', want 'void')
  PASS  no sale date is UNKNOWN, which is not the same as expired (got 'unknown', want 'unknown')
  PASS  days left (got 283, want 283)
  PASS  negative once expired (got -82, want -82)
  PASS  null when there is nothing to count to (got NULL, want NULL)
  PASS  band 30 (got '30', want '30')
  PASS  band 60 (got '60', want '60')
  PASS  band 90 (got '90', want '90')
  PASS  beyond 90 is no band at all (got '', want '')
  PASS  already expired is no band (got '', want '')
  PASS  unknown is no band (got '', want '')
  PASS  the boundary belongs to the tighter band (got '30', want '30')
  PASS  ...and 31 to the next (got '60', want '60')

== 4. Claims — the refusals ARE the product ==
  PASS  raised can be assessed or rejected outright
  PASS  REFUSAL: raised cannot jump to resolved
  PASS  REFUSAL: raised cannot jump to approved — something has to be assessed first
  PASS  assessed can go either way
  PASS  both decisions settle into resolved
  PASS  REFUSAL: resolved is terminal
  PASS  a legal move with a reason is allowed (got '', want '')
  PASS  THE RULE THIS PRODUCT EXISTS FOR: no reason, no assessment
  PASS  ...and no reason, no approval
  PASS  ...and no reason, no rejection
  PASS  resolving does not demand one — the decision above already carries it (got '', want '')
  PASS  a resolved claim explains itself
  PASS  an unknown status is refused
  PASS  moving to where it already is says so
  PASS  a claim nobody has touched is unassessed
  PASS  ...and one that has been is not

== 5. Coverage is decided on the day it was raised, and frozen ==
  PASS  raised inside the term is covered
  PASS  raised on the LAST day is still covered
  PASS  raised the day after is not
  PASS  THE POINT: a claim raised in time stays in time however long the assessment takes — the answer depends on the RAISED date and nothing else
  PASS  no sale date means it cannot be claimed as covered

== 6. Returns ==
  PASS  issued can go in transit (got '', want '')
  PASS  issued can be received directly (a customer who walks in) (got '', want '')
  PASS  REFUSAL: a received parcel cannot go back to in transit
  PASS  REFUSAL: a closed return is terminal
  PASS  moving to the state it is already in says so

== 7. One rule per door, and the scope at the door ==
  PASS  claim_advance refuses through Warranty::advanceRefusal(), not its own matrix
  PASS  api_claim_advance refuses through Warranty::advanceRefusal(), not its own matrix
  PASS  the MCP tool does too
  PASS  DECIDING needs its own permission on every door
  PASS  in-warranty is frozen from the RAISED date on every door
  PASS  expiry is never STORED — the items table has no expiry column and nothing writes one
  PASS  every screen decorates through the ONE function, so no two disagree

== 8. MCP tools + key scope ==
  PASS  EVERY MCP tool declares readOnly — `perm` is a role gate and is NOT the scope annotation
  PASS  the read tools outnumber the write tools, and every write tool has a role gate
  PASS  /mcp hands the key scope to the shared core
  PASS  no tool can void a registration or delete anything — those stay human clicks
  PASS  api_keys.scope exists and DEFAULTS to full
  PASS  a read key is refused any non-GET AT THE DOOR

== 9. Evidence, exports and platform truths ==
  PASS  a PNG is recognised by its bytes (got 'image/png', want 'image/png')
  PASS  a JPEG is (got 'image/jpeg', want 'image/jpeg')
  PASS  a PDF is (got 'application/pdf', want 'application/pdf')
  PASS  a WEBP is (got 'image/webp', want 'image/webp')
  PASS  REFUSAL: an SVG named .png is not an image here (got NULL, want NULL)
  PASS  REFUSAL: a text file is not (got NULL, want NULL)
  PASS  the upload cap is 10 MB and an empty file is refused
  PASS  the served type is decided by the bytes on the way OUT too
  PASS  a CSV cell starting with = is defused (got '\'=cmd|calc', want '\'=cmd|calc')
  PASS  ...and a negative number is NOT (it is data) (got '-12.50', want '-12.50')
  PASS  ...and ordinary text is untouched (got 'Meridian 600', want 'Meridian 600')
  PASS  ZERO external hosts on any page
  PASS  every routed handler exists
  PASS  the certificate .pdf route is registered BEFORE /items/{id}, which would swallow it
  PASS  no empty-state asks the kit for an icon it does not draw
  PASS  no standing copy sits in a bare .main > .alert (the kit toasts those away)
  PASS  the demo seeder is offline and deterministic

== 10. The rules hold against a real database, in a subprocess ==
  PASS  expiry is derived from the row, not read from a column
  PASS  THE POINT: editing a term re-dates every item on it
  PASS  ...and no expiry column exists to go stale
  PASS  the MCP endpoint offers its tools
  PASS  MCP: an agent can register a sold item
  PASS  MCP: 31 March + 12 months is 31 March, not 3 April
  PASS  MCP: 31 February is refused, not rolled forward
  PASS  MCP: a claim can be raised
  PASS  ...and the in-warranty answer is frozen onto it
  PASS  MCP: a decision with no reason is REFUSED
  PASS  ...and accepted with one
  PASS  a member key cannot approve or reject
  PASS  ...and the claim did not move when it tried
  PASS  an admin key can
  PASS  ...and the reason is on the record
  PASS  an out-of-warranty claim is accepted and flagged, never blocked
  PASS  the read-only expiring report answers
  PASS  every mutation left an audit row
  PASS  every .kv uses the kit's own dt/dd markup, not hand-rolled div/span rows
  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

== F2: fixes ==
  PASS  F2 security: backup_json is administrators only (a viewer could download the whole register)
  PASS  F2 security: backup_sqlite is administrators only (a viewer could download the whole register)
  PASS  F2 PHP: the installer refuses PHP older than 8.1 with a plain message, before anything else runs
  PASS  F2 PHP: the requirements no longer say PHP 8.0 or "PHP 8+"
  PASS  F2 security: a signed-in non-admin (viewer, member) gets 403 on every backup and restore route
  PASS  F2 security: the probe is real — an administrator is let through to the backup download
  PASS  F2 routes: every POST form in the views reaches a POST route (36 checked)
  PASS  F2 webhooks: every event offered in Settings is one the code actually sends, and vice versa
  PASS  F2 webhooks: a receiver added from Settings with every box ticked hears a real claim move
  PASS  F2 webhooks: an agent raising a claim over MCP reaches the receiver too (agent writes fired nothing)
  PASS  F2 webhooks: a receiver saved with only the old phantom events is widened to every event; others untouched
  PASS  F2b ribbon: Warrantora has no public page, so its pages keep the admin wording (/, /claims)
  PASS  F2b ribbon: below 600px it starts as the small "Own it" pill; a tap opens it and that choice holds for the session
  PASS  F2b ribbon: injected once, just before </body>; output with no </body> (JSON, CSV, PDF) passes untouched
  PASS  F2b ribbon: boot() installs it only behind the .demo-mode marker
  PASS  F2b security: /backup/scheduled refuses a signed-in viewer, member with no token or a wrong one (403)
  PASS  F2b backup: a value was planted and read back in every credential column (settings.oidc_client_secret, settings.backup_token_hash, settings.feed_token_hash, users.password_hash, users.totp_secret, users.totp_recovery, api_keys.token_hash, webhooks.secret, invites.token_hash)
  PASS  F2b backup: the JSON download carries none of them
  PASS  F2b backup: the scheduled backup carries none of them
  PASS  F2b backup: both backup doors use the one list, _as_backup_redact()

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

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