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

Complia Test run, as shipped in the download

330 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. Schema install (SQLite in-memory) ==
  PASS  schemaExists() after install
  PASS  table users exists
  PASS  table settings exists
  PASS  table complaints exists
  PASS  table complaint_log exists
  PASS  install() is idempotent (re-run does not throw)

== 2. Settings ==
  PASS  org_name read through App::set
  PASS  ack_window_days read
  PASS  timezone read
  PASS  App::today() uses business timezone

== 3. s.164A(3) acknowledgement-deadline math ==
  PASS  default 29-day window (day-1-inclusive safe)
  PASS  strict statutory reading satisfied: received Mon 2026-08-03 -> due 2026-09-01, never 09-02
  PASS  explicit 30-day window still available
  PASS  month rollover
  PASS  year rollover
  PASS  leap year Feb
  PASS  non-leap Feb
  PASS  custom window 14 days
  PASS  custom window 60 days
  PASS  window clamped: 0 -> 1 day
  PASS  window clamped: 9999 -> 365

== 4. daysRemaining (signed) ==
  PASS  10 days left
  PASS  due today = 0
  PASS  overdue by 5
  PASS  across month
  PASS  across year

== 5. Status workflow transitions ==
  PASS  received -> acknowledged
  PASS  received -> investigating
  PASS  received -> resolved
  PASS  received -> rejected
  PASS  acknowledged -> investigating
  PASS  acknowledged -> resolved
  PASS  investigating -> resolved
  PASS  investigating -> rejected
  PASS  NO acknowledged -> received (backwards)
  PASS  NO investigating -> acknowledged (backwards)
  PASS  NO resolved -> anything (terminal)
  PASS  NO rejected -> anything (terminal)
  PASS  NO received -> received (no-op)
  PASS  NO transition to bogus status

== 6. Derived display status (ack_overdue precedence) ==
  PASS  received before due -> received
  PASS  received past due -> ack_overdue
  PASS  received due today -> received (not overdue)
  PASS  acknowledged past due -> acknowledged
  PASS  investigating past due -> investigating
  PASS  resolved past due -> resolved
  PASS  rejected past due -> rejected
  PASS  received + ack timestamp -> received (duty met)
  PASS  bogus stored status -> received

== 7. Public intake -> token -> lookup flow ==
  PASS  create returns id
  PASS  token is 40 hex chars
  PASS  token hex only
  PASS  byToken finds complaint
  PASS  email lowercased + trimmed
  PASS  status starts received
  PASS  ack_due_at = today + 30 (business tz)
  PASS  reference stored
  PASS  submitted log entry written
  PASS  byToken miss -> null
  PASS  byToken rejects malformed token
  PASS  byToken rejects empty token
  PASS  byToken rejects SQLi-ish token

== 8. Intake validation ==
  PASS  valid input accepted
  PASS  empty name rejected
  PASS  overlong name rejected
  PASS  bad email rejected
  PASS  empty subject rejected
  PASS  empty details rejected

== 9. Anti-abuse: per-IP rate limit ==
  PASS  10 submissions/hour from one IP -> limited
  PASS  different IP not limited
  PASS  empty IP never limited (proxied installs)

== 10. clip/clamp overflow safety (MySQL strict-mode defense) ==
  PASS  clip long string to 190
  PASS  clip short string untouched
  PASS  clip multibyte-safe
  PASS  clamp_int huge -> 2e9
  PASS  clamp_int -huge -> -2e9
  PASS  clamp_int non-numeric -> 0
  PASS  clamp_int custom bounds
  PASS  oversized subject clipped to 255
  PASS  oversized reference clipped to 100
  PASS  oversized ip clipped to 45
  PASS  100KB details stored intact (big text column)

== 11. Investigation log (append-only) ==
  PASS  two notes appended
  PASS  order preserved (append-only)
  PASS  no UPDATE/DELETE of complaint_log anywhere in controllers

== 12. Status change semantics (ack timestamp + outcome) ==
  PASS  acknowledged_at recorded
  PASS  isAcked true after acknowledgement
  PASS  derived status acknowledged even past due
  PASS  outcome stored
  PASS  outcome_date stored
  PASS  terminal: no transitions from resolved

== 13. Auth (real users table) ==
  PASS  correct password accepted
  PASS  wrong password rejected
  PASS  unknown user rejected
  PASS  email case-insensitive
  PASS  inactive user rejected

== 14. Audit CSV export (formula guard, numeric-aware) ==
  PASS  =SUM formula neutralized
  PASS  @ formula neutralized
  PASS  +cmd neutralized
  PASS  -text neutralized
  PASS  leading TAB neutralized
  PASS  negative number NOT corrupted
  PASS  negative decimal NOT corrupted
  PASS  positive numeric "+1" kept
  PASS  plain text untouched
  PASS  comma cell RFC-4180 quoted
  PASS  quote cell escaped
  PASS  formula + comma: guarded AND quoted
  PASS  CSV header
  PASS  CSV row count = complaints + history + header
  PASS  CRLF line endings
  PASS  complaint rows tagged
  PASS  history rows present
  PASS  complaint row cell count
  PASS  formula name neutralized in real export
  PASS  formula subject neutralized in real export

== 15. Security hardening regressions (source-level) ==
  PASS  csrf_check rejects empty stored token (hash_equals empty-token bypass)
  PASS  installer rejects empty install token (_it bypass)
  PASS  demo session cookie is None + Secure + Partitioned (CHIPS)
  PASS  buyer session cookie is HttpOnly + Lax + Secure-on-HTTPS
  PASS  portable UTC timestamps (no SQLite datetime() in app SQL)
  PASS  clip + clamp_int helpers exist
  PASS  settings save clips org fields + clamps ack window
  PASS  complaint create clips every VARCHAR-bound field
  PASS  details/notes are MEDIUMTEXT on MySQL (64KB TEXT cap avoided)
  PASS  public POST handlers csrf_check (intake + track)
  PASS  honeypot field checked in intake handler
  PASS  per-IP rate limit enforced in intake handler
  PASS  status route enforces transition matrix
  PASS  closing a complaint requires an outcome (in the shared rule)
  PASS  v3: exactly ONE status-advance rule, used by UI + API + MCP
  PASS  token lookups are exact parameterized matches
  PASS  CSV export requires the export permission (not merely login)
  PASS  public status page 404s on miss
  PASS  all output escaped: views use e() on complaint fields
  PASS  .htaccess blocks config/data/src/bin/tests/controllers

== 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/v3: tables
  PASS  v2/v3: totp_cols
  PASS  v2/v3: smtp_cols
  PASS  v2/v3: mint
  PASS  v2/v3: revoke
  PASS  v2/v3: totp
  PASS  v2/v3: recovery
  PASS  v2/v3: delivery
  PASS  v2/v3: no_phantom_events
  PASS  v2/v3: rate
  PASS  v2/v3: ack_due
  PASS  v2/v3: days_remaining_signed
  PASS  v2/v3: derive_status
  PASS  v2/v3: overdue_not_a_column
  PASS  v2/v3: transitions
  PASS  v2/v3: create_validates
  PASS  v2/v3: create_sets_clock
  PASS  v2/v3: complaint_serializer
  PASS  v2/v3: serializer_nocompute
  PASS  v2/v3: csv_dry
  PASS  v2/v3: csv_backlog_clock
  PASS  v2/v3: backup_tables
  PASS  v2/v3: v3_tables
  PASS  v2/v3: audit_redacts
  PASS  v2/v3: gdpr_map_columns_exist
  PASS  v2/v3: letter_fields_are_data_only
  PASS  v2: routes registered (api, 2fa, security, backup, import, healthz)
  PASS  v2: migrate() runs on every boot (1.x had none — existing installs need it)
  PASS  v2: 2FA intercepts login + login is rate-limited
  PASS  v2: notifications + webhooks fire on the BROWSER path too, never fatally
  PASS  v2/v3: the product reuses its tested core (no re-derived guards)
  PASS  v2: the API never computes its own due date or overdue flag
  PASS  v2/v3: closing needs an outcome (shared rule), and ?status filters the DERIVED status
  PASS  v2: UK-reframed marketing/public copy left untouched
  PASS  v2: dark palette + no hardcoded white backgrounds outside @media print
  PASS  v2: theme.js + qr.js ship and layout loads theme.js + a toggle
  PASS  v2: shared modules are copy-identical to the shared core (never forked per app)
  PASS  v3: API.md ships + sw cache is v3.1.5 (App::VERSION)
  PASS  v3: no call to a function this product does not define
  PASS  C2: no sensitive handler is login-only — all are role-gated
  PASS  C2: both backup doors call the ONE shared redaction list
  PASS  C2: Api::requireKey refuses a deactivated user (u.active)
  PASS  C2: every App:: method the controllers call actually exists
  PASS  C2: Api::base() does not double an already-absolute base
  PASS  C2: the subject-access export takes the same standing as anonymise
  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: acknowledging from the register ==
  PASS  register ack: the transition is accepted
  PASS  register ack: the stored status moved
  PASS  register ack: the statutory clock stopped (acknowledged_at is set)
  PASS  register ack: the derived status is no longer overdue-able
  PASS  register ack: it appended to the append-only log, it did not overwrite it
  PASS  register ack: an unacknowledged complaint past its deadline reads as overdue first
  PASS  register ack: a late acknowledgement is still recorded (lateness is evidence, not a lock)
  PASS  register ack: a resolved complaint refuses the transition, in the engine's own words
  PASS  register ack: the refused complaint was not touched
  PASS  register ack: acknowledging can never CLOSE a complaint (closing needs a written outcome)
  PASS  register ack: POST /complaints/{id}/ack is registered
  PASS  register ack: the endpoint requires complaint.advance and checks CSRF
  PASS  register ack: it delegates to the ONE shared transition rule — no second copy of the workflow
  PASS  register ack: an unknown complaint 404s and a refused transition answers 409
  PASS  register ack: it can only ever ask for "acknowledged"
  PASS  kit: layout loads kit css+js, groups the nav with icons + g-keys, carries the theme toggle and a who-line
  PASS  kit: the overdue count rides the sidebar on every page
  PASS  kit: no theme.js; app css is an app layer with a dark accent; the pre-kit file is gone
  PASS  kit: the printed register still forces a light palette (this print-out is the evidence pack)
  PASS  kit: App::asset() versions by mtime and the service worker revalidates code
  PASS  kit: nothing is fetched from another host
  PASS  dashboard: the first-run checklist is computed from the database, not the session
  PASS  dashboard: KPI sparkline, needs-your-action and the stage strip are wired
  PASS  hero: the register carries a clock in every row, tabs with counts, row links and the inline acknowledge
  PASS  record: the complaint page has the kit rail, the big clock and the append-only log
  PASS  public: the complaint form and the status page are on the kit public frame
  PASS  copy: no other product's vocabulary survives in the views

== 21. v3.1 — the complainant's receipt (P1) ==
  PASS  receipt: the subject names the organisation and nothing else
  PASS  receipt: the body carries received date, the response deadline, their own reference and the tracking link
  PASS  receipt: it says why it is so short, so the silence reads as design rather than a bug
  PASS  receipt: sending is due when the operator AND the complainant both said yes
  PASS  receipt REFUSES to echo the complaint back: no subject line, no details, in body OR subject
  PASS  receipt carries no token beyond the one already in the complainant's own link, and no secret
  PASS  receipt refuses: operator switch off means nothing sends, even for a complainant who asked
  PASS  receipt refuses: complainant did not tick the box — the operator switch cannot override that
  PASS  receipt refuses: an unusable email address is never mailed
  PASS  receipt refuses: an install with no organisation name still produces a sane subject
  PASS  receipt idempotency: a complaint already receipted is never due again
  PASS  receipt idempotency: the row is STAMPED before the mail is sent, and the UPDATE re-checks the stamp
  PASS  receipt: a mail failure can never lose the complaint — the send is wrapped and swallowed
  PASS  receipt: one emitter — the automatic path and the resend button both call _cp_send_receipt
  PASS  receipt permission: resending needs complaint.note + CSRF; a viewer cannot
  PASS  opt-in ticked is stored as 1
  PASS  opt-in untouched is stored as 0 — silence is never consent
  PASS  the public form only offers the box when the operator has switched receipts on

== 22. v3.1 — the complainant's evidence (P3) ==
  PASS  evidence: ONE storage implementation serves both doors — staff and complainant
  PASS  evidence: the file's own BYTES decide its type, in the one place that stores it
  PASS  evidence: stored under a RANDOM name, never the name the uploader chose
  PASS  evidence: capped at 10 MB and refused at zero bytes
  PASS  evidence: the public form is multipart, or the browser would send filenames and no files
  PASS  evidence: PHP's parallel-array upload shape is normalised, so evidence[] actually stores
  PASS  evidence: the record page finally RENDERS attachments — the 3.0 route had no screen at all
  PASS  evidence refuses: public uploads are OFF until the operator opens that door
  PASS  evidence refuses: at most 3 files from the public form, whatever the browser posts
  PASS  evidence refuses: a rejected file NEVER fails the submission — the complaint still lands
  PASS  evidence refuses: an attachment is never public — serving needs a login and a permission
  PASS  evidence permission: a viewer can neither attach nor delete
  PASS  evidence permission: delete is owner-gated — your own uploads, or admin
  PASS  evidence permission: only an ADMIN may remove what the complainant supplied
  PASS  evidence: deleting removes the ROW AND THE FILE — a delete that leaves the bytes is the defect
  PASS  evidence: deleting a file that is already gone still removes the row (a second click is safe)

== 23. v3.1 — anonymising now reaches the evidence ==
  PASS  erasure: the map now names the identifying columns on attachments
  PASS  erasure: the FILES are unlinked, and stored_file is blanked so nothing points at them
  PASS  erasure: the files go BEFORE the column pass — a half-done erasure must not leave the bytes
  PASS  erasure: the count is reported to the operator and recorded in the log and the audit trail
  PASS  erasure: still refuses while the complaint is open — that rule is untouched
  PASS  the tracking link in the receipt actually opens the case — GET /track?t= looks it up
  PASS  a wrong or absent tracking code on that link is a 404, never a listing
  PASS  a refused file is REPORTED to the complainant, never silently dropped

== 24. v3.1 — truth on the surfaces ==
  PASS  README changelog carries 3.1.1 and both features
  PASS  README no longer claims Complia never emails the complainant
  PASS  every empty-state icon a view asks for is one the kit actually draws
  PASS  no .alert stacks multiple element children straight into the kit's flex row
  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

== F2. Uploads: the pages promise what the server accepts, and an over-size form never loses a complaint ==
  PASS  ini sizes: 2M, 512K, 1G and "unlimited"
  PASS  upload limits: per file never above 10 MB and never above what PHP takes
  PASS  upload limits: labels read as the page shows them
  PASS  an over-size form (PHP dropped every field) is recognised, not mistaken for an expired session
  PASS  ...and an ordinary form is not
  PASS  a file PHP refused as too large is kept (and so refused out loud); an empty slot is not
  PASS  ...and the refusal names the size limit instead of "choose a file first"
  PASS  the public form states the server's real limit and checks sizes before sending
  PASS  the public submit handles an over-size form BEFORE the CSRF check (which would 419 and lose the complaint)
  PASS  the Docker image accepts 10 MB files and a three-file form
  PASS  receipt: a send the mail server refuses is reported as not sent
  PASS  receipt: ...the register does not say "emailed", it says NOT sent
  PASS  receipt: ...and the claim is released, so it can be resent once mail works
  PASS  settings: saving the Organisation form keeps the SMTP host, the recipient and every switch
  PASS  settings: the Email form still saves its own fields, unticked boxes included
  PASS  reminder steps: parsed descending, de-duplicated, nonsense dropped
  PASS  reminder step: 10 days left reaches none
  PASS  reminder step: 5 days left is the 7-day step
  PASS  reminder step: 2 days left is the TIGHTEST step reached (3), not every step
  PASS  reminder step: the day after the deadline is the overdue step (0)
  PASS  reminder step: a week overdue sends nothing more
  PASS  owners: only active members and admins can own a complaint
  PASS  reminders: off (the default), nothing is sent
  PASS  reminders: on, the complaint 3 days out gets the 3-day step, to its owner and the notification address
  PASS  reminders: the complaint due yesterday gets one overdue reminder
  PASS  reminders: long-overdue and acknowledged complaints get nothing
  PASS  reminders: with no mail server the step is recorded as no-smtp, never as sent
  PASS  reminders: the attempt is written to the complaint's own log, failure included
  PASS  reminders: a second run (cron after a page load) sends nothing — one latch
  PASS  reminders: undelivered steps are listed for the dashboard
  PASS  reminders: a cron route runs them, guarded by the backup token
  PASS  owner: a route names the owner, gated like a note and CSRF-checked
  PASS  dashboard: the reminder steps ride the page load
  PASS  the demo checklist promises a register, not a "defensible" one
  PASS  backup guard: the harness made one signed-in account per role (viewer,member,admin)
  PASS  backup guard: GET /backup.json — 403 for every non-admin role, open to admin
  PASS  backup guard: GET /backup.sqlite — 403 for every non-admin role, open to admin
  PASS  backup guard: GET /restore — 403 for every non-admin role, open to admin
  PASS  backup guard: POST /restore/dry-run — 403 for every non-admin role, open to admin
  PASS  backup guard: POST /restore/commit — 403 for every non-admin role, open to admin
  PASS  backup guard: POST /restore/token — 403 for every non-admin role, open to admin
  PASS  backup guard: GET /backup/scheduled without its token refuses every signed-in role, admin included
  PASS  backup guard: GET /reminders/run without its token refuses every signed-in role, admin included
  PASS  webhooks: "Add" reaches its route and saves the endpoint (was a 404)
  PASS  webhooks: "Pause" reaches its route and flips the endpoint
  PASS  webhooks: "Delete" reaches its route and removes the endpoint
  PASS  demo card: the public pages say what the page is, not "signed in as the admin"
  PASS  demo card: the admin pages keep "signed in as the admin"
  PASS  demo card: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session
  PASS  demo card: boot() hooks it only behind the .demo-mode marker, and a page with no </body> passes through untouched
  PASS  backup redaction: a canary sits in every credential column of the schema (api_keys.token_hash, feed_tokens.token_hash, invites.token_hash, settings.smtp_pass, settings.backup_token_hash, settings.oidc_client_secret, users.password_hash, users.totp_secret, users.totp_recovery, webhooks.secret)
  PASS  backup redaction: each arrives as [REDACTED] and no canary is anywhere in the JSON backup

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

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