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

Rentara Test run, as shipped in the download

437 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. occupancy: occupied / total, notice excluded ==
  PASS  total units (got 5, want 5)
  PASS  occupied count (got 3, want 3)
  PASS  vacant count (got 1, want 1)
  PASS  notice count (got 1, want 1)
  PASS  occupancy pct = occupied/total (3/5=60) (got 60, want 60)
  PASS  empty portfolio pct is 0 (no div-by-zero) (got 0, want 0)
  PASS  empty portfolio total 0 (got 0, want 0)
  PASS  fully occupied = 100 (got 100, want 100)
  PASS  rounding 1/3 -> 33 (got 33, want 33)

== 2. arrears: unpaid AND due_date strictly before today ==
  PASS  arrears count (only #1 and #5) (got 2, want 2)
  PASS  arrears total is sum of overdue unpaid (got 235000, want 235000)
  PASS  due-today charge is NOT arrears
  PASS  paid charge is NOT arrears
  PASS  future charge is NOT arrears
  PASS  yesterday charge IS arrears
  PASS  no charges -> zero arrears (got 0, want 0)

== 3. monthTotals: collected vs due for a calendar month ==
  PASS  due this month = sum of July charges (got 455000, want 455000)
  PASS  collected this month = paid July charges (got 335000, want 335000)
  PASS  outstanding = due - collected (got 120000, want 120000)
  PASS  due count (got 3, want 3)
  PASS  paid count (got 2, want 2)
  PASS  month with no charges -> 0 due (got 0, want 0)

== 4. planCharges: idempotent monthly generation for active leases ==
  PASS  generates one charge per active lease (2) (got 2, want 2)
  PASS  ended lease produces no charge
  PASS  upcoming lease produces no charge
  PASS  due date is month-01 (got '2026-07-01', want '2026-07-01')
  PASS  amount comes from lease rent (got 145000, want 145000)
  PASS  re-running with existing charges creates nothing (idempotent) (got 0, want 0)
  PASS  only the un-charged active lease is planned (got 1, want 1)
  PASS  and it is lease 11 (got 11, want 11)
  PASS  a different month generates fresh charges (got 2, want 2)
  PASS  duplicate lease id in input -> single charge (got 1, want 1)

== 5. expiringLeases: active leases ending within N days (inclusive) ==
  PASS  three leases in the 30-day window (got 3, want 3)
  PASS  lease ending exactly today is included
  PASS  lease ending on the last day (today+30) is included
  PASS  lease ending one day past the window is excluded
  PASS  already-expired lease excluded
  PASS  non-active lease excluded
  PASS  open-ended lease excluded
  PASS  sorted soonest-first (first is today) (got 1, want 1)
  PASS  next is July 20 (id 7) before Aug 4 (id 2) (got 7, want 7)

== 6. ticket state machine: open -> in_progress -> resolved ==
  PASS  open -> in_progress allowed
  PASS  open -> resolved allowed
  PASS  in_progress -> resolved allowed
  PASS  in_progress -> open allowed
  PASS  resolved -> open allowed (reopen)
  PASS  resolved -> in_progress NOT allowed
  PASS  open -> open NOT allowed (no-op)
  PASS  unknown state NOT allowed
  PASS  moving to resolved stamps resolved_at (got '2026-07-05 12:00:00', want '2026-07-05 12:00:00')
  PASS  resolving keeps an existing stamp (got '2026-06-01 09:00:00', want '2026-06-01 09:00:00')
  PASS  reopening clears resolved_at (got NULL, want NULL)
  PASS  moving to in_progress clears resolved_at (got NULL, want NULL)

== 7. unit status flips on lease assign / end ==
  PASS  assigning a lease occupies the unit (got 'occupied', want 'occupied')
  PASS  ending a lease frees the unit (got 'vacant', want 'vacant')
  PASS  occupyUnit does not mutate the input array
  PASS  lease status derives to upcoming for a future start (got 'upcoming', want 'upcoming')
  PASS  lease status derives to active for a past/today start (got 'active', want 'active')

== 8. toCsv: RFC-4180 escaping + CRLF ==
  PASS  header row (got 'Property,Tenant,Amount,Note', want 'Property,Tenant,Amount,Note')
  PASS  plain row unquoted (got 'Maple Court,Sarah Johnson,1450.00,ok', want 'Maple Court,Sarah Johnson,1450.00,ok')
  PASS  CRLF line endings used
  PASS  field with a comma is quoted
  PASS  embedded double-quotes are doubled
  PASS  newline-containing field survives a round-trip (got 'line1
line2', want 'line1
line2')
  PASS  quoted comma field parses back correctly (got 'O\'Brien, James', want 'O\'Brien, James')
  PASS  formula = neutralized
  PASS  formula + neutralized
  PASS  formula - neutralized
  PASS  formula @ neutralized
  PASS  leading TAB neutralized
  PASS  formula with comma is neutralized AND rfc-quoted
  PASS  dangerous cell in a later column neutralized
  PASS  neutralized value carries the guard quote (got '\'=1+1', want '\'=1+1')
  PASS  a plain non-formula value is NOT altered
  PASS  negative amount exports verbatim (numeric-aware guard) (got '-1234.56', want '-1234.56')
  PASS  positive amount exports verbatim (got '1450.00', want '1450.00')
  PASS  zero exports verbatim (got '0', want '0')
  PASS  negative amount round-trips to a parseable number (got '-1234.56', want '-1234.56')

== 9. money / cents helpers ==
  PASS  money formats cents with symbol (got '$1,450.00', want '$1,450.00')
  PASS  money handles negatives (got '-$45.00', want '-$45.00')
  PASS  money zero (got '$0.00', want '$0.00')
  PASS  to_cents parses plain decimal (got 145000, want 145000)
  PASS  to_cents strips grouping + symbol (got 145050, want 145050)
  PASS  to_cents empty -> 0 (got 0, want 0)
  PASS  to_cents clamps oversize positive to INT max (got 2147483647, want 2147483647)
  PASS  to_cents clamps oversize negative to INT min (got -2147483648, want -2147483648)
  PASS  to_cents leaves an in-range amount intact (got 2100000000, want 2100000000)
  PASS  str_cap truncates to the column length (got 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', want 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
  PASS  str_cap leaves a short string intact (got 'Maple Court', want 'Maple Court')
  PASS  centsToDecimal round-trips (got '1450.00', want '1450.00')
  PASS  centsToDecimal null -> empty (got '', want '')
  PASS  currency_symbol known code (got '€', want '€')
  PASS  currency_symbol unknown code falls back (got 'SEK ', want 'SEK ')

== 10. date helpers + validators ==
  PASS  date_add_days forward (got '2026-08-04', want '2026-08-04')
  PASS  date_add_days backward (got '2026-06-30', want '2026-06-30')
  PASS  date_add_months (got '2027-07-05', want '2027-07-05')
  PASS  Rent::addDays matches (got '2026-08-19', want '2026-08-19')
  PASS  normalizeUnitStatus keeps valid (got 'notice', want 'notice')
  PASS  normalizeUnitStatus defaults invalid (got 'vacant', want 'vacant')
  PASS  normalizePriority keeps valid (got 'high', want 'high')
  PASS  normalizePriority defaults invalid (got 'med', want 'med')
  PASS  normalizeLeaseStatus defaults invalid (got 'active', want 'active')

== 11. billingPeriods: anchor day, clamping, frequency, end date ==
  PASS  mid-month start: multi-month catch-up plans 5 periods (Mar–Jul) (got 5, want 5)
  PASS  first due date IS the lease start date (got '2026-03-10', want '2026-03-10')
  PASS  first period key (got '2026-03', want '2026-03')
  PASS  later periods keep the anchor day (got '2026-06-10', want '2026-06-10')
  PASS  Jan-31 anchor clamps to Feb 28 (got '2026-02-28', want '2026-02-28')
  PASS  …back to the 31st in March (got '2026-03-31', want '2026-03-31')
  PASS  …and the 30th in April (got '2026-04-30', want '2026-04-30')
  PASS  quarterly: 3 periods Jan/Apr/Jul (got 3, want 3)
  PASS  quarterly periods land every 3rd month (got '2026-04', want '2026-04')
  PASS  quarterly keeps the anchor day (got '2026-07-15', want '2026-07-15')
  PASS  periods due after the lease end date are excluded (got 3, want 3)
  PASS  last period is the one still due before end (got '2026-05-10', want '2026-05-10')
  PASS  future start yields no periods (got 0, want 0)
  PASS  garbage start date yields no periods (got 0, want 0)

== 12. planRecurring: idempotent catch-up keyed on lease+period ==
  PASS  catch-up plans 5 + 2 periods for the two active leases (got 7, want 7)
  PASS  ended lease posts nothing
  PASS  rows carry the clear ledger label (got 'Rent 2026-03', want 'Rent 2026-03')
  PASS  amount comes from the lease (got 100000, want 100000)
  PASS  2nd catch-up run plans nothing (idempotent) (got 0, want 0)
  PASS  3rd catch-up run still plans nothing (got 0, want 0)
  PASS  legacy month-01 charge suppresses that period (no double-billing) (got 4, want 4)
  PASS  and the suppressed period is March
  PASS  planForMonth returns only the asked month (got 2, want 2)
  PASS  …with the anchored due date (got '2026-07-10', want '2026-07-10')
  PASS  planForMonth on a pre-start month is empty (got 0, want 0)
  PASS  quarterly lease skips an off-cycle month (got 0, want 0)

== 13. planLateFees: grace boundary, flat vs bp vs both, one per period ==
  PASS  due+grace day exactly -> NO fee (grace boundary) (got 0, want 0)
  PASS  due+grace+1 -> fee posts (got 1, want 1)
  PASS  flat fee amount (got 2500, want 2500)
  PASS  fee due date is deterministic (due+grace+1) (got '2026-06-07', want '2026-06-07')
  PASS  fee label names the overdue period (got 'Late fee — Rent 2026-06', want 'Late fee — Rent 2026-06')
  PASS  fee keeps the rent period key (got '2026-06', want '2026-06')
  PASS  bp fee: 5% of 1450.00 = 72.50 (got 7250, want 7250)
  PASS  flat + bp combine: 25.00 + 72.50 = 97.50 (got 9750, want 9750)
  PASS  an existing fee for the period suppresses a second one, forever (got 0, want 0)
  PASS  a PAID rent charge never gets a fee (got 0, want 0)
  PASS  late fees never compound (late_fee input ignored) (got 0, want 0)
  PASS  rule disabled (0 flat, 0 bp) -> automation off (got 0, want 0)
  PASS  zero grace: overdue by 1 day fees immediately (got 1, want 1)
  PASS  lateFeeCents clamps to the INT range (got 2147483647, want 2147483647)

== 14. statementTotals: hand-computed cash-basis fixture ==
  PASS  rent collected = 1450.00 + 1650.00 (got 310000, want 310000)
  PASS  late fees collected = 97.50 (got 9750, want 9750)
  PASS  maintenance = only the in-range resolved cost (got 40000, want 40000)
  PASS  NET = 3100.00 + 97.50 − 400.00 = 2797.50 (got 279750, want 279750)
  PASS  billed in range (all types due Jun) (got 609750, want 609750)
  PASS  still-open billed (got 190000, want 190000)
  PASS  empty statement nets zero (got 0, want 0)

== 15. Billing on a real database: 3× catch-up -> identical ledger; token flow ==
[DB: SQLite]
  PASS  3.0: migrate() created the settings singleton, so the fixture UPDATE lands
  PASS  run 1 posts 5 rent periods (Mar 15 … Jul 15) (got 5, want 5)
  PASS  run 1 posts 4 late fees (Jul rent not yet past grace) (got 4, want 4)
  PASS  late fee amount = 25.00 flat + 1% of 1000.00 (got 3500, want 3500)
  PASS  run 2 posts nothing (got 0, want 0)
  PASS  run 3 posts nothing (got 0, want 0)
  PASS  ledger after 3 runs is byte-identical to after 1 run
  PASS  ledger row count stays 9 (got 9, want 9)
  PASS  rent rows carry clear labels (got 'Rent 2026-03', want 'Rent 2026-03')
  PASS  fee rows carry clear labels (got 'Late fee — Rent 2026-03', want 'Late fee — Rent 2026-03')
  PASS  later run: only the missing Aug rent posts; paid-July gets no fee (got 1, want 1)
  PASS  …and no new fees for old periods (one per period, ever) (got 0, want 0)
  PASS  DB unique index rejects a duplicate (lease,type,period) row

-- portal token flow --
  PASS  unknown token -> null (404 path) (got NULL, want NULL)
  PASS  empty token -> null (got NULL, want NULL)
  PASS  oversize token -> null (got NULL, want NULL)
  PASS  issued token is 40 hex chars (got 1, want 1)
  PASS  valid token resolves to the lease with display context
  PASS  regenerating rotates the token
  PASS  the OLD link is dead immediately after regenerate (got NULL, want NULL)
  PASS  the new link works
  PASS  revoked token -> null (404 path) (got NULL, want NULL)

-- portal per-IP rate limit --
  PASS  5 submissions/hour from one IP -> limited
  PASS  a different IP is not limited
  PASS  empty IP never limited (proxied installs)

== 16. Database::migrate(): old-shape install upgrades in place ==
  PASS  settings gains the version marker (got 2, want 2)
  PASS  settings data intact (got 'Buyer LLC', want 'Buyer LLC')
  PASS  leases gain portal_token / frequency / auto_bill
  PASS  migrated lease keeps automation OFF (no surprise backfill) (got 0, want 0)
  PASS  migrated lease defaults to monthly (got 'monthly', want 'monthly')
  PASS  charge rows intact (got 2, want 2)
  PASS  period keys backfilled from due dates (got '2026-05', want '2026-05')
  PASS  legacy charges typed as rent (got 'rent', want 'rent')
  PASS  paid data intact (got '2026-05-02', want '2026-05-02')
  PASS  lease_documents table exists and accepts rows
  PASS  properties gain owner fields (got '', want '')
  PASS  tickets gain source (default staff) (got 'staff', want 'staff')
  PASS  late fee can share a due date with a rent charge (old index dropped) (got 2, want 2)
  PASS  second migrate() is a no-op (version short-circuits) (got 3, want 3)
  PASS  migrated lease does not auto-post (automation off by default) (got 0, want 0)

== 17. upload-name safety + honeypot + clip ==
  PASS  pdf upload gets a random name under data/uploads (got 1, want 1)
  PASS  jpeg upload name pattern (got 1, want 1)
  PASS  names are random (two calls differ)
  PASS  text/html upload rejected (got NULL, want NULL)
  PASS  image/svg+xml rejected (script-capable) (got NULL, want NULL)
  PASS  application/x-php rejected (got NULL, want NULL)
  PASS  pdf is NOT allowed for tenant photos (got NULL, want NULL)
  PASS  png IS allowed for tenant photos (got 1, want 1)
  PASS  honeypot trips on a filled website field
  PASS  honeypot silent for real users (empty/absent)
  PASS  9000-char tenant title clips to the column width (got 190, want 190)
  PASS  tenant details clip to 16000 chars (utf8mb4-safe under 64 KB) (got 16000, want 16000)

== 18. CSRF empty-token rejection + new POST handlers guarded ==
  PASS  empty stored + empty submitted CSRF token is REJECTED (no hash_equals("","") bypass)
  PASS  new POST handler portal_submit enforces CSRF
  PASS  new POST handler lease_portal_issue enforces CSRF
  PASS  new POST handler lease_portal_revoke enforces CSRF
  PASS  new POST handler lease_autobill enforces CSRF
  PASS  new POST handler document_upload enforces CSRF
  PASS  new POST handler document_delete enforces CSRF
  PASS  new POST handler ticket_cost enforces CSRF
  PASS  portal submit checks the honeypot
  PASS  portal submit enforces the per-IP rate limit
  PASS  document serve is auth-gated
  PASS  document serve sends nosniff
  PASS  ticket photo serve is auth-gated + nosniff
  PASS  statement endpoints are auth-gated
  PASS  document upload verifies MIME with finfo, not the client

== 19. statement CSV: formula injection neutralized, money stays numeric ==
  PASS  malicious property name neutralized in statement CSV
  PASS  malicious tenant name neutralized
  PASS  malicious label neutralized
  PASS  negative money survives verbatim (numeric-aware guard)
  PASS  positive money survives verbatim

== 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: v20_survives_version_guard
  PASS  v2: mint
  PASS  v2: revoke
  PASS  v2: totp
  PASS  v2: recovery
  PASS  v2: delivery
  PASS  v2: rate
  PASS  v2: csv_dry
  PASS  v2: csv_commit
  PASS  v2: csv_dup_skips
  PASS  v2: payment_rule
  PASS  v2: payment_fallbacks
  PASS  v2: ticket_rules
  PASS  v2: arrears_computed
  PASS  v2: backup_tables
  PASS  v2: routes registered (api, 2fa, security, backup, import, healthz)
  PASS  v2: 2FA intercepts login + login is rate-limited
  PASS  v2: notifications + webhooks fire on the BROWSER path (incl. the tenant portal), never fatally
  PASS  v2: API reuses the tested core (no re-derived guards)
  PASS  v2: the UI posts payments through the SAME rule as the API
  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  v2: API.md ships (the sw cache version is asserted in the 3.0 block)

== Own It 3.0: MCP - roles - GDPR - iCal - PDF - the migrate trap ==
  PASS  migrate() creates audit_log + invites + saved_views
  PASS  users gain role; settings gain branding, SSO and the calendar token; tenants gain anonymized_at
  PASS  install() ALONE produces the current schema
  PASS  migrate() creates the settings singleton row
  PASS  so "UPDATE settings ... WHERE id = 1" is no longer a silent no-op
  PASS  **THE TRAP: migrate() returns early on a current install — the v3 block still runs**
  PASS  a member records payments, works tickets, edits leases, issues portal links
  PASS  but cannot delete a tenant or property, or reach settings
  PASS  a viewer reads the rent roll and statements and cannot record or edit
  PASS  a viewer canNOT read the audit trail or the team roster
  PASS  an admin holds deletions and the late-fee rule
  PASS  AN UPGRADE NEVER LOOSENS: the existing user stays an admin
  PASS  the last admin cannot be demoted into a lockout
  PASS  tickets move open -> in_progress -> resolved
  PASS  **resolved -> open is ALLOWED (the intentional reopen the row names)**
  PASS  but resolved -> in_progress is not a legal move
  PASS  resolving stamps resolved_at
  PASS  and reopening clears it
  PASS  MCP exposes exactly the row's seven tools, in order
  PASS  every tool description declares READ-ONLY or WRITES
  PASS  GET /mcp is refused with 405, not 404
  PASS  a nested argument that is not allowlisted is refused
  PASS  an unknown argument is refused BY NAME
  PASS  lease_detail returns the ledger and the outstanding balance
  PASS  **arrears_report EQUALS Rent::arrears() — never re-derived**
  PASS  and it finds the overdue charges with the right total
  PASS  rent_roll EQUALS Rent::monthTotals()
  PASS  create_ticket raises a ticket with a normalised priority
  PASS  advance_ticket moves a ticket forward
  PASS  **an agent may REOPEN a resolved ticket, exactly as the UI can**
  PASS  and an illegal move is refused with the allowed ones named
  PASS  MCP role gate matches the UI: a viewer key cannot raise a ticket
  PASS  and a viewer key can still read arrears
  PASS  MCP writes land in the audit trail
  PASS  the fixture has a live tenancy AND money owed (the blocked case)
  PASS  subject access returns the person
  PASS  **anonymise really blanks name, email and phone (verified on the ROW)**
  PASS  **and the tenancy stays — it is the record of a tenancy, not of a person**
  PASS  and it reports what it touched
  PASS  the statement renders as a real PDF
  PASS  and its totals come from Rent::statementTotals()
  PASS  the calendar token verifies and a wrong one does not
  PASS  the feed is a real VCALENDAR with stable UIDs
  PASS  **the calendar feed carries no tenant name or email**
  PASS  the backup drops bcrypt password hashes
  PASS  and API-key hashes
  PASS  and the SMTP password
  PASS  **and the tenant portal token — a live magic link into a ledger**
  PASS  while still carrying the estate itself
  PASS  and a restore keeps the live token rather than blanking it
  PASS  inkOn() clears 4.5:1 on a very light and a very dark accent
  PASS  a PHP script named .png is refused
  PASS  another product's backup is refused by name stamp
  PASS  OIDC refuses alg=none
  PASS  mutations write audit entries
  PASS  the trail exports as CSV
  PASS  **no audit row contains a tenant portal token**
  PASS  exactly ONE place fires payment.recorded
  PASS  exactly ONE place fires maintenance.status_changed
  PASS  the ticket transition guard is called from one place
  PASS  all four payment doors go through _rn_payment_recorded()
  PASS  all four ticket doors go through _rn_ticket_status_apply()
  PASS  no door invents an event name nobody subscribes to
  PASS  bulk actions email and fire webhooks AFTER the commit
  PASS  the two tenant-facing toggles exist
  PASS  a fresh install writes to no tenant
  PASS  a dead mail server returns false, never an exception
  PASS  no address (or a malformed one) is skipped, not an error
  PASS  the receipt reaches the tenant: amount, date, what is left
  PASS  **the receipt never asks a tenant to pay online**
  PASS  a status change reaches the tenant who raised it
  PASS  both messages carry ONE well-formed portal link
  PASS  a refused transition writes nothing and says why
  PASS  saving the org card no longer erases the SMTP config
  PASS  absolute URLs are built once — no doubled scheme, views included
  PASS  Settings shows and saves the tenant-facing switches

== Own It 3.0: wiring (static) ==
  PASS  Database::migrate() runs at boot
  PASS  POST and GET /mcp are both routed
  PASS  src includes are idempotent
  PASS  migrateV3() is called before migrate()'s fast-path return
  PASS  install() also reaches the v3 schema
  PASS  the maintenance bulk literal is registered BEFORE /maintenance/{id}
  PASS  the MCP layer computes no arrears, rent totals or transitions of its own
  PASS  the statement PDF reuses the screen's own scope/range/rows helpers
  PASS  bulk pay loops Billing::recordPayment() inside one transaction
  PASS  the roll's pay endpoint delegates to Billing::recordPayment and never writes the ledger itself
  PASS  the roll's pay endpoint is role-gated (403) and CSRF-checked before any write
  PASS  the roll's pay endpoint maps refusals to honest codes and audits the payment
  PASS  the rent roll is unit-centric: every unit is a row, vacant ones included
  PASS  the roll keeps its bulk-pay and saved views (no feature lost to the redesign)
  PASS  no bulk action mass-updates the ledger behind the product's back
  PASS  bulk ticket moves are validated per ticket, not per batch
  PASS  the GDPR map uses the key the core actually reads
  PASS  anonymising is refused while a tenancy is active or money is owed
  PASS  the audit trail never receives a raw portal token
  PASS  destructive + money actions are role-gated
  PASS  the REST surface is role-gated too
  PASS  money mutations write audit entries
  PASS  the backup redaction list covers the portal token
  PASS  BOTH backup paths use that one list
  PASS  filled controls take their ink from the branding contrast helper
  PASS  the uploaded logo is actually RENDERED in the layout
  PASS  the branding accent overrides BOTH themes
  PASS  P5 is reachable and does not nest forms on the rent roll
  PASS  P5 is reachable on the maintenance list too
  PASS  the tenant page exposes subject access and anonymisation
  PASS  the PDFs are reachable from the screens that own them
  PASS  src/Mcp.php is byte-identical to the shared core copy of Mcp.php
  PASS  src/AuditLog.php is byte-identical to the shared core copy of AuditLog.php
  PASS  src/Pdf.php is byte-identical to the shared core copy of Pdf.php
  PASS  src/Ical.php is byte-identical to the shared core copy of Ical.php
  PASS  src/BackupRestore.php is byte-identical to the shared core copy of BackupRestore.php
  PASS  src/Branding.php is byte-identical to the shared core copy of Branding.php
  PASS  src/RolesKit.php is byte-identical to the shared core copy of RolesKit.php
  PASS  src/Oidc.php is byte-identical to the shared core copy of Oidc.php
  PASS  src/Gdpr.php is byte-identical to the shared core copy of Gdpr.php
  PASS  a switched-off person's API key stops working (u.active in requireKey)
  PASS  sw.js cache is static-v3.1.6
  PASS  healthz + api_ping + openapi report 3.1.6
  PASS  API.md documents the MCP endpoint and the reopen rule
  PASS  README documents "New in 3.0"
  PASS  the deploy kit is filled in
  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)
  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

== fix wave: the landlord hears when a lease is ending ==
  PASS  lease end setting and ledger exist
  PASS  lease end is off on a fresh install
  PASS  lease end needs smtp and an address
  PASS  lease end dry run lists only active leases inside 60 days
  PASS  lease end a refused send releases its claims
  PASS  lease end one message to the landlord listing both
  PASS  lease end the same stage is never sent twice
  PASS  lease end the 14 day stage comes once more
  PASS  lease end a renewed lease starts over
  PASS  lease end is audited
  PASS  lease ends: the viewer is refused the check-now button (403)
  PASS  lease ends: the member is refused the check-now button (403)
  PASS  lease ends: an admin presses the button and is told what happened
  PASS  lease ends: the switch and the schedule line are on the Email settings
  PASS  lease ends: cron/lease-ends.php runs the one function and refuses anything but the command line
  PASS  lease ends: the web server refuses /cron/ (router and .htaccess)
  PASS  ui: the sidebar brand cannot be shrunk to nothing (flex-shrink:0)

== fix wave: the webhook buttons work ==
  PASS  webhooks: Add creates the endpoint (no 404)
  PASS  webhooks: Pause switches it off (no 404)
  PASS  webhooks: pressing it again switches it back on
  PASS  webhooks: the page offers Pause and Delete for the endpoint, at those same addresses
  PASS  webhooks: Delete removes it (no 404)
  PASS  readme: no feature is still labelled "new in 1.1"

== fix wave: every backup and restore route is administrator-only ==
  PASS  backup: a signed-in viewer gets 403 on GET /backup.json
  PASS  backup: a signed-in viewer gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in viewer gets 403 on GET /restore
  PASS  backup: a signed-in viewer gets 403 on POST /restore/preview
  PASS  backup: a signed-in viewer gets 403 on POST /restore/commit
  PASS  backup: a signed-in viewer gets 403 on POST /backup/token
  PASS  backup: a signed-in viewer without the token gets nothing from GET /backup/scheduled
  PASS  backup: a signed-in member gets 403 on GET /backup.json
  PASS  backup: a signed-in member gets 403 on GET /backup.sqlite
  PASS  backup: a signed-in member gets 403 on GET /restore
  PASS  backup: a signed-in member gets 403 on POST /restore/preview
  PASS  backup: a signed-in member gets 403 on POST /restore/commit
  PASS  backup: a signed-in member gets 403 on POST /backup/token
  PASS  backup: a signed-in member without the token gets nothing from GET /backup/scheduled
  PASS  backup: an admin still opens the restore page

-- F2b: the demo ribbon's wording and size; the JSON backup's secrets --
  PASS  demo ribbon: on the tenant link (/t/…) it says what the page is, not "signed in as the admin"
  PASS  demo ribbon: an administrator's page keeps the admin wording
  PASS  demo ribbon: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session
  PASS  demo ribbon: registered only behind the .demo-mode marker, and non-HTML output passes through untouched
  PASS  backup: the JSON download carries none of the 11 credentials this schema holds (settings.oidc_client_secret, settings.backup_token_hash, settings.ical_token_hash, settings.smtp_pass, users.password_hash, users.totp_secret, users.totp_recovery, leases.portal_token, invites.token_hash, api_keys.token_hash, webhooks.secret)

————————————————————————————————————————————————————
ALL 437 TESTS PASSED (0 failed)
437 assertions

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