# Automated regression example: workspace order visibility This is a complete teaching fixture for a local Laravel 12 project with PHPUnit. It registers its own test-only route, creates synthetic records in a dedicated in-memory SQLite connection and runs simulated application requests. It does not test your application's existing order endpoints. ## Setup and execution Use a local development/test checkout with its Composer development dependencies installed, an existing `Tests\TestCase`, a PHPUnit configuration selecting `APP_ENV=testing`, and PHP's SQLite/PDO SQLite extensions enabled. The example was executed with **PHP 8.3.6, Laravel 12.40.2, PHPUnit 11.5.44 and SQLite 3.45.1**; these are the tested versions, not a claim that they are the latest releases. Copy the complete PHP block below into `tests/Feature/WorkspaceOrdersRegressionTest.php`, then run from the project root: ```sh php vendor/bin/phpunit tests/Feature/WorkspaceOrdersRegressionTest.php --do-not-cache-result ``` The fixture refuses to run outside the testing environment or if its route already exists. It explicitly sets the named `regression_example` connection to SQLite `:memory:`, creates only its own example tables on that connection and purges the connection during teardown. It does not run migrations or use `RefreshDatabase`. Each test starts with new synthetic data and makes one request. The host project's own boot and global middleware configuration still apply. ## Expected contract and observed result The synthetic user with ID 101 belongs to workspace 7; user 202 belongs to workspace 9. `actingAs` supplies these identities to a dedicated Laravel session guard. This bypasses an interactive login and is not a test of your production membership model. | Case | Expected observation | | --- | --- | | User 101 requests the list with `workspace_id=9` in the query | HTTP 200; IDs 10, 30 and 40, in that order. Caller input does not replace authenticated workspace 7. | | User 101 requests `status=open` | HTTP 200; IDs 10 and 40. Workspace 9's open order 20 stays excluded. | | User 202 requests the list | HTTP 200; only ID 20. | | User 101 requests `status=cancelled` | HTTP 422 with a status validation error. | | A guest requests the list | HTTP 401. | The three successful responses assert exact `id` and `status` values, not only HTTP 200 or a count. The baseline executed successfully: **5 tests, 10 assertions**. ## Full example ```php app->environment('testing')) { throw new \LogicException('Run this fixture only in the testing environment.'); } foreach (Route::getRoutes() as $route) { if ($route->uri() === ltrim(self::PATH, '/')) { throw new \LogicException('The fixture route already exists.'); } } config([ 'database.connections.'.self::CONNECTION => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'foreign_key_constraints' => true, ], 'session.driver' => 'array', 'auth.guards.'.self::GUARD => [ 'driver' => 'session', 'provider' => 'regression_example', ], 'auth.providers.regression_example' => [ 'driver' => 'database', 'connection' => self::CONNECTION, 'table' => 'regression_example_users', ], ]); DB::purge(self::CONNECTION); $database = DB::connection(self::CONNECTION); if ($database->getDriverName() !== 'sqlite' || $database->getConfig('database') !== ':memory:') { throw new \LogicException('The fixture requires isolated in-memory SQLite.'); } $schema = $database->getSchemaBuilder(); $schema->create('regression_example_users', function (Blueprint $table) { $table->unsignedInteger('id')->primary(); $table->unsignedInteger('workspace_id'); }); $schema->create('regression_example_orders', function (Blueprint $table) { $table->unsignedInteger('id')->primary(); $table->unsignedInteger('workspace_id'); $table->string('status'); }); $database->table('regression_example_users')->insert([ ['id' => 101, 'workspace_id' => 7], ['id' => 202, 'workspace_id' => 9], ]); $database->table('regression_example_orders')->insert([ ['id' => 30, 'workspace_id' => 7, 'status' => 'closed'], ['id' => 20, 'workspace_id' => 9, 'status' => 'open'], ['id' => 10, 'workspace_id' => 7, 'status' => 'open'], ['id' => 40, 'workspace_id' => 7, 'status' => 'open'], ]); Route::get(self::PATH, function (Request $request) { $validated = $request->validate([ 'status' => ['sometimes', 'required', 'string', Rule::in(['open', 'closed'])], ]); $query = DB::connection(self::CONNECTION) ->table('regression_example_orders') ->where('workspace_id', $request->user()->workspace_id); if (isset($validated['status'])) { $query->where('status', $validated['status']); } return response()->json([ 'data' => $query->orderBy('id')->get(['id', 'status']), ]); })->middleware('auth:'.self::GUARD); } public function test_lists_only_its_workspace_in_id_order(): void { $this->actingAs($this->fixtureUser(101), self::GUARD) ->getJson(self::PATH.'?workspace_id=9') ->assertOk() ->assertExactJson(['data' => [ ['id' => 10, 'status' => 'open'], ['id' => 30, 'status' => 'closed'], ['id' => 40, 'status' => 'open'], ]]); } public function test_filters_status_within_its_workspace(): void { $this->actingAs($this->fixtureUser(101), self::GUARD) ->getJson(self::PATH.'?status=open') ->assertOk() ->assertExactJson(['data' => [ ['id' => 10, 'status' => 'open'], ['id' => 40, 'status' => 'open'], ]]); } public function test_another_workspace_sees_its_own_order(): void { $this->actingAs($this->fixtureUser(202), self::GUARD) ->getJson(self::PATH) ->assertOk() ->assertExactJson(['data' => [['id' => 20, 'status' => 'open']]]); } public function test_rejects_an_unknown_status(): void { $this->actingAs($this->fixtureUser(101), self::GUARD) ->getJson(self::PATH.'?status=cancelled') ->assertUnprocessable() ->assertJsonValidationErrors('status'); } public function test_rejects_an_unauthenticated_request(): void { $this->getJson(self::PATH)->assertUnauthorized(); } private function fixtureUser(int $id): GenericUser { $row = DB::connection(self::CONNECTION) ->table('regression_example_users')->where('id', $id)->first(); return new GenericUser((array) $row); } protected function tearDown(): void { try { if ($this->app) { DB::purge(self::CONNECTION); } } finally { parent::tearDown(); } } } ``` ## Demonstrate that the checks detect a regression In a temporary copy of the example, remove only the workspace condition from the query. Replace: ```php $query = DB::connection(self::CONNECTION) ->table('regression_example_orders') ->where('workspace_id', $request->user()->workspace_id); ``` with: ```php $query = DB::connection(self::CONNECTION) ->table('regression_example_orders'); ``` Keep every assertion and fixture row unchanged, and rerun the same command against that copy. In the recorded run, all **three visibility/filtering cases failed**, while invalid-status validation and guest rejection still passed. The failure output includes unexpected records from the other workspace. Restoring the original query restores the intended boundary; do not change the assertions to accept leaked records. This is a deliberate single-fault sensitivity check. It shows that these assertions catch this removed restriction with these records. It is not a complete security assessment, mutation-testing score or guarantee against every authorization defect. ## Adapt it to an application In a real application's regression suite, target its real route and use its actual identity, workspace-membership and authorization model. Keep independently specified expected records, including records the caller must not receive. Add cases selected from that application's risk and contract; do not retain a test-only route as supposed coverage of a different production endpoint. The example accepts `open` and `closed` but the executed five-case suite exercises only the `open` filter. A valid `closed` filter and an empty matching result are useful next cases for a real contract. Membership changes, role distinctions, pagination, writes, background jobs and other endpoints also need their own requirements and checks where applicable. Laravel's HTTP test client simulates a request internally. This example does not exercise TLS, a reverse proxy, browser JavaScript, CSRF defenses, concurrent writes, workload capacity or the production database engine. Human usability research and accessibility evaluation require other evidence. Related material: - https://nomadicsoft.io/blog/automated-regression-testing - https://nomadicsoft.io/blog/software-qa-testing-services - https://nomadicsoft.io/downloads/qa-testing-brief.md - https://laravel.com/framework/docs/12.x/http-tests - https://laravel.com/framework/docs/12.x/database-testing