# Technical documentation — ui-testing

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/ui-testing.git`, `main` at `bcdafbf` (2025-05-24, Joshua G-D). Secret values are named by file and not repeated.

## Stack

| Item | Evidence |
|---|---|
| Language | PHP `^8.3` (`composer.json`, `README.md`) |
| Framework | CodeIgniter 4, `codeigniter4/framework` `^4.0`. Exact lock is not in git (`composer.lock` not present) |
| Database | MySQLi. `env` database `uitesting`, user `uitesting`, host `localhost`, password empty |
| Sessions | `CodeIgniter\Session\Handlers\DatabaseHandler`, table name `sessions`, cookie `ci_session`, match IP, regenerate and destroy |
| Front | Server-rendered PHP views, Bootstrap-style sidebar, jQuery DataTables on the Selenium dashboard |
| Browser automation | `php-webdriver/webdriver` `^1.15`, `symfony/process` `^7.2`, local geckodriver and chromedriver |
| Tests | PHPUnit `^9.1`, config `phpunit.xml.dist` |
| CI | Bitbucket Pipelines, image `ubuntu:20.04` |

Other Composer requirements: `mpdf/mpdf` `^8.1`, `phpoffice/phpspreadsheet` `^1.25`, `league/csv` `^9.16`, `orhanerday/open-ai` `^3.5`, `ext-simplexml`. Dev: `fakerphp/faker`, `mikey179/vfsstream`.

`README.md` lists `aws/aws-sdk-php`. It is not in `composer.json`. No OpenAI or AWS use was found under `app/`.

## Repository layout

```text
app/                 CodeIgniter application
  Commands/          monitor:system
  Config/            routes, filters, database, app
  Controllers/       Welcome, Users, Selenium*, inherited screens
  Database/          Migrations and Seeds
  Filters/           Authentication, Authorization
  Models/
  Views/
public/              index.php front controller; deploy.php is commented out
tests/               PHPUnit
docs/                lamp, selenium driver install, overview, controller pseudo-code
env                  environment template (copy to .env)
install_lamp.sh      experimental installer (README and script banner)
bitbucket-pipelines.yml
phpunit.xml.dist
sonar-project.properties
spark                CodeIgniter CLI
writable/            logs, cache, screenshots at runtime
```

`vendor/` is not in git. `builds/` exists and contained no files in a recursive search. `sonarqube_scan.bat` is not in this revision. The onboarding scan listed that bat file.

## Application structure

MVC. `app/Config/Routes.php` sets the default namespace `App\Controllers`, default controller `Home`, default method `index`, and `setAutoRoute(true)`. Explicit routes and auto-routed public methods both apply. `setTranslateURIDashes(false)`.

Global filters, in order, before the controller:

1. CSRF, with an except list that does not include the Selenium routes.
2. `isLoggedIn` (`App\Filters\Authentication`).
3. `isGranted` (`App\Filters\Authorization`).

After: debug toolbar, then CSRF again with a shorter except list.

Base URL template: `app.baseURL = 'https://uitesting.php8'`. Site title `UI Testing`. Logo `assets/img/Default_Logo_Expanded_Items.png`. `CI_ENVIRONMENT` in the template is `development`. `CI_DEVELOPER` is `jaco`.

## Data model

Migrations under `app/Database/Migrations/`, oldest first:

| Migration | Tables |
|---|---|
| `2022-11-20-124016_Users.php` | `users` (name, username, password, address, phone, role, active, `2fa_enabled` default 1) |
| `2022-11-20-125608_UserRole.php` | `user_role` |
| `2022-11-20-125818_UserAccess.php` | `user_access` |
| `2022-11-20-130307_UserMenu.php` | `user_menu` |
| `2022-11-20-130307_UserSubmenu.php` | submenu table (same timestamp as UserMenu) |
| `2022-11-25-102709_UserMenuCategory.php` | `user_menu_category` |
| `2024-03-19-000001_create_sessions_table.php` | `sessions` |
| `2024-09-12-123957_AddTwoFactorAuthenticationTable.php` | 2FA codes (`user_id`, `user_email`, `previous_code`, `new_code`, `status`) |
| `2024-09-18-132105_AddLoginHistoryTable.php` | `login_history` |
| `2024-09-20-084946_AddBlockedUsersListTable.php` | `blockedusers` |
| `2025-05-14-000001_create_selenium_tables.php` | `selenium_tests`, `selenium_schedules`, `selenium_test_logs`, `selenium_test_results` |

Selenium relationships in the migration:

- `selenium_tests.created_by` → `users.id` `ON DELETE CASCADE`.
- `selenium_tests.updated_by` → `users.id` `ON DELETE SET NULL`.
- `selenium_schedules.test_id` → `selenium_tests.id` `ON DELETE CASCADE`.
- `selenium_test_logs.test_id` → `selenium_tests.id` `ON DELETE CASCADE`.
- `selenium_test_logs.schedule_id` → `selenium_schedules.id` `ON DELETE SET NULL`.
- `selenium_test_logs.triggered_by` → `users.id` `ON DELETE SET NULL`.
- `selenium_test_results.test_log_id` → `selenium_test_logs.id` `ON DELETE CASCADE`.

Models declare `belongsTo` / `hasMany` against `App\Models\User`. The class in the tree is `UserModel`. CodeIgniter's `Model` does not provide `belongsTo` unless another layer adds it. Those relationship methods are not a working ORM. Provenance: **Implemented** as methods that will fail if called.

`SeleniumTest::$allowedFields` includes `browser`. The `selenium_tests` table has no `browser` column. The list filter checks `fieldExists` before filtering on it.

`SeleniumTestLog::$allowedFields` includes `browser`, `started_at`, `completed_at`. `SeleniumController` writes `end_time`, which will be rejected by allowed fields.

Seeds (`app/Database/Seeds/Users.php`) call `UserRole`, `UserMenuCategory`, `UserMenu`, `UserAccess`, then insert the developer user. Menu categories: General, Settings, Developers. Menus: Dashboard (`home`), Users, Menu Management, CRUD Generator, System Settings, Selenium Dashboard (`seleniumDashboard`).

Models that point at tables with no migration in this repo include `CompanyModel` (`companies`), `ClientsModel` (`clients`), `CronjobsModel` (`cronjob_log`), `FileModel` (`files`). `CronJobs` also queries `cronjob_actions`.

## HTTP surface (Selenium)

Explicit routes:

| Method | Path | Controller |
|---|---|---|
| GET | `seleniumDashboard` | `SeleniumTestController::dashboard` |
| GET | `seleniumTestController/index` | list |
| GET | `seleniumTestController/show/{id}` | show |
| POST | `seleniumTestController/create` | create |
| POST | `seleniumTestController/update/{id}` | update |
| DELETE | `seleniumTestController/delete/{id}` | delete |
| GET | `seleniumTestController/download/{id}` | download |
| GET | `seleniumTestController/statistics/{id}` | statistics |
| POST | `seleniumTestController/disable2fa` | stub |
| GET | `seleniumTestLogController/index` | list |
| GET | `seleniumTestLogController/view/{id}` | **no method** (controller has `show`) |
| DELETE | `seleniumTestLogController/delete/{id}` | delete |
| GET | `seleniumTestLogController/getResults/{id}` | **no method** |
| GET | `seleniumTestResultController/index` | list |
| GET | `seleniumTestResultController/view/{id}` | **no method** (controller has `show`) |
| GET | `seleniumTestResultController/getErrorSummary` | **no method** (controller has `errorSummary`) |
| * | `seleniumScheduleController/...` | index, view (**missing**), create, update, delete, changeStatus (**missing**) |

`SeleniumController` has no explicit routes. Auto-routing exposes `execute`, `cancel`, and `status/{logId}`. The dashboard calls `execute/{testId}` and `executeAll`.

JSON list and mutation methods return 400 when `isAJAX()` is false, except `status`, which checks the `X-Requested-With` header itself.

## Runner process

`SeleniumController` (extends `BaseController`, not `ResourceController`):

- `basePort` 4444, `serverTimeout` 30 seconds.
- `startGeckoDriver` / `startChromeDriver` via Symfony `Process`.
- `findAvailablePort` walks upward while `isPortAvailable` is false.
- `initializeWebDriver` builds `RemoteWebDriver` against `http://localhost:{port}`.
- `configureBrowser` sets timeouts and a 1920×1080 window.
- `executeTestSteps` reads `$sideData[0]['commands']` only.
- `takeScreenshot` writes `writable/screenshots/log_{id}_{browser}_{timestamp}.png`.
- Destructor and `quitWebDriver` stop the driver and the process recorded for that port.

`execute` reads `$test['test_file']` and updates `end_time`. Both disagree with the schema. `executeTestSteps` and `executeCommand` interpolate `$logId` without a parameter. Provenance: **Implemented** defects.

## Auth

Passwords are `password_hash` / checked in `Welcome` (the check itself is in the login branch; the seed uses `PASSWORD_DEFAULT`).

Session keys set in `setLoginSession` include `isLoggedIn`. 2FA success sets `isLoggedIn` in `confirmCode`.

Authorisation is menu-row based, not a permission string list. See `business-rules.md` BR-001 and BR-002.

`encryption.key` is set in the committed `env` template. Treat it as a secret to rotate. It is not repeated here.

`phpunit.xml.dist` contains `database.tests.password`. Treat it as a secret to rotate. Tests point at database `uitesting` on localhost, user `uitesting`, migrations disabled (`database.tests.migrations` false).

## Background work

| Mechanism | What it does | Selenium? |
|---|---|---|
| `php spark monitor:system` | Infinite loop, `top` every 5 seconds, PID file `writable/monitoring.pid`, `posix_kill` | No |
| `GET cronJobs` | Inherited job dispatcher, clock gate, production key constant | No. Missing tables and classes |
| `selenium_schedules` | Rows only | No worker |

There is no queue library in `composer.json`.

## Integrations

| System | Where | Notes |
|---|---|---|
| MySQL | `env`, `app/Config/Database.php` | Primary store |
| geckodriver 0.36.0 | `docs/selenium_setup.md` | Installed to `/usr/bin/geckodriver` by the doc's commands |
| chromedriver | same doc | Version taken from `google-chrome --version`, Chrome for Testing download |
| Firefox / Chrome | same doc | Mozilla APT repo and Google Chrome stable |
| SonarQube | pipeline pipe `sonarsource/sonarqube-scan:3.0.2` | `SONAR_HOST_URL`, `SONAR_TOKEN`. `EXTRA_ARGS` forces embedded Java |
| detect-secrets | pipeline pip install | Artifacts `secrets-report.json` and `.txt`. Scan command is `\|\| true` |
| OWASP Dependency-Check 10.0.4 | YAML step `&dependency-scan` | Not called from `pipelines.default` |
| Mailgun | YAML step `&send-email` | Not called from `pipelines.default`. Variables `MAILGUN_API_KEY`, `MAILGUN_DOMAIN`, `FROM_EMAIL`, `TO_EMAIL` |
| SMTP | commented keys in `env` | Used if password reset or 2FA mail helpers run |

`install_lamp.sh` is a bash installer for the LAMP stack. `README.md` calls the setup path `docs/lamp_setup.md` first, then `docs/selenium_setup.md`. The script banner says "UI Testing Installation". `docs/lamp_setup.md` assumes Debian or Ubuntu, SSH, root, 2 GB RAM, and removes snapd.

`public/deploy.php` does nothing when requested: the token check, IP allowlist, and `git pull` are inside a block comment. A commented project label in that file says `SAWIS: Label`, which matches the "cross site contamination" cleanup commits.

## Security notes (observations, not a review)

- Login is session-based. Selenium JSON routes skip menu checks and still require a session.
- `cronJobs` skips both login and menu checks. In non-production the key check is skipped. The clock gate still exits outside weekday 08:00–22:59.
- Screenshot directory is created mode `0777`.
- `Authentication` logs session contents, cookies, and headers at debug. That will include session data in the log file when the logger level allows debug.
- Committed secrets to rotate: `env` encryption key, PHPUnit database password, `CronJobs` key constant, commented deploy token, seeder password literal.
- Pipeline detect-secrets is the in-repo control for that class of finding. This document does not record scan results.

## Development and test process

Documented in `README.md`:

1. `composer install` (the README also says `php composer install`).
2. Copy `env` to `.env`.
3. `php spark db:create`, `php spark migrate`, `php spark db:seed Users`, `php spark key:generate`.
4. `php spark serve`, or an Apache vhost. The vhost example still uses `adminOverdrive.local` and `c:/wamp64/www/adminOverdrive/public`.

Tests: `composer test` or `./phpunit`. `tests/README.md` is the CodeIgniter testing quick-start and still mentions PHPUnit 8.5, while `composer.json` requires PHPUnit 9. Selenium controller tests use `FeatureTestTrait` and `SeleniumTestTrait` and reference a live database through `phpunit.xml.dist`.

The Bitbucket default pipeline does not run those tests. Clone depth is `full`.

## Configuration files that matter

| File | Role |
|---|---|
| `env` | Environment template. Not all keys are commented; base URL, database, session, and encryption key are active lines |
| `app/Config/Routes.php` | HTTP map. Trailing comment describes an organisations API that has no controller |
| `app/Config/Filters.php` | Auth, authz, CSRF |
| `app/Config/Database.php` | Connection groups, including tests |
| `phpunit.xml.dist` | Test bootstrap and test database |
| `sonar-project.properties` | One line, the project key |
| `bitbucket-pipelines.yml` | CI |

## Source docs and how they differ from the code

| File | Use it for | Do not treat as current behaviour |
|---|---|---|
| `README.md` | PHP extensions, spark setup commands, feature list of auth and menus | Product title, AWS SDK, vhost path `adminOverdrive`, Jira/Confluence links (not verified) |
| `docs/lamp_setup.md` | Debian/Ubuntu package steps | — |
| `docs/selenium_setup.md` | Firefox, Chrome, driver install commands and the Mozilla key fingerprint printed in that file | — |
| `docs/selenium-controllers.md` | Intended shape of the runner | Method signatures, cancellation loop, schedule cron on the controller |
| `docs/overview.md` | Directory map | Product name Transactflow, PHP `^8.0`, "likely" feature list inferred only from Composer |

## Operations

**Unknown / Requires Confirmation:** deploy target, process manager for `monitor:system`, cron entry if any, and whether `https://uitesting.php8` resolves. Nothing in git defines a production host beyond the `env` base URL and the inert `deploy.php`.
