# 10 — Test Strategy

## 1. Tooling

- **Pest 4** (unit + feature), Laravel testing helpers, `Livewire::test()`, `Http::fake()` + JSON fixtures recorded from provider docs/sandboxes (`tests/Fixtures/Providers/{provider}/*.json`).
- **MySQL 8.4 + Redis** in Docker for all feature tests. SQLite is not used: multi-database isolation and `SKIP LOCKED` must be exercised for real.
- **Larastan** level 8, **Pint**, `composer audit`.
- **Browser smoke tests** (Pest browser plugin / Dusk): signup → provisioning → compose → schedule (M3+).
- **k6** load tests for the dispatcher and composer endpoints (M6).

## 2. Tenant test harness

- `tests/Support/TenantFactory`: provisions a real tenant DB (`social_tenant_test_{token}`) through the real provisioning pipeline once per test class. Each test runs inside a transaction on both `master` and `tenant` connections, rolled back after the test.
- `tenantUrl($tenant, $path)` sets the session's current workspace; `inTenant($tenant, $user, fn)` runs code with auth and tenancy initialised.
- Parallel testing: `ParallelTesting::setUpProcess` creates a per-process DB prefix to avoid collisions.
- A global `afterEach` assertion checks that **no tenant is active** and the `tenant` connection config is empty after every test (catches context leaks).

## 3. Test suites mapped to spec §11

| Suite | Representative tests | Milestone |
|-------|---------------------|-----------|
| **Isolation** | Tenant A user on tenant B host → denied. Tenant DB credential cannot read master/other tenant (`SQLSTATE[42000]` asserted). Signed media URL for A rejected in B. Jobs for A then B on the same worker share no state. Cache keys are prefixed. Reports and exports are scoped | M1, M3, M5 |
| **Client scoping** | Unassigned client → 404 on every resource route (dataset over all routes via route introspection) | M2+ |
| **Onboarding** | Failure injected after each provisioning step → tenant inactive → retry succeeds. Duplicate slug race → one wins | M1 |
| **Auth/MFA** | 2FA mandatory roles, recovery codes, rate limits, password reset, session revoked on role change | M1 |
| **Authorization matrix** | role × permission × assignment dataset (doc 04) | M1–M6 |
| **Social connections** | State replay, PKCE, consent capture, refresh success/failure, revoked scope, reconnect, deauthorise webhook | M2 |
| **Bulk publishing** | 10 targets / 8 succeed / 2 fail. Job replay ×2 → no duplicates. Rate limit → release without attempt. Timeout-after-send → reconciliation | M3 |
| **Approvals** | Material vs non-material edits, superseding, unauthorised decision via a forged Livewire call, multi-stage + delegation | M3, M4 |
| **Scheduling** | DST gap/overlap (NY, London), `Asia/Kolkata`, client vs user timezone, restart recovery (truncate Redis mid-flight → sweeper restores) | M3, M4 |
| **Billing** | Paise arithmetic, GST split (intra/inter-state), suspended gating, over-limit gating, webhook replay | M6 |
| **Operations** | Context cleared after every job (including failures), webhook replay idempotent, audit entries for critical actions, log redaction (no token patterns in captured logs) | M1–M6 |
| **Reporting** | Unavailable metric → "not available" state. Freshness/provenance rendered. No fabricated zeros | M5 |
| **AI** | Budget enforcement, consent required, prompt contains only the allowed client context, no auto-publish path | M5 |
| **Architecture** | Pest arch tests: tenant models use the tenant connection; no `DB::connection('tenant')` in master domains; no `env()` outside config; Livewire components don't write via Eloquent directly; no `dd/dump` | M1+ |

## 4. Provider fakes

`FakeProvider` supports scripted outcomes per external account:
```php
FakeProvider::script('acct_9', [Outcome::validationError('caption_too_long')]);
FakeProvider::script('acct_10', [Outcome::transient(), Outcome::transient(), Outcome::authError()]);
FakeProvider::script('acct_3', [Outcome::timeoutAfterSend(externalId: 'p_123')]); // reconcile finds it
```
It records every publish call, so tests assert the exact call count (no duplicates). The boot guard prevents fakes outside `local`/`testing`.

Contract tests: each real adapter is tested against recorded fixtures for success and for each error class, so the normalisation into `ProviderException` subclasses is verified per provider.

## 5. CI gates (per PR)

1. `pint --test`
2. `phpstan analyse` (level 8)
3. `pest --parallel --coverage --min=80` (`app/Tenancy`, `app/Domain/Publishing` ≥ 90 %)
4. `composer audit`, `npm audit --omit=dev`
5. Migration check: fresh master + tenant migrations, then `tenants:migrate --pretend` on a seeded previous-release snapshot (expand/contract discipline)
6. Nightly: browser smoke suite, k6 baseline (from M6)
