# Business rules — ui-testing

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/ui-testing.git`, `main` at `bcdafbf`.

No business-rules document existed in the source repository or in Silicon Ark. Rules below are taken from code, seeds, and docs. Each rule is marked **Documented**, **Implemented**, or **Inferred**. Where the UI, the controller, and the database disagree, the conflict is recorded and is not resolved into a single rule.

Secret values are not copied.

## Identity and access

### BR-001 — Session required

**Implemented.** A request must have `session.isLoggedIn === true`, except the paths listed in `app/Config/Filters.php` (`/`, `GetLogin`, register, organisations, tracking-pixel, 404, `cronJobs`, 2FA, forgot-password, reset-password).

Failure: redirect to `/`.

Evidence: `app/Filters/Authentication.php`.

### BR-002 — Menu permission

**Implemented.** If the first URI segment is not in the `isGranted` except list, it must match a menu URL, and the session role must have a `user_access` row for that menu.

Failure: AJAX JSON 404 or 403; otherwise redirect to `/` or `blocked`.

Selenium JSON prefixes (`seleniumTestController`, `seleniumController`, `seleniumScheduleController`, `seleniumTestLogController`, `seleniumTestResultController`) skip this check. They do not skip BR-001.

Evidence: `app/Filters/Authorization.php`, `Filters.php`.

### BR-003 — Seeded roles

**Implemented** seed data, not a written policy.

Roles inserted in order: Developer, Admin, Company Admin, Company User. Only Developer (`role_id` 1) receives `user_access` rows.

**Unknown / Requires Confirmation:** whether Admin and company roles are still meant to be used. No access seed exists for them.

### BR-004 — Seeded operator

**Implemented.** `php spark db:seed Users` creates username `admin@overdrive.co.za`, role Developer, active. The password is hashed from a short literal in the seeder.

**Requires Confirmation:** this is a development seed, not a production account policy. The literal should be rotated and not reused.

### BR-005 — Two-factor flag defaults on

**Implemented.** Column `users.2fa_enabled` defaults to 1. `Welcome` checks the flag after a successful password and can refuse `isLoggedIn` until an email code is confirmed.

**Unknown:** whether operators are expected to leave 2FA on. SMTP is not filled in `env`.

Evidence: users migration, `Welcome::index`, `Welcome::confirmCode`.

### BR-006 — Blocked IP

**Implemented.** `Welcome` constructor loads `BlockedUsersModel` and checks the request IP. The blocked-list screen is `system_settings` via `getBlockedUsers` and `changeBlockedActive`.

**Inferred:** a blocked IP should not reach a normal login. The exact session flag set in the constructor was not copied here. Evidence: `Welcome.php` lines that call `getBlockedIP`.

### BR-007 — CSRF

**Implemented.** The CSRF filter runs on every request except the URI list in `Filters.php`. Selenium create/update/execute routes are not on that list. The dashboard sends a CSRF field on upload, run, run-all, and delete.

## Selenium tests

### BR-008 — Test name

**Implemented** in both the controller and `SeleniumTest` validation.

Name is required, 3 to 255 characters. Update rejects a name that another row already has. Create does not perform that duplicate check.

### BR-009 — Side file

**Implemented** on create.

- Field name expected by the controller: `side_file`.
- Max size 10240 (CodeIgniter `max_size`, kilobytes).
- Extension `side` or `json`.
- Empty file rejected.
- Body must pass `isValidSideFile`.
- Stored in `file_contents`. `file_size` is the upload size, and the model `beforeInsert` overwrites `file_size` with `strlen` of the contents when that key is present.

**Discrepancy:** the dashboard file input is named `sidefile`. **Requires Confirmation** which name is intended. Until one side changes, the dashboard upload does not satisfy this rule.

### BR-010 — Active flag is the delete

**Implemented.** `SeleniumTest::delete`, `SeleniumSchedule::delete`, and `SeleniumTestLog::delete` set `is_active` to 0 unless the caller passes purge. List filters treat `status=active` as `is_active` 1 and `inactive` as 0.

**Inferred:** "delete" in the UI means hide, not remove the row. The controller delete methods were not all checked for a purge flag.

### BR-011 — Who created the test

**Implemented.** Create requires session `user_id` and stores it in `created_by` and `updated_by`. The column is a foreign key to `users.id` with `ON DELETE CASCADE` for `created_by`.

**Inferred:** deleting a user deletes the tests they created. That cascade is in the migration. No product statement says user deletion should destroy tests.

### BR-012 — One running log per test

**Implemented** on `SeleniumTestLog::startLog`. A second start throws "Test is already running" when an active row is `running`.

**Not applied** by `SeleniumController::execute`, which does not call `startLog`.

**Documented** in `docs/selenium-controllers.md` as a check inside `execute`. The document and the runner disagree.

### BR-013 — Browsers

**Implemented** enum on schedules, logs, and results: `firefox`, `chrome`, `both`. Default `firefox`.

`SeleniumController::initializeWebDriver` accepts only `firefox` and `chrome`. Passing `both` throws "Unsupported browser".

**Inferred** from the enum comment: `both` means run the test in each browser. **Not implemented** as two sequential runs. **Requires Confirmation.**

### BR-014 — Command set

**Implemented.** Only these WebDriver commands are accepted: `open`, `click`, `type`, `select`, `assert`, `verify`, `waitForElementPresent`, `waitForElementVisible`, `waitForElementNotVisible`.

`select` chooses by visible text.

Locators: `id=`, `name=`, `css=`, `xpath=`, `linkText=`. Any other string is a CSS selector.

A failed step stops the rest of `tests[0].commands`. Later tests in the `.side` file are not walked. **Inferred** that only the first test in the file is in scope. The pseudo-code doc says "for each command in sideData" and does not say "first test only".

### BR-015 — Assertions do not match Selenium IDE names

**Implemented**, and in conflict with normal `.side` files.

`handleAssertion` compares page title when `target` starts with `title`. It also handles `assertText`, `verifyText`, `assertValue`, `verifyValue`, and element present / not present. Those names are not accepted by the outer command switch, which only lists `assert` and `verify`.

**Requires Confirmation:** which command names the product is supposed to run. As written, a Selenium IDE `assertText` step fails as an unsupported command.

### BR-016 — Timeouts and window

**Implemented.** Page load 30 seconds, script timeout 30 seconds, implicit wait 10 seconds, explicit wait 10 seconds, window 1920×1080 for both browsers. Screenshot directory `writable/screenshots/`, created as mode `0777`.

### BR-017 — Expected runtime

**Implemented** as a nullable column "Expected runtime in seconds", and as `calculateExpectedRuntime` / `measureTestRuntime` on the test controller. Create does not write the column.

**Inferred** from the migration comment and from `SeleniumTest::canScheduleAt`: runtime was meant to size a schedule window. That link is not wired.

## Schedules

### BR-018 — Cron shape on the model

**Implemented** on `SeleniumSchedule`, not on the controller.

A cron expression must be five fields. Each field is checked with a regular expression. Ranges must be numeric and ascending after month and weekday names are converted. Browsers must be the enum in BR-013. `name`, `test_id`, `timezone`, `created_by`, and `is_active` are required by model validation.

`beforeInsert` throws if the cron string or browser value fails.

### BR-019 — Controller schedule shape

**Implemented** on `SeleniumScheduleController::create` and `update`, and **in conflict** with BR-018.

The controller requires `frequency` of `once`, `daily`, `weekly`, or `monthly`, plus `start_time` and optional `end_time`. It calls `hasConflict`, which is not a method on the model. It does not send `name`, `cron_expression`, or `created_by`.

**Requires Confirmation:** schedules are defined as cron (model, migration, pseudo-code doc) or as a frequency enum (controller). Both cannot be inserted as specified.

### BR-020 — Overlapping runs against the same URL

**Implemented** and unused.

If `target_url` is empty, `canScheduleAt` returns true (no conflict). If it is set, active schedules for other tests with the same URL conflict when `[start_time, end_time]` overlaps `start_date`/`end_date`. A schedule with a null start or end date is skipped and does not count as a conflict.

Upload never sets `target_url`, so this rule does not fire on the current create path.

### BR-021 — No automatic start

**Inferred** from absence. No command, filter, or `CronJobs` method queries `selenium_schedules` and calls the runner.

The HTTP cron endpoint is a different rule (BR-025) and does not mention Selenium.

**Requires Confirmation:** whether schedules are supposed to run unattended.

## Logs and results

### BR-022 — Log status

**Implemented** enum: `pending`, `running`, `completed`, `failed`, `cancelled`. Default `pending`.

`startLog` writes `running` and `started_at`. `completeLog` allows only `completed` or `failed`, and sets `completed_at`. `cancelLog` sets `cancelled` only when the model object's `status` property is `running` (it does not load the row by id first).

`SeleniumController::cancel` requires a JSON body whose `status` is `running`, then writes `cancelled` and `end_time`. `end_time` is not an allowed field. The cancel method also checks `status === 'cancelled'` after already returning when status is not `running`, so that second branch does not run.

### BR-023 — Step status

**Implemented** enum: `passed`, `failed`, `skipped`. Default `passed`. No writer of `skipped` was found in `SeleniumController`. **Inferred:** `skipped` is unused.

`execution_time` is a datetime of the step, not a duration, despite the name. Duration for a log is computed from `started_at` and `completed_at` in `SeleniumTestLog::calculateDurationForLog`.

### BR-024 — Screenshots

**Implemented.** A PNG is taken for commands `assert`, `verify`, and `waitForElementPresent`, and again when a step throws. The stored value is the file name, not a full URL. `getScreenshot` on the result controller is the reader. It was not line-traced.

## Inherited rules that are not Selenium policy

### BR-025 — Cron HTTP window

**Implemented** in `CronJobs::index`. The script exits on Saturday and Sunday, before 08:00, and from 23:00 onward. A comment says the evening bound was changed to 23:00 by "Wessels". In production the query parameter `key` must match a constant in that file.

This does not schedule Selenium. The tables it reads are not created by migrations in this repository.

**Inferred** leftover from another product. **Requires Confirmation** before treating the clock window as a rule of UI Testing.

### BR-026 — Company roles

**Implemented** as role names only. `Welcome::setCompanyUsersAndAdminSessionVars` exists. `CompanyModel` uses table `companies`, which has no migration here.

**Unknown** whether company scoping is in force.

### BR-027 — Monitoring thresholds

**Implemented** defaults in `MonitorSystemUsage::getThresholdValues`: CPU 5, memory 50. The comment says the values are hardcoded for now. `ServerMonitoring::updateAlertThresholds` is a separate HTTP path.

**Requires Confirmation** which threshold source is intended. A CPU alert at 5% will fire on a normal machine. That may be a defect, not a rule.

## Documented rules that the code does not enforce

| Documented in `docs/selenium-controllers.md` | Code |
|---|---|
| `execute(logId)` loads the log, refuses an already-running log, sets status running, runs every browser | `execute()` reads a JSON body, does not create a log, updates `end_time` |
| Cancellation is checked on each command | `isTestCancelled` is never called from the loop |
| Schedule create validates a cron expression | Controller validates `frequency`; model validates cron; they are not connected |
| Result `failed(testRunId)` | Method `failed()` takes no id argument |

`docs/overview.md` states PHP `^8.0` and that PDF, Excel, and CSV features are "likely" present because those Composer packages are required. Package presence is not a business rule. Those packages are required. Call sites were not established as user-facing Selenium behaviour.
