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

Warrantora Test run, as shipped in the download

347 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. 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 recognized 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  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

== 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 (60 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, settings.smtp_pass, settings.sms_api_key, users.password_hash, users.totp_secret, users.totp_recovery, claims.public_token, api_keys.token_hash, webhooks.secret, updater_state.license_api_key, backup_state.s3_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()

== 1.1 · customer email: what a customer reads, and when ==
  PASS  mail: "we have your claim" names the claim, the product and serial, and carries the claim's own link
  PASS  mail: a decision carries the decision, the outcome and the written REASON, and never the internal assessment
  PASS  mail: "your cover ends" states the date in words
  PASS  mail: every customer email is OFF on a fresh install (master switch and each kind)
  PASS  mail: a kind ticked without the master switch still sends nothing
  PASS  mail: a claim raised → the customer's "we have your claim" lands (demo: in the outbox, no socket) with a fresh 32-hex claim link
  PASS  mail: the decision goes once per decision, with the reason (a second call records nothing)
  PASS  mail: a customer with no address on file is not written to
  PASS  mail: "cover ending" goes to the one item inside its window — not a later one, an ended one, a voided one or one with no address
  PASS  mail: run again, nothing more — one note per item per expiry date (the reference is claimed first)
  PASS  mail: switched off, the cover-ending run sends nothing

== 1.1 · supplier claim-back ==
  PASS  recovery: approved, in-warranty repairs, replacements and refunds wait under their supplier; goodwill, rejected and out-of-warranty do not
  PASS  recovery: a pack takes only that supplier's eligible claims (another supplier's and a rejected one are left out)
  PASS  recovery: a claim sits in one pack at most
  PASS  recovery: no supplier named, no pack
  PASS  recovery: the CSV carries every decision and its reason, and no amount column
  PASS  recovery: marking it sent stamps the sent date
  PASS  recovery: a pack that went to the supplier cannot be taken apart
  PASS  recovery: the supplier's answer, reference and note are recorded, with the date answered
  PASS  recovery: an unknown status is refused
  PASS  recovery: an open pack can be taken apart and its claims wait again
  PASS  backup + audit: the customer's claim link and the SMTP password are on the redaction lists

== 1.1 pass B · faults by product and batch ==
  PASS  faults: claims per 100 units, and none when nothing is registered
  PASS  faults: by product, units registered against claims raised in the period, with each outcome counted
  PASS  faults: by batch, the batch with three claims at twice its product's rate stands out, the clean one does not
  PASS  faults: two claims are not enough to mark a batch, however high its rate
  PASS  faults: with no period, the old claim counts too

== 1.1 pass B · the evidence archive ==
  PASS  archive: every evidence file with its claim, original name and checksum; a missing file and a non-minted name are listed, not read
  PASS  archive: restore puts back the missing files byte for byte, and a second run overwrites nothing
  PASS  archive: a file no evidence row names is not written
  PASS  archive: bytes that do not match the checksum are left out
  PASS  archive: something that is not a Warrantora archive is refused

== 1.1 pass B · registering at the point of sale ==
  PASS  sale: a sale needs its reference, a real date, a valid email and at least one line with a product and a quantity
  PASS  sale: the term is the line's own, else the first rule that matches the product or model, else the default
  PASS  sale: every unit is registered: one per serial, one per quantity, on the term its rule or the default gives, to the customer
  PASS  sale: the same sale sent twice registers nothing twice
  PASS  sale: a term that does not exist refuses that unit rather than registering it on the wrong term

== 1.1 · the doors, through the real front controller ==
  PASS  portal: OFF on a fresh install — /warranty is a 404
  PASS  portal: a member cannot switch it on
  PASS  portal: switched on, the page opens with no login, the owner's line on top, and out of search engines
  PASS  portal: the right serial with the wrong email shows nothing
  PASS  portal: serial and email together find it (case and outer spaces do not matter)
  PASS  portal: the customer sees the product, "Covered" and until when
  PASS  portal: a one-word fault is refused with a plain sentence
  PASS  portal: the claim lands as raised, marked as the customer's, in warranty on the day, with its own link
  PASS  portal: one open claim per item from the page
  PASS  portal: the hidden trap field — a bot is thanked and nothing is kept
  PASS  portal: the claim's own link shows the decision and its reason, and nothing internal
  PASS  portal: a made-up claim link is a 404
  PASS  portal: at most 20 look-ups an hour from one address (a guesser is stopped, even with the right pair)
  PASS  portal: switched off again, the page is a 404
  PASS  decide: rejecting a claim tries to email the customer the reason; a refused send is recorded and never blocks the decision
  PASS  recovery door: a viewer sees the claim-back page
  PASS  recovery door: starting a pack is the owner's act (a member is refused)
  PASS  recovery door: an admin starts a pack
  PASS  recovery door: the pack downloads as CSV (with the reason) and as a PDF
  PASS  item: the supplier is saved from the registration form

————————————————————————————————————————————————————————————
  PASS  api roles: POST /api/items refuses every role without the permission (403) and admits the rest
  PASS  api roles: POST /api/sales refuses every role without the permission (403) and admits the rest
  PASS  api roles: POST /api/claims refuses every role without the permission (403) and admits the rest
  PASS  api roles: POST /api/claims/{id}/advance refuses every role without the permission (403) and admits the rest

== 1.1 pass B · the doors ==
  PASS  door: a viewer reads the faults page and its CSV
  PASS  door: the evidence archive is for administrators, like the backups
  PASS  door: restoring evidence is for administrators
  PASS  door: sale rules are for whoever sets the warranty terms

== 1.2 · your own reports (in-process) ==
  PASS  reports: a definition keeps only known columns (in their fixed order), statuses and sort keys; anything else is dropped
  PASS  reports: a product filter shaped like SQL is only text: it matches nothing and changes nothing
  PASS  reports: items filtered by the status the app works out (expiring), and by product
  PASS  reports: sorted by sale date either way, and by days left as a number
  PASS  reports: items sold in a date range
  PASS  reports: claims filtered by status, in warranty and product, with labels and outcomes
  PASS  reports: the table has the chosen columns, headed by their names
  PASS  reports: saved under a name used once (any case), up to 80 characters, and stored as a clean definition

== 1.2 · the items import's preview runs the import's own checks ==
  PASS  import preview: it counts exactly what the import brings in, names the same skipped rows, and writes nothing
  PASS  import preview: a serial already on the register, one twice in the file, an impossible sale date and a bad email are each named
  PASS  import preview: the upload step runs the import's own handler in normal mode, never the dry-run flag

== 1.2 · the reports doors ==
  PASS  door: a viewer reads a report, and has no save form
  PASS  door: a viewer cannot save or remove a report (403)
  PASS  door: a member saves a report; the same name again is refused
  PASS  door: a saved report opens by its link, and downloads as CSV
  PASS  door: the CSV keeps a formula-looking value as text
  PASS  door: a member removes a saved report

== Claim updates by text, on the owner's own SMS gateway (in-process) ==
  PASS  texts: OFF on a fresh install (no switch, no gateway, hours 9 to 20), and a customer who ticked the box is not texted
  PASS  texts: switched on they need an https gateway AND a business name (every text starts with it)
  PASS  texts: field mapping — JSON to/from/text with a Bearer key by default; Twilio-style To/From/Body form-encoded with Basic; no key header when "not sent"; a blank sender or field name falls back
  PASS  texts: a number is digits with an optional + (00 becomes +); words, or too few or too many digits, are not a number
  PASS  texts: the opt-in decides, not the phone number — a number without the tick, or a tick without a number, is never texted
  PASS  texts: "received" goes at once, to the customer's number, starting with the business name, with the claim's own link where they can stop texts
  PASS  texts: never the same text twice — a second call is refused by the claimed key and the gateway is not called
  PASS  texts: the decision and the close each go once, with the business name and the stop link; assessing texts nobody
  PASS  texts: the sending hours are counted in the business time zone (08:00 out, 09:00 in, 19:59 in, 20:00 out)
  PASS  texts: a decision made at 22:00 waits in the outbox; "received" (the customer's own action) goes at once
  PASS  texts: the waiting text goes on the first run inside the hours (not at 01:00), once, with the text written at the decision
  PASS  texts: a customer who stops texts while one waits never gets it (the opt-in is checked again when it goes)
  PASS  texts: the business day starts at local midnight, daylight saving included (New York: 04:00 UTC in October, 05:00 UTC in November; 1 November 2026 runs 25 hours)
  PASS  texts: a gateway refusal is recorded with the gateway's answer and returned to whoever acted; asking again sends nothing
  PASS  texts: an unreachable gateway (connection error, or no HTTP answer) reads "Gateway unreachable." and breaks nothing
  PASS  texts: a refused text is never tried twice on one day
  PASS  texts: the next day it is tried once more; a second refusal ends that run (the other refused text is left for later)
  PASS  texts: on a later day a working gateway takes both, each once (the first on its third and last attempt)
  PASS  texts: a refused text is left alone after 3 attempts, and once the customer stops texts
  PASS  texts: a waiting text the gateway refuses is marked failed with its answer, and the run stops there (the next one keeps waiting)
  PASS  texts: the daily cap — past it a text waits (even "claim received") and goes the next day
  PASS  texts: every attempt is in the audit trail with the last four digits only, sent or refused
  PASS  texts: a demo install writes the text to the outbox and never calls the gateway, not even for a test text
  PASS  texts: a decision made by an agent through the MCP tool texts the customer too
  PASS  texts: every door that moves a claim on (the screen, the API, the MCP tool) calls the one sender, and the waiting texts ride the dashboard and the cron
  PASS  texts: the gateway key is on the backup and the audit redaction lists

== Claim updates by text: the doors, through the real front controller ==
  PASS  door: the SMS settings are for administrators (a member is refused)
  PASS  door: with texts off, the claim form has no text box
  PASS  door: an http gateway is refused
  PASS  door: hours that end before they start are refused
  PASS  door: switched on with the business still unnamed, texts stay off and the owner is told why
  PASS  door: named, they switch on with the daily cap set; the key is kept from before and never shown back (write-only)
  PASS  door: with texts on, the claim form offers the box, unticked, and says who texts, how often and how to stop
  PASS  door: the box ticked with something that is not a number is refused with a plain sentence, and nothing is kept or sent
  PASS  door: ticked with a number, the claim keeps the opt-in and the number, and one text goes: the business name, the claim, the stop link
  PASS  door: a number typed WITHOUT the tick is not kept and nobody is texted
  PASS  door: the customer's claim page shows the last digits and a Stop button; the team's claim page shows the number and the text sent
  PASS  door: deciding on the claim page texts the customer once, and says so
  PASS  door: the customer stops texts from the claim's page: the tick goes, the number is forgotten, the button goes
  PASS  door: after a stop, closing the claim texts nobody
  PASS  door: a made-up claim link cannot stop anything (404)
  PASS  door: a gateway refusal never blocks the decision; the team is told what the gateway said, and the outbox and claim page show it
  PASS  door: the team's Stop texts button: a viewer is refused, a member stops them
  PASS  door: Send test sends one text with the business name and shows the gateway's answer; a member cannot
  PASS  door: switching texts off cancels a text still waiting for the sending hours (it is never sent late)
  PASS  door: with texts off, the claim form shows no box again

== 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)
  PASS  updates: the updater table holds exactly one state row
  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 Warrantora 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 Warrantora 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: the owner's "keep" count is honored (2 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 Warrantora, 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 8 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 (25 tables dumped)
  PASS  erase planting · sqlite: the photo they sent is deleted from disk
  PASS  erase planting · sqlite: the claim keeps its record: fault and outcome unchanged, raised_by says the customer raised it
  PASS  erase planting · sqlite: a service-desk member cannot erase: HTTP 403, nothing changed
  PASS  erase planting · sqlite: a service-desk member cannot open the Data requests page: HTTP 403
  PASS  erase planting · sqlite: an admin is refused while a claim on their items is open, and nothing changes
  PASS  erase planting · sqlite: an admin opens the Data requests page and the customer is listed
  PASS  erase planting · sqlite: the erase page (/gdpr) says what an erase keeps: "Kept: the fault as the customer described it in their own wo…"
  PASS  erase planting · sqlite: MANUAL.md says the same: "Kept: the fault as the customer described it in their own wo…"
ALL 347 TESTS PASSED (0 failed)

← Back to Warrantora · Manual · Quickstart · API