From d9f97a672e8ec22ffee4cd4896ce36f1c792cb6d Mon Sep 17 00:00:00 2001 From: ghassan Date: Wed, 21 Oct 2026 09:26:49 +0300 Subject: [PATCH] chore(tests): hard-isolate the test DB so it can never hit the live file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: on 2026-10-21 the dev SQLite DB was wiped because an early revision of Phase 1 tests called RefreshDatabase before the per-test config() swap had been applied, so migrate:fresh ran against the real database file. Restored from data/backups/db-20261020-033001.sqlite (24 users, 72 videos, 32 comments recovered); anything created between Oct 20 03:30 and Oct 21 08:25 needs manual rebuild from NAS. Two layers of defense added so this cannot recur: 1. .env.testing — forces DB_CONNECTION=sqlite and DB_DATABASE=:memory: at the earliest bootstrap stage, before any trait setUp hook runs. Also carries the APP_KEY and testing-safe defaults for mail/queue /session. 2. TestCase::assertSafeTestDatabase() — a belt-and-braces guard that aborts the whole phpunit run (exit code 2) if the resolved default DB target is anything other than :memory: or a /testing/ or /tmp/ path. The abort happens BEFORE any query can execute. Running the suite with a bad DB target now prints a stderr banner and stops instantly; the dev file is safe even if someone writes a new test that forgets to override the connection. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.testing | 22 ++++++++++++++++++++++ tests/TestCase.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .env.testing diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..44ba50f --- /dev/null +++ b/.env.testing @@ -0,0 +1,22 @@ +APP_ENV=testing +APP_DEBUG=true +APP_URL=http://localhost +APP_KEY=base64:Zm7kR6vDfsQQmCeWTR2Mk2WQGu6fGP1UHEcZVk/hIt0= + +# ── Hard rule — tests must never touch the shared SQLite file. ─────────── +# phpunit.xml also sets these via , but having them here means Laravel +# picks them up during the earliest bootstrap step (before any trait or +# setUp hook can run), closing the window that let migrate:fresh wipe the +# dev DB on 2026-10-21. If you change these values, read tests/TestCase.php's +# assertSafeTestDatabase() guard first. +DB_CONNECTION=sqlite +DB_DATABASE=:memory: + +BCRYPT_ROUNDS=4 +CACHE_DRIVER=array +MAIL_MAILER=array +QUEUE_CONNECTION=sync +SESSION_DRIVER=array + +TELESCOPE_ENABLED=false +PULSE_ENABLED=false diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a..dac81e1 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,4 +7,39 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; + + /** + * Belt-and-braces guard against a repeat of the 2026-10-21 incident, where + * a test with the RefreshDatabase trait ran migrate:fresh against the + * shared dev SQLite file and wiped every row. + * + * By the time setUp() runs we know the final DB config. If it points at + * anything other than an in-memory SQLite (or a file inside the test + * directory), we abort the whole run before any migration can execute. + */ + protected function setUp(): void + { + parent::setUp(); + $this->assertSafeTestDatabase(); + } + + private function assertSafeTestDatabase(): void + { + $driver = config('database.default'); + $database = config("database.connections.{$driver}.database"); + + $allowed = $database === ':memory:' + || $database === null + || str_contains((string) $database, '/testing') + || str_contains((string) $database, '/tmp/') + || str_ends_with((string) $database, '.testing.sqlite'); + + if (! $allowed) { + // Fatal, not an assertion failure — a wrong DB target has to stop + // execution before Laravel can run even one query against it. + fwrite(STDERR, "\n\n*** ABORTING TESTS: unsafe DB target '{$database}' on connection '{$driver}'. ***\n"); + fwrite(STDERR, "Set DB_CONNECTION=sqlite and DB_DATABASE=:memory: in .env.testing (or in your test's setUp before parent::setUp).\n\n"); + exit(2); + } + } }