# Testing

Packstub Tenancy ships a `TenantTestHelpers` trait for writing tenant-aware tests in your own application, and the package itself carries two suites you can run: a dependency-free default suite that runs entirely on SQLite and a multi-server end-to-end suite that provisions real tenants across three real PostgreSQL server instances. This page covers how to set up your test harness, how to use the helpers, how to test database pool placement, and how to run the package's own suites.

## Setting up your test harness

Two things make tenant tests fast and reliable: a **sync queue** and **SQLite tenant databases**.

Tenant provisioning (`CreateDatabase` → `MigrateDatabase` → `MarkTenantReady`) runs on the queue and is dispatched after the surrounding transaction commits. With `QUEUE_CONNECTION=sync`, the whole pipeline runs inline — by the time `Tenant::create()` returns, the tenant database exists, is migrated, and the tenant's status is `ready`. Set it in your `phpunit.xml`:

```xml
<php>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
</php>
```

> **Warning:** Don't `Queue::fake()` in tests that need a working tenant. A faked queue swallows the provisioning jobs, the tenant stays in `provisioning` status forever, and the `EnsureTenantIsReady` middleware will redirect every panel request to the provisioning page.

With an SQLite template connection, Stancl's `SQLiteDatabaseManager` creates each tenant database as a `.sqlite` file — by default inside `database_path()`, where files collide across test runs and pollute your repository. Redirect them to a per-process temporary directory instead, and always end tenancy before teardown so the framework's rollback queries hit the central database, not a tenant database that may already be gone:

```php
use Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager;
use Tests\TestCase as BaseTestCase;

abstract class TenantTestCase extends BaseTestCase
{
    protected string $tenantDbPath;

    protected function setUp(): void
    {
        parent::setUp();

        // Isolate tenant SQLite files to a per-process temp dir so parallel
        // and repeated runs never collide, and nothing leaks into database/.
        $this->tenantDbPath = sys_get_temp_dir().'/tenant-tests-'.getmypid();

        if (! is_dir($this->tenantDbPath)) {
            mkdir($this->tenantDbPath, 0777, true);
        }

        SQLiteDatabaseManager::$path = $this->tenantDbPath;
    }

    protected function tearDown(): void
    {
        // Any test that entered a tenant leaves the default connection pointed
        // at that tenant's database. Revert to the central context BEFORE the
        // framework rolls anything back.
        if (tenancy()->initialized) {
            tenancy()->end();
        }

        foreach (glob($this->tenantDbPath.'/*') ?: [] as $file) {
            @unlink($file);
        }

        parent::tearDown();
    }
}
```

This is the same pattern the package's own `tests/TestCase.php` uses. `SQLiteDatabaseManager::$path` is a static property, so setting it in `setUp()` affects every tenant database created during that test.

If your app's tenant seeder touches app-specific tables, either leave `packstub-tenancy.seeder` at its `null` default in tests (the seed step is skipped entirely) or point it at a tenant-safe seeder.

## The `TenantTestHelpers` trait

Add `Packstub\Tenancy\Testing\TenantTestHelpers` to any test class that needs tenants:

```php
use Packstub\Tenancy\Testing\TenantTestHelpers;
use Tests\TenantTestCase;

class InvoiceTest extends TenantTestCase
{
    use TenantTestHelpers;

    public function test_an_owner_sees_their_invoices(): void
    {
        [$tenant, $user] = $this->createTenantWithUser();

        $this->actingAsTenant($tenant, $user);

        // You are now authenticated as $user, inside $tenant's database.
        // Every Eloquent query on the default connection hits the tenant DB.

        $this->leaveTenant();
    }
}
```

In Pest, the trait works the same way — bind it with `uses()` and call the helpers on `$this`:

```php
use Packstub\Tenancy\Testing\TenantTestHelpers;

uses(Tests\TenantTestCase::class, TenantTestHelpers::class);

it('creates ready tenants', function () {
    $tenant = $this->createTenant(['name' => 'Acme', 'slug' => 'acme']);

    expect($tenant->fresh()->status)->toBe('ready');
});
```

### `createTenant(array $attributes = [])`

Creates a tenant using your configured `packstub-tenancy.tenant_model` — so if you point the config at your own model class, the helper returns instances of it. Defaults: `name` is `Test Tenant`, `slug` is `test-tenant-` plus a unique suffix; anything you pass in `$attributes` overrides the defaults. On a sync queue, the returned tenant is fully provisioned.

```php
$tenant = $this->createTenant(['name' => 'Acme', 'slug' => 'acme']);
```

The helper also creates a domain record (`{slug}.{central_domain}`) for the tenant. It does this **regardless of your identification mode** — unlike the onboarding flow, which skips domain creation in path mode. This is a deliberate test convenience: the extra domain row is inert in path mode, and always creating it means the same helper works for subdomain-identified tests without branching. If a test asserts on your tenant's domain count in path mode, account for this row.

### `createTenantWithUser(array $tenantAttributes = [], array $userAttributes = [])`

Creates a tenant, then a user via your configured `auth.providers.users.model`, and attaches the user to the tenant with the `owner` role. Returns a `[$tenant, $user]` pair for destructuring. User defaults: name `Test User`, a unique `@example.com` email, and password `password`.

```php
[$tenant, $user] = $this->createTenantWithUser(
    ['name' => 'Acme'],
    ['email' => 'jane@example.com'],
);
```

### `actingAsTenant(Tenant $tenant, ?Authenticatable $user = null)`

Authenticates as `$user` (when given) and enters the tenant's context via Stancl v4's `$tenant->enter()`. From this point the default database connection targets the tenant's database. Returns `$this`, so it chains with other test methods.

### `leaveTenant()`

Ends tenancy (`tenancy()->end()`) and returns to the central context. Call it whenever a test needs to assert against central data after working inside a tenant — and rely on the base-class `tearDown()` shown above as the safety net for tests that forget.

## Testing database pool placement

If you use [horizontal scaling](https://packstub.dev/docs/filament-tenancy/horizontal-scaling), you can test placement behavior without any real database servers: pool members are ordinary connections from `config/database.php`, so in-memory SQLite connections work fine. Define fake pool members and enable the pool in `setUp()`:

```php
use Packstub\Tenancy\Database\DatabasePool;

protected function setUp(): void
{
    parent::setUp();

    foreach (['tenant_pool_a', 'tenant_pool_b'] as $name) {
        config()->set("database.connections.{$name}", [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }

    config()->set('packstub-tenancy.database_pool', [
        'enabled' => true,
        'connections' => ['tenant_pool_a', 'tenant_pool_b'],
        'strategy' => DatabasePool::STRATEGY_LEAST_TENANTS,
        'weights' => [],
    ]);
}
```

Then assert on the persisted assignment and the pool's distribution:

```php
use Packstub\Tenancy\Database\DatabasePool;
use Packstub\Tenancy\Models\Tenant;

public function test_tenants_spread_evenly(): void
{
    foreach (range(1, 4) as $i) {
        Tenant::create(['name' => "T{$i}", 'slug' => "t{$i}"]);
    }

    $this->assertSame(
        ['tenant_pool_a' => 2, 'tenant_pool_b' => 2],
        app(DatabasePool::class)->distribution(),
    );
}

public function test_assignment_is_persisted(): void
{
    $tenant = Tenant::create(['name' => 'Acme', 'slug' => 'acme'])->fresh();

    $this->assertSame('tenant_pool_a', $tenant->getInternal('db_connection'));
}
```

The same pattern covers the edge cases worth pinning in your app:

- **Scale-out:** define a `tenant_pool_c` connection, append it to `packstub-tenancy.database_pool.connections`, create another tenant, and assert it lands on the new member.
- **Explicit pins:** create a tenant with `'tenancy_db_connection' => 'residency_de'` and assert the pool never reassigns it.
- **Databaseless tenants:** create a tenant with `'tenancy_create_database' => false` and assert `getInternal('db_connection')` stays `null`.
- **Disabled pool:** set `packstub-tenancy.database_pool.enabled` to `false` and assert new tenants get no assignment.

The package's own `tests/Feature/DatabasePoolAssignmentTest.php` demonstrates all of these, including driving the pool through the fluent `TenancyPlugin::make()->databasePool(...)` API.

## Running the package's own suites

### Default suite

```bash
composer test
```

Runs the `Unit` and `Feature` suites entirely on SQLite with a sync queue. No external services are required. The multi-server E2E tests are part of the PHPUnit configuration but skip themselves unless the `PACKSTUB_POOL_E2E=1` environment flag is set, so a plain `composer test` stays dependency-free.

To run a single test:

```bash
composer test:filter -- test_tenants_spread_evenly_under_least_tenants
```

### Multi-server E2E suite

```bash
composer test:multi-server
```

This proves horizontal scaling against real infrastructure: it boots **three independent PostgreSQL server instances** (separate data directories, ports, and postmasters) via `tests/bin/multi-server-pg.sh`, runs the 5-test `MultiServerE2E` suite with `PACKSTUB_POOL_E2E=1`, then stops the servers — even if the suite fails. The suite verifies that:

- Tenants distribute across two real servers, each tenant's physical database existing on its own server and **only** there.
- Runtime connections hit the right server (asserted by port) and tenant data is isolated across servers.
- Adding a third server to the pool mid-test scales out: the next tenant provisions onto it and is immediately usable.
- Deleting a tenant drops its database from its own server and leaves other servers untouched.
- `tenants:pool --check` confirms the `CREATE DATABASE` privilege on the real servers.

You need PostgreSQL installed locally — the script uses `pg_ctl` from your `PATH`, or falls back to common install locations (Homebrew, Postgres.app, DBngin, Debian/Ubuntu paths). The servers run as superuser `postgres` with trust auth, listening on `127.0.0.1` only.

You can also manage the servers directly:

```bash
tests/bin/multi-server-pg.sh start    # initdb on first run, then start all three
tests/bin/multi-server-pg.sh stop     # stop all three
tests/bin/multi-server-pg.sh destroy  # stop and delete the data directories
```

#### Ports and environment overrides

By default the three servers listen on ports `54331`, `54332`, and `54333`, with data directories under `/tmp/packstub-pool-pg`. Everything is overridable:

| Variable | Consumed by | Default | Purpose |
|---|---|---|---|
| `PACKSTUB_POOL_PG_PORTS` | server script | `"54331 54332 54333"` | Space-separated ports for the three servers |
| `PACKSTUB_POOL_PG_DIR` | server script | `/tmp/packstub-pool-pg` | Base directory for the three data directories |
| `PACKSTUB_POOL_PG1_PORT` | test suite | `54331` | Port the suite connects to for server 1 |
| `PACKSTUB_POOL_PG2_PORT` | test suite | `54332` | Port the suite connects to for server 2 |
| `PACKSTUB_POOL_PG3_PORT` | test suite | `54333` | Port the suite connects to for server 3 |
| `PACKSTUB_POOL_PG_HOST` | test suite | `127.0.0.1` | Host the suite connects to |
| `PACKSTUB_POOL_PG_USER` | test suite | `postgres` | Username the suite connects with |
| `PACKSTUB_POOL_PG_PASSWORD` | test suite | *(empty)* | Password the suite connects with |
| `PACKSTUB_POOL_E2E` | test suite | *(unset)* | Set to `1` to enable the E2E tests (set for you by `composer test:multi-server`) |

Note that the script and the suite read **separate** port variables. If you change ports, set both sides consistently:

```bash
PACKSTUB_POOL_PG_PORTS="55501 55502 55503" \
PACKSTUB_POOL_PG1_PORT=55501 \
PACKSTUB_POOL_PG2_PORT=55502 \
PACKSTUB_POOL_PG3_PORT=55503 \
composer test:multi-server
```

If a suite run fails with "Pool server [tenant_pool_N] is not reachable", the servers did not start (or ports don't match) — check `server.log` inside each data directory, or run `tests/bin/multi-server-pg.sh destroy` and start clean.
