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

Slotly Test run, as shipped in the download

169 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.

[DB: SQLite]

== 1. Working hours & slot generation ==
  PASS  slot count Mon 9-17 30min/15step (got 31, want 31)
  PASS  first slot 09:00 (got '09:00', want '09:00')
  PASS  last slot 16:30 (got '16:30', want '16:30')

== 2. Timezone / DST correctness ==
  PASS  09:00 EST -> 14:00 UTC (got '2026-03-02 14:00:00', want '2026-03-02 14:00:00')
  PASS  09:00 EDT -> 13:00 UTC (after DST) (got '2026-03-09 13:00:00', want '2026-03-09 13:00:00')
  PASS  02:30 on spring-forward day does not exist (null)
  PASS  slots still 31 after DST week (got 31, want 31)

== 3. Booking removes the slot ==
  PASS  book 10:00 succeeds
  PASS  10:00 slot now gone
  PASS  09:45 blocked (overlaps 10:00-10:30)
  PASS  10:15 blocked (overlaps 10:00-10:30)
  PASS  09:30 still free (ends 10:00, no overlap)
  PASS  three candidate starts consumed by one 30m booking (got 28, want 28)

== 4. Overlap prevention ==
  PASS  10:15 overlapping slot rejected
  PASS  exact double-book 10:00 rejected

== 5. Buffers ==
  PASS  book Color 11:00 (buffer 15 after)
  PASS  11:30 blocked by 15m buffer
  PASS  11:45 free after buffer

== 6. Lead time & max advance ==
  PASS  48h lead time hides Monday slots (got 0, want 0)
  PASS  beyond max advance -> no slots (got 0, want 0)

== 7. Blackout / time off ==
  PASS  13:00 removed by time off
  PASS  13:30 removed by time off
  PASS  12:45 kept (before time off... but 12:45+30=13:15 overlaps -> removed)
  PASS  14:00 kept (after time off)

== 8. Group capacity ==
  PASS  group seat 1 ok
  PASS  group seat 2 ok
  PASS  group seat 3 ok
  PASS  group seat 4 rejected (capacity 3)
  PASS  overlapping different-start group slot blocked

== 9. Cancel releases the seat ==
  PASS  after cancel, a seat reopens
  PASS  cancelled 10:00 haircut slot reopens

== 10. Recurrence across weekdays ==
  PASS  Sunday has no slots (not in working hours) (got 0, want 0)
  PASS  Tuesday has slots (weekly recurrence)

== 11. Reschedule (atomic move) ==
  PASS  book 14:00
  PASS  move to overlapping-adjacent 14:15 succeeds (self excluded)
  PASS  moved start persisted (got '2026-03-02 19:15:00', want '2026-03-02 19:15:00')
  PASS  same row id -> payment history intact
  PASS  reminder re-armed after move
  PASS  slot_key updated to new time
  PASS  move into conflicting time rejected
  PASS  original booking untouched after failed move (got '2026-03-02 19:15:00', want '2026-03-02 19:15:00')
  PASS  slot_key still held after failed move

== 12. Oversized customer input is capped (MySQL strict-mode 500 guard) ==
  PASS  cap_len trims + caps name to 160 (got 160, want 160)
  PASS  cap_len caps email to 190 (got 190, want 190)
  PASS  cap_len caps phone to 60 (got 60, want 60)
  PASS  cap_len leaves normal value untouched (got 'Jane Doe', want 'Jane Doe')
  PASS  capped name stored within column width (got 160, want 160)
  PASS  capped phone stored within column width (got 60, want 60)

== 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  both slotly layouts (public booking + admin) link the manifest + register the SW
  PASS  v3: migrate created api_keys/webhooks/invites/feed_tokens/saved_views/audit_log (idempotent ×3)
  PASS  v3: users gained totp_secret + totp_recovery
  PASS  v3: settings gained oidc_* + backup_token_hash + backup_keep
  PASS  v3: viewer is read-only (refused appointment.create, allowed *.view)
  PASS  v3: staff runs the desk but not the till settings
  PASS  v3: admin wildcard covers team.manage + audit.view + customer.delete
  PASS  v3: an unknown role is refused everything
  PASS  v3: last-admin guard math
  PASS  v3: role normalize keeps slotly vocabulary
  PASS  v3: every role the team screen offers can sign in (one list, not two)
  PASS  v3: viewer gets through the door and is still refused every write
  PASS  v3: invite token verifies + expiry honoured
  PASS  v3: a live key resolves
  PASS  v3: deactivating the owner kills the key (same query the API door runs)
  PASS  v3: revoking the key kills it too
  PASS  v3: Auth attempt+user both carry AND active = 1
  PASS  v3: Api::requireKey joins u.active = 1
  PASS  v3: exactly one slotly_backup_redact definition
  PASS  v3: manual + scheduled backup doors both use the one list
  PASS  v3: redaction covers password/TOTP/token hashes + webhook secret + cancel_token
  PASS  v3: audit write + filtered read
  PASS  v3: audit detail never stores a secret it was told to redact
  PASS  v3: audit_log is append-only — no UPDATE/DELETE on it anywhere in the product
  PASS  v3: MCP write tools call Schedule::book and Schedule::move (never a re-implementation)
  PASS  v3: MCP tool 'list_services' registered
  PASS  v3: MCP tool 'list_staff' registered
  PASS  v3: MCP tool 'availability' registered
  PASS  v3: MCP tool 'list_appointments' registered
  PASS  v3: MCP tool 'appointment_detail' registered
  PASS  v3: MCP tool 'book_appointment' registered
  PASS  v3: MCP tool 'reschedule_appointment' registered
  PASS  v3: MCP tool 'day_report' registered
  PASS  v3: DELIBERATE refusals — no cancel, delete or payment tool exists for agents
  PASS  v3: MCP write gates use the same permission map as the UI
  PASS  v3: TOTP code verifies and a wrong code does not
  PASS  v3: ten recovery codes
  PASS  v3: webhook events are slotly domain events
  PASS  v3: deliveries are signed X-Slotly-Signature
  PASS  v3: Pdf core emits a real PDF
  PASS  v3: Ical core emits VCALENDAR/VEVENT with UID + DTSTART
  PASS  v3: a foreign product backup is refused
  PASS  v3: own backup parses
  PASS  v3: dry-run diff sees exactly one added customer
  PASS  v3: apply restored the row (and kept the rest)
  PASS  v3: subject-access export contains the customer + appointment tables
  PASS  v3: anonymise blanks identity but keeps the row
  PASS  v3: the agent-door engine refuses the second booking of one seat
  PASS  v3: reschedule through Schedule::move keeps the row
  PASS  v3: index wires /mcp, /healthz, /api/v1, audit, invites, restore, SSO
  PASS  v3: version is 3.1.5 at boot + migrate-at-boot present
  PASS  v3: service worker cache is versioned 3.1.5
  PASS  v3: api-settings controller passes users AND the view renders the owner dropdown
  PASS  v3: CSV dry-run reports but writes nothing
  PASS  v3: CSV commit imports the good row and reports the bad one line-by-line
  PASS  v3: README has New in 3.0 + API.md has the MCP agent door
  PASS  v3: demo auto-login re-seats a non-admin session (not just an empty one)
  PASS  v3: the demo seat logic is gated on the .demo-mode marker (buyers unaffected)
  PASS  v3: a 403 renders inside the admin chrome (a bare page reads as a dead click)
  PASS  v3: every sidebar link is highlighted by the page it opens (no dead-looking nav)
  PASS  v3: no page claims a nav key the sidebar cannot highlight
  PASS  v3: Team and Staff each say what they are for (near-identical people tables)
  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: calendar + move endpoint + dashboard ==
  PASS  calendar: overlapping bookings share lanes inside their cluster only (9:00+9:30 → 2 lanes; 10:00 chains the cluster; 12:00 stands alone full-width)
  PASS  calendar: an empty day yields no lanes and no error
  PASS  kit: admin layout loads the kit css+js, groups the nav, carries the theme toggle and a who-line
  PASS  kit: no theme.js shipped; app css is an app layer (no generic palette, accent tokens present, dark accent defined)
  PASS  kit: App::asset() is versioned by mtime and the service worker revalidates code
  PASS  kit: no font or script is fetched from another host (fonts bundled)
  PASS  routes: POST admin/appointments/{id}/move is registered and calls the locked Schedule::move path
  PASS  move: a booking exists to move
  PASS  move: Schedule::move accepts a free in-hours slot (the endpoint delegates here, so drag → same guarantees as the public page)
  PASS  move: a slot outside working hours is refused with a human sentence
  PASS  dashboard: first-run checklist is computed from the database (services / working hours / bookings), not the session
  PASS  appointments list: search (q) and the Upcoming tab are wired in the controller
  PASS  appointment record: rail with status actions, payment bar, customer history and manage link

== v3.1 P5: payment link ==
  PASS  P5: settings carries payment_link_url (migration ran)
  PASS  P5: the save path refuses a non-https link with a sentence and clips to 500
  PASS  P5: the public booking page shows the link ONLY when a link is set, target=_blank noopener, and Slotly still never processes cards
  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
  PASS  vet: GET /api/ping is registered, the route the store publishes as the keyless check
  PASS  vet: api_ping() demands a key, so a keyless caller is refused rather than answered
  PASS  vet: the refusal names the header to send, verbatim as the vetting page quotes it
  PASS  vet: /healthz still exists and is still keyless, and is NOT the ping route

-- F2: per-staff feeds, the bookings CSV, webhooks from every door, the demo ribbon --
  PASS  feeds: a feed for one staff member carries only that person's appointments (got array (
  0 => 'FEEDA001',
  1 => 'FEEDA002',
), want array (
  0 => 'FEEDA001',
  1 => 'FEEDA002',
))
  PASS  feeds: the whole-team feed (0) still carries everybody's (got array (
  0 => 'FEEDA001',
  1 => 'FEEDB001',
  2 => 'FEEDA002',
), want array (
  0 => 'FEEDA001',
  1 => 'FEEDB001',
  2 => 'FEEDA002',
))
  PASS  feeds: a feed token stores whose diary it is, refuses an unknown staff id, and feed_serve reads it
  PASS  bookings CSV: one row per booking, local and UTC times, money as decimals
  PASS  bookings CSV: a customer name that a spreadsheet would run as a formula is neutralised
  PASS  bookings CSV: routed at /admin/appointments.csv and administrator-only (it carries customer contact details)
  PASS  webhooks: every event offered in Settings is sent by the app (got array (
  0 => 'appointment.cancelled',
  1 => 'appointment.created',
  2 => 'appointment.rescheduled',
  3 => 'payment.recorded',
), want array (
  0 => 'appointment.cancelled',
  1 => 'appointment.created',
  2 => 'appointment.rescheduled',
  3 => 'payment.recorded',
))
  PASS  webhooks: the booking page and the admin form send appointment.created; both cancel doors send appointment.cancelled; the customer's move sends appointment.rescheduled; a recorded payment sends payment.recorded
  PASS  demo ribbon: on the public booking pages it says what the page is, not "signed in as the admin"
  PASS  demo ribbon: below 600px it starts as the small pill unless the visitor opened it
  PASS  demo seed: today has bookings (the history stopped at yesterday, so "Today" was always empty)
  PASS  backups: every non-admin role gets 403 on every backup, restore and backup-token route
  PASS  backups: an administrator still gets the JSON backup (got 'GATE 200', want 'GATE 200')
  PASS  forms: every POST form on a screen reaches a registered route (35 checked)
  PASS  backup JSON: an administrator gets a backup, with every credential column planted (11)
  PASS  backup JSON: the download carries none of them
  PASS  backup JSON: the scheduled backup carries none of them
  PASS  backup JSON: the download and the scheduled writer read the one redaction list
================================================
  169 passed, 0 failed
================================================

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