# Testing

Source: https://docs.externa.qiick.io/docs/testing

Externa uses **Pest 4** with the Laravel and browser plugins. Feature tests refresh the database; prefer focused suites that mirror the domain folders under `tests/Feature`.

## Layout

```text
tests/
  Pest.php              # binds TestCase + RefreshDatabase; grantAiPermissions()
  TestCase.php
  Unit/                 # no RefreshDatabase by default
  Feature/
    Admin/              # users, groups, roles, files, activity
    Ai/                 # assistant, tools, webhook, attachments
    Auth/               # Fortify / verification flows
    Authorization/      # permission sync, super-admin gate, resolver
    Collections/        # collections, fields, items, normalizer
    Settings/           # account / profile settings
    DashboardTest.php
    NotificationsTest.php
    CreateSuperAdminSeederTest.php
    …
  Browser/              # pest-plugin-browser (Playwright)
    FileManagerBrowserTest.php
```

Configured in `tests/Pest.php`:

| Suite     | Base             | Traits            |
| --------- | ---------------- | ----------------- |
| `Feature` | `Tests\TestCase` | `RefreshDatabase` |
| `Browser` | `Tests\TestCase` | `RefreshDatabase` |
| `Unit`    | `Tests\TestCase` | _(none)_          |

### Domain folders (what lives where)

| Folder                  | Covers                                                                                                     |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `Feature/Admin`         | User/group/role/permission management, file manager JSON, zip/duplicate async, thumbnails, activity log UI |
| `Feature/Api`           | Public CMS API (`/api/v1`) — public role grants, API keys, IP allowlist, per-action deny/allow             |
| `Feature/Ai`            | Chat, attachments, tool permission matrix, import remote JSON, sync/webhook phases, multi-role smoke       |
| `Feature/Collections`   | Collection CRUD, item data normalizer, management flows                                                    |
| `Feature/Auth`          | Login, registration, verification, 2FA-related behaviour                                                   |
| `Feature/Authorization` | `permissions:sync`, effective permissions, `Gate::before` super-admin                                      |
| `Feature/Settings`      | Profile / password / appearance settings pages                                                             |
| `Browser`               | Real browser interactions (file manager drive-like UX)                                                     |

## Browser plugin notes

Dependencies: `pestphp/pest-plugin-browser` and `playwright` (npm).

- Browser tests live under `tests/Browser/`, not `Feature/`.
- Default `phpunit.xml` / `composer test` run **Unit + Feature** only — Browser is **not** in the main Pest gate.
- CI runs a dedicated **`browser`** job on `FileManagerBrowserTest.php` with `continue-on-error: true` (fail-soft) so flakes do not block merges — see `.github/workflows/tests.yml`.
- Feature CI also covers **`ci-mysql`**, **`ci-mariadb`**, and **`ci-pgsql`** (PHP 8.4) alongside the SQLite `ci` matrix — see [Supported databases](/docs/supported-databases).
- They use the same factories/seeders patterns as feature tests (e.g. seed `PermissionSeeder`, grant a disposable role with exact permission names).
- Prefer `data-testid` / `data-test` selectors over visible copy (i18n).
- Require a working Playwright install (`npx playwright install` if browsers are missing) and **Node 24**.
- Slower than HTTP feature tests — keep them for interactions that need real DOM/Inertia behaviour (drag/select, panels), not for every CRUD path.

Example helper pattern (see `FileManagerBrowserTest.php`): create a unique role per test, `syncPermissions`, assign to the user, then drive the UI.

```bash
# Isolated File Manager browser suite (local)
./vendor/bin/pest tests/Browser/FileManagerBrowserTest.php
```

## How to run tests

From `externa-core`:

```bash
# Full gate used in CI-style scripts (clears config, Pint --test, then Pest)
composer test

# Direct Pest / Artisan
php artisan test
php artisan test --filter=FileManagerTest
php artisan test tests/Feature/Ai
php artisan test tests/Browser

# Frontend static checks (also part of composer ci:check)
npm run lint:check
npm run format:check
npm run types:check
```

`composer test` runs:

1. `php artisan config:clear`
2. `pint --parallel --test`
3. `php artisan test`

> **PHP version**
> The app targets **PHP 8.4+** (`composer.json` `^8.4`). Run tests with the same CLI PHP your Herd/FPM stack uses so platform checks match Composer.

### Helpers

`grantAiPermissions(User $user, array $permissions): User` in `tests/Pest.php` creates a unique role, syncs the given permission names, and assigns it — safe for parallel/isolation-sensitive AI tests.

## Raw SQL

New Feature/Unit tests that exercise raw SQL paths should assert behaviour through public APIs, not paste ad-hoc `DB::select` into the test unless the production call site is already allowlisted. Contributor rule and PHPStan allowlist: [Contributing — Raw SQL allowlist](/docs/contributing#raw-sql-allowlist-core-208). Marker: `// raw-sql: reviewed` next to reviewed production calls.

## AuthZ regression matrix

Table-driven Public API / GraphQL / file ACL coverage lives in `tests/Feature/Api/AuthzRegressionMatrixTest.php` ([core#114](https://github.com/qiick-io/externa-core/issues/114)). Prefer extending those Pest datasets when adding a new actor × action cell. Surface map: [AuthZ map](/docs/authz-map#master-matrix).

```bash
herd php artisan test --filter=AuthzRegressionMatrix
```

## What to cover for new features

| Change                    | Minimum tests                                                                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| New permission            | Sync includes it; route/FormRequest **403** without it and **200/allowed** with it; UI gate optional via browser or Inertia assert    |
| New admin/files route     | Feature test for happy path + permission denial; async path asserts job dispatched / worker-friendly outcome                          |
| New collections behaviour | Feature test for store/update/validation; normalizer cases for new field shapes                                                       |
| New field type            | Normalizer + validation rules; create field via HTTP or AI tool; UI smoke if non-trivial                                              |
| New AI tool               | Permission denied string; success JSON with `"ok": true`; register presence in `AppAssistant` for a user who has the gate permissions |
| Webhook / import          | Token rejection (empty/wrong); successful import summary; throttle not required in unit tests                                         |
| Seeder / config defaults  | Assert super-admin email/password from `config/super_admin.php` when touching seeders                                                 |
| Browser-only UX           | One Browser test for the interaction that HTTP tests cannot express                                                                   |

Prefer **one focused Pest file** next to the domain folder over a giant catch-all. Reuse factories and seed `PermissionSeeder` (or full `DatabaseSeeder`) instead of hand-inserting permission rows.

## Related

- [Contributing](/docs/contributing) — lint/format before push
- [Extending Externa](/docs/extending) — end-to-end checklists that include tests
- [Operations](/docs/operations) — queue/scheduler needed when testing async jobs locally (`composer run dev` or a separate `queue:listen`)
- [Post-v1 polish smoke](/docs/post-v1-polish-smoke) — optional signed-off Pest/Playwright checklist (internal)
