Tests can wipe the dev database: phpunit.xml env vars are not forced #53

Closed
opened 2026-08-18 21:50:10 +02:00 by myrmidex · 0 comments
Owner

Impact

A test run wiped the local dev database (dishplanner). Symptom: logged out of the local app mid-run, because SESSION_DRIVER=file sessions key off a users row that no longer existed.

49 of 59 test files use RefreshDatabase, which runs migrate:fresh — drop all tables, re-migrate — against whatever connection resolves at bootstrap.

Root cause

phpunit.xml declares its test environment with bare <env> entries and no force="true":

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

<env> without force sets a default — it does not overwrite a variable that is already present in the environment. <server> writes to $_SERVER, which is where Laravel's config actually resolves from, and force="true" overwrites unconditionally.

So whenever phpunit is launched in a process that already has the container's .env loaded (APP_ENV=local, DB_CONNECTION=mysql, DB_DATABASE=dishplanner), the real dev database wins and RefreshDatabase drops it.

.env.testing does not help here: Laravel only loads it when APP_ENV=testing, and nothing sets that in this path.

Reference: ffr is immune, and shows why

ffr has more exposure by volume — 100 of 121 test files use RefreshDatabase — and has never been wiped, because its phpunit.xml forces every value:

<env name="APP_ENV" value="testing" force="true"/>
<server name="APP_ENV" value="testing" force="true"/>
<server name="DB_CONNECTION" value="sqlite" force="true"/>
<server name="DB_DATABASE" value=":memory:" force="true"/>
<server name="SESSION_DRIVER" value="array" force="true"/>
...

Audit across projects:

project force="true" <server> <env> RefreshDatabase files
ffr 12 11 1 100
dishplanner 0 0 11 49
trove 0 18 0 6

Same tooling, same RefreshDatabase pattern, opposite outcomes. The only thing protecting ffr is force="true".

Fix

Rewrite the <php> block in phpunit.xml to match ffr: convert each <env> to <server> with force="true", keeping one <env name="APP_ENV" value="testing" force="true"/> alongside the <server> equivalent.

This makes the repo defend itself on every invocation path — MCP tooling, shell.nix helpers, CI, manual runs — instead of depending on the caller to inject the right environment.

Notes

  • .env.testing, .env.dusk.local, and phpunit.dusk.xml are all correct and need no change. .env.dusk.local legitimately uses MySQL (dishplanner_test), since Dusk needs a real server.
  • The container-dev MCP server has been patched separately to inject .env.testing and refuse to run against a non-test database. That is defense in depth and lives outside this repo; it does not remove the need for this fix.
  • Worth a follow-up: trove uses <server> but with zero force attributes, so it has the same latent gap.

Acceptance

  • phpunit.xml <php> block uses <server> + force="true" for every value
  • Full suite passes
  • Test run started with APP_ENV=local in the environment still resolves to sqlite :memory: and leaves the dishplanner database untouched
## Impact A test run wiped the local dev database (`dishplanner`). Symptom: logged out of the local app mid-run, because `SESSION_DRIVER=file` sessions key off a `users` row that no longer existed. 49 of 59 test files use `RefreshDatabase`, which runs `migrate:fresh` — drop all tables, re-migrate — against whatever connection resolves at bootstrap. ## Root cause `phpunit.xml` declares its test environment with bare `<env>` entries and **no `force="true"`**: ```xml <env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/> ``` `<env>` without `force` sets a *default* — it does not overwrite a variable that is already present in the environment. `<server>` writes to `$_SERVER`, which is where Laravel's config actually resolves from, and `force="true"` overwrites unconditionally. So whenever phpunit is launched in a process that already has the container's `.env` loaded (`APP_ENV=local`, `DB_CONNECTION=mysql`, `DB_DATABASE=dishplanner`), the real dev database wins and `RefreshDatabase` drops it. `.env.testing` does not help here: Laravel only loads it when `APP_ENV=testing`, and nothing sets that in this path. ## Reference: ffr is immune, and shows why `ffr` has *more* exposure by volume — 100 of 121 test files use `RefreshDatabase` — and has never been wiped, because its `phpunit.xml` forces every value: ```xml <env name="APP_ENV" value="testing" force="true"/> <server name="APP_ENV" value="testing" force="true"/> <server name="DB_CONNECTION" value="sqlite" force="true"/> <server name="DB_DATABASE" value=":memory:" force="true"/> <server name="SESSION_DRIVER" value="array" force="true"/> ... ``` Audit across projects: | project | `force="true"` | `<server>` | `<env>` | RefreshDatabase files | |---|---|---|---|---| | ffr | 12 | 11 | 1 | 100 | | **dishplanner** | **0** | **0** | **11** | **49** | | trove | 0 | 18 | 0 | 6 | Same tooling, same `RefreshDatabase` pattern, opposite outcomes. The only thing protecting ffr is `force="true"`. ## Fix Rewrite the `<php>` block in `phpunit.xml` to match ffr: convert each `<env>` to `<server>` with `force="true"`, keeping one `<env name="APP_ENV" value="testing" force="true"/>` alongside the `<server>` equivalent. This makes the repo defend itself on every invocation path — MCP tooling, `shell.nix` helpers, CI, manual runs — instead of depending on the caller to inject the right environment. ## Notes - `.env.testing`, `.env.dusk.local`, and `phpunit.dusk.xml` are all correct and need no change. `.env.dusk.local` legitimately uses MySQL (`dishplanner_test`), since Dusk needs a real server. - The `container-dev` MCP server has been patched separately to inject `.env.testing` and refuse to run against a non-test database. That is defense in depth and lives outside this repo; it does not remove the need for this fix. - Worth a follow-up: `trove` uses `<server>` but with zero `force` attributes, so it has the same latent gap. ## Acceptance - [ ] `phpunit.xml` `<php>` block uses `<server>` + `force="true"` for every value - [ ] Full suite passes - [ ] Test run started with `APP_ENV=local` in the environment still resolves to sqlite `:memory:` and leaves the `dishplanner` database untouched
myrmidex added this to the v0.9.0 milestone 2026-08-18 21:50:10 +02:00
myrmidex added the
bug
testing
labels 2026-08-18 21:50:10 +02:00
myrmidex self-assigned this 2026-08-18 23:32:33 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/dishplanner#53
No description provided.