'Tenant A']); $otherTenant = Tenant::create(['name' => 'Tenant B']); $company = Company::create(['tenant_id' => $tenant->id, 'name' => 'Company A']); $sibling = Company::create(['tenant_id' => $tenant->id, 'name' => 'Company B']); $foreign = Company::create(['tenant_id' => $otherTenant->id, 'name' => 'Company C']); $user = User::create(['name' => 'Synthetic User', 'email' => 'synthetic@example.test', 'password' => 'test-only-password']); DB::table('company_memberships')->insert(['user_id' => $user->id, 'tenant_id' => $tenant->id, 'company_id' => $company->id]); \Tests\Support\IdentityFixture::grant($user->id, $tenant->id, $company->id, 'company.view'); return [$user, $tenant, $company, $sibling, $foreign]; } public function test_membership_is_required_even_for_another_company_in_the_same_tenant(): void { [$user, $tenant, $company, $sibling, $foreign] = $this->scenario(); $this->actingAs($user)->withSession(['auth_version' => 1, 'company_context' => ['tenant_id' => $tenant->id, 'company_id' => $company->id, 'unit_id' => null, 'token' => 'synthetic']]) ->getJson('/api/context')->assertOk()->assertExactJson(['tenant_id' => $tenant->id, 'company_id' => $company->id, 'unit_id' => null]); foreach ([$sibling, $foreign] as $denied) { $this->withSession(['company_context' => ['tenant_id' => $denied->tenant_id, 'company_id' => $denied->id, 'unit_id' => null, 'token' => 'synthetic']])->getJson('/api/context')->assertForbidden(); } $this->withSession(['company_context' => []])->getJson('/api/context')->assertForbidden(); } public function test_company_owned_reads_and_bulk_writes_do_not_cross_company_boundaries(): void { [$user, $tenant, $company, $sibling] = $this->scenario(); DB::table('units')->insert([ ['tenant_id' => $tenant->id, 'company_id' => $company->id, 'name' => 'Visible'], ['tenant_id' => $tenant->id, 'company_id' => $sibling->id, 'name' => 'Private'], ]); $visibleId = DB::table('units')->where('company_id', $company->id)->value('id'); \Tests\Support\IdentityFixture::grant($user->id, $tenant->id, $company->id, 'unit.view', $visibleId); $context = app(CompanyContext::class); $context->selectFor($user->id, $tenant->id, $company->id); $this->assertSame(['Visible'], Unit::query()->pluck('name')->all()); $this->assertSame(1, Unit::query()->update(['name' => 'Updated'])); $this->assertSame('Private', DB::table('units')->where('company_id', $sibling->id)->value('name')); $unit = Unit::create(['name' => 'New']); $this->assertSame($company->id, $unit->company_id); $context->clear(); $this->expectException(AccessDeniedHttpException::class); Unit::query()->count(); } public function test_stale_model_cannot_be_saved_under_another_company_context(): void { [$user, $tenant, $company, $sibling] = $this->scenario(); DB::table('company_memberships')->insert(['user_id' => $user->id, 'tenant_id' => $tenant->id, 'company_id' => $sibling->id]); \Tests\Support\IdentityFixture::grant($user->id, $tenant->id, $sibling->id, 'company.view'); $context = app(CompanyContext::class); $context->selectFor($user->id, $tenant->id, $company->id); $unit = Unit::create(['name' => 'Original']); $context->selectFor($user->id, $tenant->id, $sibling->id); $unit->name = 'Forbidden'; $this->expectException(AccessDeniedHttpException::class); $unit->save(); } public function test_database_rejects_mismatched_tenant_company_relationship(): void { [, $tenant, , , $foreign] = $this->scenario(); $this->expectException(\Illuminate\Database\QueryException::class); DB::table('units')->insert(['tenant_id' => $tenant->id, 'company_id' => $foreign->id, 'name' => 'Invalid']); } public function test_context_is_cleared_after_a_request(): void { [$user, $tenant, $company] = $this->scenario(); $this->actingAs($user)->withSession(['auth_version' => 1, 'company_context' => ['tenant_id' => $tenant->id, 'company_id' => $company->id, 'unit_id' => null, 'token' => 'synthetic']])->getJson('/api/context')->assertOk(); $this->expectException(AccessDeniedHttpException::class); app(CompanyContext::class)->identifiers(); } }