Run this checklist against a production-like deployment over HTTPS with a fresh backup available. Record the date, application version, PHP/MariaDB versions, tester and evidence for each item. Do not record passwords, API keys or credential values in the evidence.
## Pre-flight and deployment
- [ ] The virtual host/document root is `public/`; the repository root, `.env`, SQL files and `storage/` are not web-accessible.
- [ ]`.env` is present outside the public root, has restrictive permissions (for example `chmod 600 .env`), and contains production-only values.
- [ ]`php bin/healthcheck.php` passes with no secret values printed.
- [ ] HTTPS is enabled and HTTP redirects to HTTPS; the certificate and hostname are valid.
- [ ] A backup was taken before UAT and its location/time is recorded separately from this checklist.
## Administrator
- [ ] Sign in with the bootstrap Administrator account; invalid credentials are rejected.
- [ ] The dashboard loads and the Administrator can view clients, jobcards, SLA data, reports and audit events.
- [ ] Create, deactivate and reactivate a test Accounts user and a test Technician user.
- [ ] Assign roles/permissions; verify an unauthorized permission is not granted by merely hiding a navigation link.
- [ ] Assign a Technician to a test jobcard and verify the assignment is visible in the expected workflow.
- [ ] Review audit events for login and test administrative changes; confirm timestamps and actor are present.
- [ ] Verify sensitive credentials are masked by default, access is permission-controlled, and reveal/access is audited (where that module is enabled).
## Accounts
- [ ] Create and edit a client, including contact details and preferred contact method.
- [ ] Add a primary contact and verify duplicate primary contacts are rejected.
- [ ] Create a jobcard with client, priority and requested work; verify its reference number and initial status.
- [ ] Assign a Technician, update the jobcard through the supported statuses, and verify status history.
- [ ] Configure an SLA agreement and verify allocated/used/remaining hours and period boundaries.
- [ ] View reports and export a report if permitted; verify exported data contains only intended fields and no internal notes or credentials.
- [ ] Verify validation errors are understandable and do not discard unrelated entered fields.
## Technician
- [ ] Sign in as a Technician and verify only assigned jobcards are accessible.
- [ ] Verify a Technician cannot access another Technician's jobcard by changing an ID in the URL or form payload.
Use distinct fixture IDs for each client and record expected/observed results without including secrets in evidence. The executable contract companion is `php -d assert.exception=1 tests/FinalScopeIntegrationTest.php`.
- [ ]**FS-01 — Technical information:** Add valid hosting/VPN/domain/database/SSH/API metadata; verify labels and usernames are trimmed, supported categories are enforced, control characters/oversized notes are rejected, and display output contains metadata only (never a credential secret).
- [ ]**FS-02 — Contact actions:** Edit a contact, promote a secondary contact, and delete a primary contact; verify duplicate names/emails are rejected, only same-client primary contacts are demoted, the lowest remaining same-client contact is promoted on primary deletion, and a client's sole contact cannot be deleted.
- [ ]**FS-03 — Time corrections:** Correct date, duration, notes and SLA-counting state while retaining the original time-entry ID, jobcard ID and technician ID. Verify ownership changes, corrections to voided entries, invalid ranges and missing void reasons are rejected. Confirm the original and correction/void actor are retained by the deployment's audit trail.
- [ ]**FS-04 — Custom roles:** Create a custom role, assign a least-privilege permission set, rename it and remove it; verify permissions are normalized/deduplicated, server-side authorization remains enforced on direct URLs/forms, and the Administrator role cannot be renamed, deleted or permission-edited.
- [ ]**FS-05 — Notifications:** Trigger assignment, status-change and SLA-threshold events; verify normalized per-user rows, stable deduplication, inactive/unknown recipients skipped, mark-read changes only the authenticated user's row, and notification bodies contain no credential or internal-note values.
- [ ]**FS-06 — Report audience separation:** Compare the same fixtures in client and internal reports/print/CSV output. Client audience must omit technician identity, internal notes, credentials and other operational-only fields; internal audience may retain authorized attribution. Direct report URLs and exports must enforce the same audience and role checks.
- [ ]**FS-07 — Technician scope:** With two technicians and two clients, verify each technician can list/view/update only assigned jobcards and sees only their own time totals. Changing jobcard, client, attachment, credential, report or time-entry IDs must return the documented not-found/denied response without leaking metadata or mutating another technician's records.
- [ ]**FS-08 — CSRF and method checks:** Submit missing and wrong CSRF tokens to login, logout, client/contact, jobcard/status/assignment/time, attachment, credential, SLA, notification and custom-role state changes; every request must be rejected before mutation (HTTP 419 or documented equivalent). Verify GET requests are read-only and logout is POST-only.
- [ ]**FS-09 — Attachment boundary:** Attempt traversal names, executable/double extensions, MIME/signature mismatches, oversized files and client-visible files without explicit approval; each must be rejected before storage. Upload a valid image/PDF and verify a generated server filename, validated MIME, `X-Content-Type-Options: nosniff`, no executable download behavior, and cross-client/jobcard access denial.
- [ ]**FS-10 — Credential boundary:** Create a canary credential and verify the database stores only `secret_ciphertext`, normal views show a mask, reveal is permission-controlled, client-bound, audited and returned with `Cache-Control: no-store`; a different client/credential ID cannot reveal it. Do not put the canary in screenshots, tickets or UAT evidence.
- [ ]**FS-11 — Production healthcheck:** Run `php bin/healthcheck.php` with valid configuration and capture exit status plus status-only output. Verify all current schema tables are probed, no password/APP_KEY/DSN/SQL exception/path is printed, and a disposable database missing one required table produces a non-zero exit. Run the same check after restore.
Record the request URL/route, authenticated role, test fixture IDs, expected response, observed response, and evidence for each case. Use separate Technician accounts and at least two clients/jobcards so an ID change cannot accidentally target the same tenant.
- [ ]**SEC-01 — Technician cross-client IDOR (read):** Technician A requests Technician B's jobcard URL and an attachment URL belonging to that jobcard. Both requests return the same not-found/denied behavior as an unknown ID, and no client name, jobcard details, attachment bytes or metadata are disclosed.
- [ ]**SEC-02 — Technician cross-client IDOR (write):** Technician A submits status, notes, assignment or time-entry payloads with Technician B's jobcard ID. CSRF-valid requests are still denied by authorization, and the target jobcard, assignments and time entries remain unchanged.
- [ ]**SEC-03 — Own-technician time isolation:** Create one assigned jobcard with time recorded by Technician A and Technician B. Technician A's report/UI/export contains only A's hours; it does not include B's hours or another client's totals. Repeat with a direct report URL and CSV export.
- [ ]**SEC-04 — Credential canonical storage:** Create a credential containing a unique canary secret. The database row has `secret_ciphertext` and no plaintext `secret` field/value; normal views show a mask; only an authorized reveal returns the secret once, with no-store headers and an audit event. A different client's credential ID cannot be revealed.
- [ ]**SEC-05 — Attachment safe metadata:** Attempt traversal names (`../x.pdf`), executable/double extensions (`invoice.php.jpg`), MIME mismatches, oversized files, and client-visible without explicit approval. Each is rejected before storage. A valid image/PDF is stored under a generated server name, served with its validated MIME and `X-Content-Type-Options: nosniff`, and remains inaccessible through a different jobcard/client ID.
- [ ]**SEC-06 — Healthcheck schema contract:** Run `php bin/healthcheck.php` with valid configuration and confirm every current schema table is probed, output contains statuses only, and no password, APP_KEY, database DSN, SQL exception, or secret value is printed. Remove/rename one required table in a disposable database and confirm a non-zero failure.
- [ ]**SEC-07 — CSRF route assumptions:** For login, logout, client create/edit/contact, jobcard create/update, time, assignment, attachment, credential and SLA POSTs, submit with a missing token and a wrong token. Every request is rejected before mutation (HTTP 419 or the documented equivalent); the valid-token control succeeds. GET requests do not mutate state.
- [ ]**SEC-08 — Migration coverage and restore:** Restore a pre-current-schema backup to an isolated database, run `database/upgrade.sql` once and a second time, and confirm feature tables/columns, permissions and the one-SLA-per-client constraint are present. Resolve/record duplicate SLA rows before the unique constraint step; rerun healthcheck and verify representative clients, jobcards, time entries, credentials and attachments.