# Unit and automated testing — CMMSchool CI4

This document gives you a **reusable prompt** (for AI or teammates) and a **repeatable process** for testing in this project.

---

## 1. How testing is set up here

| Item | Location / detail |
|------|-------------------|
| Runner | **PHPUnit 10** (`composer test` or `./vendor/bin/phpunit`) |
| Bootstrap | `vendor/codeigniter4/framework/system/Test/bootstrap.php` |
| Config | `phpunit.xml.dist` (paths: `HOMEPATH`, `CONFIGPATH`, `PUBLICPATH`, `app.baseURL`) |
| Test autoload | `Tests\Support\` → `tests/_support/` (`composer.json` `autoload-dev`) |
| Coverage | Configured in `phpunit.xml.dist` → `build/logs/` (needs **PCOV** or **Xdebug**) |

**Official CI4 guide:** [Testing](https://codeigniter.com/user_guide/testing/index.html) (overview, `CIUnitTestCase`, database, controller, feature tests).

---

## 2. Test types (when to use which)

| Type | Base / trait | Best for |
|------|----------------|----------|
| **Unit** | `CodeIgniter\Test\CIUnitTestCase` | Pure logic: helpers, libraries, small services, model methods **without** full HTTP or DB (or with mocks). |
| **Feature (HTTP)** | `CIUnitTestCase` + `FeatureTestTrait` | Full request/response: routes, filters, status codes, body (e.g. webhooks, APIs). |
| **Controller** | `CIUnitTestCase` + `ControllerTestTrait` | One controller + mocked request/response; good when you inject or override protected hooks (see `StripeWebhookControllerTest`). |
| **Database** | `CIUnitTestCase` + `DatabaseTestTrait` | Models + DB; needs working **`tests`** connection in `Database.php` + `phpunit.xml` env (or SQLite3 extension for default example). |

**Rule of thumb:** Prefer **fast, isolated unit tests** for new code; add **feature/controller** tests for HTTP boundaries and regressions.

---

## 3. Folder layout (recommended)

```
tests/
├── unit/              # Fast tests, minimal framework surface
├── feature/           # HTTP / integration-style (FeatureTestTrait)
├── database/          # DatabaseTestTrait (optional separate DB)
├── session/           # Session-related examples
└── _support/          # Doubles, seeds, migrations for tests only
    ├── Controllers/
    ├── Models/
    ├── Libraries/
    └── Database/
```

- Name test classes `*Test.php` (PHPUnit discovery).
- Keep **`tests/_support`** free of `*Test.php` classes so they are not executed as tests.

---

## 4. Reusable prompt (copy for AI or tickets)

Use this when asking someone (or an AI) to add tests **in this repo**:

```text
Context: CodeIgniter 4 app in cmmschoolupgrade. PHPUnit 10, bootstrap from CI4.
Tests live under tests/ with PSR-4 Tests\Support\ → tests/_support/.

Task: Add automated tests for: [DESCRIBE CLASS/METHOD/ROUTE — e.g. App\Libraries\Foo::bar or POST /webhook/stripe]

Requirements:
- Extend CodeIgniter\Test\CIUnitTestCase (and FeatureTestTrait or ControllerTestTrait only if HTTP is required).
- Mark test class @internal and final where appropriate.
- Do not require a real database unless using DatabaseTestTrait with a dedicated tests DB; prefer mocks or test doubles for external APIs (Stripe, SMTP).
- Match existing project patterns: see tests/feature/StripeWebhookFeatureTest.php and StripeWebhookControllerTest.php for HTTP + doubles.
- Run: ./vendor/bin/phpunit path/to/NewTest.php — all tests must pass.
- Keep changes scoped: no unrelated refactors; if the production class needs a small seam for testing (protected method or constructor injection), document it in a one-line comment.

Deliverables:
- New test file(s) under tests/unit/ or tests/feature/
- Any test doubles under tests/_support/ if needed
- Brief note in docs/UNIT-TESTING.md “Examples” section only if you add a new pattern worth documenting
```

**Short variant:**

```text
CMMSchool CI4 / PHPUnit 10. Write tests for [X] under tests/. Use CIUnitTestCase; use FeatureTestTrait only for full HTTP. Mock Stripe/DB. Follow StripeWebhook* tests. Run phpunit on new files only.
```

---

## 5. Process: how to perform unit testing (step by step)

### Phase A — One-time / environment

1. **Install dev dependencies:** `composer install`
2. **Optional — coverage:** Install **PCOV** or **Xdebug** for PHP; otherwise PHPUnit may warn “No code coverage driver”.
3. **Optional — DB tests:**  
   - Enable SQLite3 in PHP *or* configure `database.tests` in `app/Config/Database.php` and uncomment `<env>` entries in `phpunit.xml.dist` for a dedicated test database.  
   - Until then, you can skip or exclude `tests/database/ExampleDatabaseTest.php` locally.

### Phase B — Per feature or bugfix

1. **Choose the surface:** unit (class only) vs feature (route) vs controller (controller method).
2. **List behaviors:** happy path, validation errors, auth/403, missing config, external failure (mocked).
3. **Implement tests** in the right folder; add `_support` doubles if static APIs (e.g. Stripe) are hard to mock without a seam.
4. **Run locally:**
   ```bash
   cd cmmschoolupgrade
   ./vendor/bin/phpunit tests/unit/YourTest.php
   ./vendor/bin/phpunit tests/feature/
   composer test   # full suite
   ```
5. **CI (recommended):** Run `composer test` on every push/PR; fail the build on warnings if you tighten `phpunit.xml.dist`.

### Phase C — Maintenance

- When refactoring, run the **full** suite before merge.
- When adding routes, add at least one **feature** or **controller** test for critical paths (payments, auth, enrollment).

---

## 6. Commands reference

```bash
# All tests
composer test

# One file
./vendor/bin/phpunit tests/unit/HealthTest.php

# One method
./vendor/bin/phpunit --filter testMethodName tests/unit/HealthTest.php

# With coverage (if driver installed)
./vendor/bin/phpunit --coverage-text
```

Reports (if configured): `build/logs/testdox.html`, `build/logs/clover.xml`.

---

## 7. Examples in this repository

| Example | File | Pattern |
|---------|------|---------|
| Sanity / config | `tests/unit/HealthTest.php` | Basic `CIUnitTestCase` |
| Stripe webhook HTTP + real signature | `tests/feature/StripeWebhookFeatureTest.php` | `FeatureTestTrait`, `Factories::injectMock` for `Config\Stripe` |
| Stripe webhook + mocked Stripe/DB | `tests/feature/StripeWebhookControllerTest.php` | `ControllerTestTrait`, `Tests\Support\Controllers\StripeWebhookTestDouble` |
| **Admin / school owner (guest flows)** | `tests/feature/AdminHomeFeatureTest.php` | `admin/home/*` redirects, login page, empty-field validation (no DB auth) |
| **Admin sidebar modules (guest)** | `tests/feature/AdminModulesGuestFeatureTest.php` | Data provider: prospects, enrollment, users, teachers, classrooms, students, withdraws, studentParent, parentConcerns, classroster, activities, requests, internalMessage, events, emailTemplate, teacherMessage, schools → `admin/home/login`; Portals root → `admin/portals/adminAnnouncement`, then announcement → login |
| **TP portal (guest)** | `tests/feature/TpGuestFeatureTest.php` | `/` → `tp/te/login`; `tp/ur` chooser; teacher/parent login pages; `tp/pp/login` → `tp/pa/login`; `tp/studentEnrollment` → `tp/pa/login` with `redirect=`; protected UR/quests/incoming/reports/internalMessage/teacherMessage → `tp/te/login` |
| **Registration paper PDF route** | `tests/feature/RegistrationPaperFormFeatureTest.php` | Non-`.pdf` and traversal basename → 404 |
| **TP login validation** | `tests/feature/TpLoginValidationFeatureTest.php` | Empty POST on `tp/te/login` and `tp/pa/login` → form + errors (no DB auth) |
| **TP payment (no session)** | `tests/feature/TpPaymentGuestFeatureTest.php` | `GET tp/payment` → redirect to `studentEnrollment/registrationForm` |
| **404** | `tests/feature/NotFoundFeatureTest.php` | Unknown path → 404 (`My404`) |
| **Route inventory (guest smoke)** | `tests/feature/RouteInventoryGuestTest.php` | Discovers `GET`/`POST` from `RouteCollection`, maps regex patterns via `tests/_support/Http/RouteGuestSampler.php`, hits each path as guest; **skips** closure handlers, `POST webhook/stripe`, registration-form routes, `studentEnrollment/payment` (DB), `POST admin/events/setTimeZone` (empty body crashes app). Expects redirects / 2xx / 403 / 404 per area (admin AJAX often returns 200 without redirect). |
| DB (needs SQLite or test DB) | `tests/database/ExampleDatabaseTest.php` | `DatabaseTestTrait`; **`@requires extension sqlite3`** skips entire class if missing |

**Note:** `Admin\Home` calls `getLoginUserDetail('admin')` in `__construct`, which hits the database when `admin_user_id` or `school_owner_user_id` is set. Feature tests that simulate a logged-in admin therefore need a working **default** DB connection (or a refactored constructor); the current admin tests intentionally stay **guest-only** plus **validation** (no successful `authenticateLogin`).

---

## 8. Link from project docs

The main documentation hub is [README.md](README.md) — add a bullet under **Testing** pointing here if you extend the testing story.
