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