Back to Feed
article 4m read

Test suite deleted a store it never created

A browser test accepted an unattended confirmation and cascaded a delete through a shared seed store in my local test database. Now every destructive test request has to prove the database is disposable and that its run owns the exact record it touches.

Article DepthPractice

Flow

Building the guard, step by step

  1. 11 Own the prompt

    Swap the browser popup for a dialog your app controls.

  2. 22 Leases

    Write a run-owned lease in the same transaction as each test record.

  3. 33 Setup guard

    One function in front of destructive routes, one code per failed check.

  4. 44 Marker

    A one-row disposable marker that only a deliberate step can create.

  5. 55 Runner

    One command claims the marker, runs tagged tests, cleans only its own leases.

  6. 66 Defaults

    Skip tagged tests by default and scan new tests in CI.

A browser test for the ordering system deleted a store it never made. The database then wiped out everything tied to that store.

The test was meant to make a store, delete it, and check it was gone. Instead it deleted a shared seed store in my local test database. The page used the browser's own "are you sure?" prompt. Nobody was in control of that prompt during the run, and it was accepted. The cascade took the store's stock, its order needs, its sales history and its shelf layout.

Nothing deployed was touched. I rebuilt the data by copying a sibling store, but the original random numbers were lost for good. I had no snapshot.

Where it broke

The prompt wasn't owned by my app. The delete route couldn't tell a test's own store from a shared one. And nothing made the database prove it was safe to wreck.

The confirm dialog

I replaced the native prompt with a dialog the app owns. Tests now have to find it and click it on purpose. That's good, but it's not a guard. A test can still click the right button on the wrong row.

Run-owned leases

When a test creates a store, write a small lease row that names the test run. Do it in the same transaction as the store. On update or delete, check that the lease names the same run, inside the same transaction as the change. No lease, no delete. My seed store has no lease, so no test can delete it now.

The setup guard

Put one guard function in front of every route that can destroy data. It should switch on whenever any test signal is present. Mine refuses, with its own code each time, unless all of this holds:

  • the app is in test mode with an explicit opt-in
  • the run ID on the request matches the one the test run started with
  • two separately given database addresses point at the same database
  • the host is local and the name looks like a test database
  • no production-style names show up anywhere
  • the database carries a "disposable" marker that names this run

Separate codes save time. You know which setting is wrong.

The disposable marker

Keep a one-row marker table in the database. Don't create the row in a migration, so production never has one. Make marking a database a deliberate step. Mine wants the exact database name given back as confirmation.

Have one runner command for destructive tests. The claim is meant for a single run.

The claim only succeeds on a scratch database that is marked throwaway and has no owner yet.

It runs only the tagged tests and won't let you widen that set. When it's done, it deletes only stores leased to its run. If that cleanup fails, it keeps the database locked and tells you to look first.

Default command and CI scan

Tag destructive tests and skip them in the normal test command. Add a scan in CI that flags tests that seem to change data without the tag and the guarded setup.

Where the scan and the leases fall short

A test that sends no signal skips the runtime checks. The scan covers that, but it's just pattern matching, and it allows a "this is read-only" comment as a way out. I used that to cut an export spec down to opening and closing dialogs, so it no longer tests the real change. I want it back. The tests behind all this use mocked database calls, so a complete guarded run is still unproven. Stores are the only type with leases right now.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward