'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $this->company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'api-' . uniqid(), 'name' => 'API Co', 'status' => 'active', ]); app(TenantManager::class)->setCurrent($this->company); app(RbacSeeder::class)->seedTenantRoles($this->company->id); app(PermissionRegistrar::class)->setPermissionsTeamId($this->company->id); $this->admin = User::create(['name' => 'A', 'email' => 'a@e.com', 'password' => bcrypt('x'), 'role' => 'admin', 'status' => 'active']); $this->admin->syncRoles(['admin']); $this->mechanic = User::create(['name' => 'M', 'email' => 'm@e.com', 'password' => bcrypt('x'), 'role' => 'mechanic', 'status' => 'active']); $this->mechanic->syncRoles(['mechanic']); } public function test_admin_can_list_users_via_api(): void { Sanctum::actingAs($this->admin); $resp = $this->getJson('/api/v1/users'); $resp->assertOk(); $this->assertGreaterThanOrEqual(2, count($resp->json('data'))); } public function test_mechanic_cannot_list_users_403(): void { Sanctum::actingAs($this->mechanic); $resp = $this->getJson('/api/v1/users'); $resp->assertForbidden(); } public function test_admin_can_create_user_and_invitation_is_sent(): void { Mail::fake(); Sanctum::actingAs($this->admin); $resp = $this->postJson('/api/v1/users', [ 'name' => 'New U', 'email' => 'newu@e.com', 'role' => 'receptionist', 'send_invitation' => true, ]); $resp->assertCreated(); $this->assertEquals('inactive', $resp->json('data.status')); // inactive until invitation accepted $this->assertTrue($resp->json('invitation_sent')); Mail::assertQueued(UserInvitationMail::class); } public function test_admin_can_assign_and_remove_roles_via_api(): void { Sanctum::actingAs($this->admin); $resp = $this->postJson("/api/v1/users/{$this->mechanic->id}/roles", ['role' => 'receptionist']); $resp->assertOk(); $this->mechanic->refresh(); $this->assertTrue($this->mechanic->hasRole('receptionist')); $resp = $this->deleteJson("/api/v1/users/{$this->mechanic->id}/roles/receptionist"); $resp->assertOk(); $this->mechanic->refresh(); $this->assertFalse($this->mechanic->hasRole('receptionist')); } public function test_effective_permissions_endpoint_subtracts_denies(): void { Sanctum::actingAs($this->admin); // Admin has FINANCE_VIEW_OVERVIEW. Add a deny override. $perm = Permission::where('name', Permissions::FINANCE_VIEW_OVERVIEW)->first(); UserPermissionOverride::create([ 'user_id' => $this->admin->id, 'permission_id' => $perm->id, 'mode' => 'deny', 'reason' => 'test', 'granted_at' => now(), ]); $resp = $this->getJson("/api/v1/users/{$this->admin->id}/permissions"); $resp->assertOk(); $effective = collect($resp->json('data')); $this->assertFalse($effective->contains(Permissions::FINANCE_VIEW_OVERVIEW)); $denies = collect($resp->json('overrides.denies')); $this->assertTrue($denies->contains(Permissions::FINANCE_VIEW_OVERVIEW)); } public function test_add_override_via_api_persists(): void { Sanctum::actingAs($this->admin); $resp = $this->postJson("/api/v1/users/{$this->mechanic->id}/permission-overrides", [ 'permission' => Permissions::WORK_ORDERS_VIEW_ALL, 'mode' => 'grant', 'reason' => 'pinch hitter', 'expires_at' => now()->addDays(3)->toIso8601String(), ]); $resp->assertOk(); $this->mechanic->refresh(); $this->assertEquals(1, $this->mechanic->permissionOverrides()->count()); $this->assertTrue($this->mechanic->canDo(Permissions::WORK_ORDERS_VIEW_ALL)); } public function test_remove_override_via_api(): void { Sanctum::actingAs($this->admin); $perm = Permission::where('name', Permissions::WORK_ORDERS_VIEW_ALL)->first(); UserPermissionOverride::create(['user_id' => $this->mechanic->id, 'permission_id' => $perm->id, 'mode' => 'grant', 'granted_at' => now()]); $resp = $this->deleteJson("/api/v1/users/{$this->mechanic->id}/permission-overrides/" . Permissions::WORK_ORDERS_VIEW_ALL); $resp->assertOk(); $this->assertEquals(0, $this->mechanic->fresh()->permissionOverrides()->count()); } public function test_role_index_returns_all_roles_with_counts(): void { Sanctum::actingAs($this->admin); $resp = $this->getJson('/api/v1/roles'); $resp->assertOk(); $roles = collect($resp->json('data')); $this->assertGreaterThanOrEqual(7, $roles->count()); $owner = $roles->firstWhere('name', 'owner'); $this->assertNotNull($owner); $this->assertEquals(51, $owner['permissions_count']); } public function test_role_sync_permissions_updates_role(): void { Sanctum::actingAs($this->admin); $role = \Spatie\Permission\Models\Role::create(['name' => 'custom_role', 'guard_name' => 'web']); $resp = $this->putJson("/api/v1/roles/{$role->id}/permissions", [ 'permissions' => [Permissions::CLIENTS_VIEW_ALL, Permissions::VEHICLES_VIEW_ALL], ]); $resp->assertOk(); $this->assertEquals(2, $role->fresh()->permissions()->count()); } public function test_role_destroy_rejects_system_role(): void { Sanctum::actingAs($this->admin); $owner = \Spatie\Permission\Models\Role::where('name', 'owner')->where('company_id', $this->company->id)->first(); $resp = $this->deleteJson("/api/v1/roles/{$owner->id}"); $resp->assertStatus(422); $this->assertEquals('Cannot delete system role', $resp->json('error')); } public function test_permission_catalog_endpoint_returns_full_list_and_groups(): void { Sanctum::actingAs($this->admin); $resp = $this->getJson('/api/v1/permissions'); $resp->assertOk(); $this->assertEquals(51, count($resp->json('data'))); $this->assertArrayHasKey('grouped', $resp->json()); $this->assertArrayHasKey('clients', $resp->json('grouped')); $this->assertArrayHasKey('roles', $resp->json()); } public function test_revoke_all_sessions_endpoint(): void { Sanctum::actingAs($this->admin); \DB::table('sessions')->insert([ ['id' => 's1', 'user_id' => $this->mechanic->id, 'ip_address' => '1.1.1.1', 'user_agent' => 'X', 'payload' => '', 'last_activity' => time()], ['id' => 's2', 'user_id' => $this->mechanic->id, 'ip_address' => '2.2.2.2', 'user_agent' => 'Y', 'payload' => '', 'last_activity' => time()], ]); $resp = $this->deleteJson("/api/v1/users/{$this->mechanic->id}/sessions"); $resp->assertOk(); $this->assertEquals(2, $resp->json('revoked_count')); $this->assertEquals(0, \DB::table('sessions')->where('user_id', $this->mechanic->id)->count()); } }