staffWithTicketPermission(); Sanctum::actingAs($user); $response = $this->withHeader('X-Tenant-ID', $tenant->id) ->getJson('/api/me/menus'); $response ->assertOk() ->assertJsonPath('data.tenant_id', $tenant->id) ->assertJsonPath('data.access_level', 'staff') ->assertJsonPath('data.menus.0.slug', 'ticketing-ticket'); } public function test_user_cannot_select_a_tenant_they_do_not_belong_to(): void { [$user] = $this->staffWithTicketPermission(); $otherTenant = Tenant::create([ 'tenant_code' => 'OTHER', 'tenant_name' => 'Other Tenant', ]); Sanctum::actingAs($user); $this->withHeader('X-Tenant-ID', $otherTenant->id) ->getJson('/api/me/menus') ->assertForbidden() ->assertJsonPath('message', 'User tidak memiliki akses ke tenant yang dipilih.'); } public function test_tenant_owner_cannot_promote_a_user_to_master_admin(): void { [$owner, $tenant] = $this->staffWithTicketPermission('tenant_owner'); $target = User::factory()->create(['access_level' => 'staff']); $target->userTenants()->create([ 'tenant_id' => $tenant->id, 'is_default' => true, ]); $usersMenu = MenuList::create([ 'name' => 'Data Pengguna', 'slug' => 'users-user', 'url' => '/users', 'is_active' => true, ]); $group = $owner->userMenuGroups()->first()->menuGroup; $group->menus()->attach($usersMenu->id, [ 'can_view' => true, 'can_update' => true, ]); Sanctum::actingAs($owner); $this->withHeader('X-Tenant-ID', $tenant->id) ->patchJson("/api/users/{$target->id}", [ 'access_level' => 'master_admin', ]) ->assertUnprocessable() ->assertJsonValidationErrors('access_level'); } public function test_index_response_uses_the_same_paginator_contract_as_tickets(): void { $master = User::factory()->create([ 'is_master' => true, 'access_level' => 'master_admin', ]); Tenant::create([ 'tenant_code' => 'PAGINATED', 'tenant_name' => 'Pagination Test', ]); Sanctum::actingAs($master); $this->getJson('/api/tenants?per_page=10') ->assertOk() ->assertJsonStructure([ 'success', 'code', 'message', 'data' => [ 'current_page', 'data', 'first_page_url', 'from', 'last_page', 'last_page_url', 'links', 'next_page_url', 'path', 'per_page', 'prev_page_url', 'to', 'total', ], ]) ->assertJsonPath('data.current_page', 1) ->assertJsonPath('data.per_page', 10) ->assertJsonPath('data.data.0.tenant_code', 'PAGINATED'); } public function test_index_query_parameters_are_validated(): void { $master = User::factory()->create([ 'is_master' => true, 'access_level' => 'master_admin', ]); Sanctum::actingAs($master); $this->getJson('/api/tenants?page=0&per_page=101&status=unknown') ->assertUnprocessable() ->assertJsonValidationErrors(['page', 'per_page', 'status']); } public function test_tenant_owner_only_receives_tenants_assigned_to_the_user(): void { [$owner, $assignedTenant] = $this->staffWithTicketPermission('tenant_owner'); $secondAssignedTenant = Tenant::create([ 'tenant_code' => 'OWNER-SECOND', 'tenant_name' => 'Second Assigned Tenant', ]); $owner->userTenants()->create([ 'tenant_id' => $secondAssignedTenant->id, 'is_default' => false, ]); $otherTenant = Tenant::create([ 'tenant_code' => 'OWNER-OTHER', 'tenant_name' => 'Unassigned Tenant', ]); Sanctum::actingAs($owner); $response = $this->withHeader('X-Tenant-ID', $assignedTenant->id) ->getJson('/api/tenants?per_page=100') ->assertOk(); $tenantIds = collect($response->json('data.data'))->pluck('id'); $this->assertTrue($tenantIds->contains($assignedTenant->id)); $this->assertTrue($tenantIds->contains($secondAssignedTenant->id)); $this->assertFalse($tenantIds->contains($otherTenant->id)); $this->withHeader('X-Tenant-ID', $assignedTenant->id) ->getJson("/api/tenants/{$secondAssignedTenant->id}") ->assertOk(); $this->withHeader('X-Tenant-ID', $assignedTenant->id) ->getJson("/api/tenants/{$otherTenant->id}") ->assertForbidden(); } public function test_authenticated_user_can_view_and_update_only_their_own_profile(): void { $user = User::factory()->create([ 'name' => 'Nama Lama', 'access_level' => 'staff', ]); Sanctum::actingAs($user); $this->getJson('/api/me/profile') ->assertOk() ->assertJsonPath('data.id', $user->id) ->assertJsonPath('data.access_level', 'staff'); $this->putJson('/api/me/profile', [ 'name' => 'Nama Baru', 'username' => 'nama.baru', 'email' => $user->email, 'whatsapp_number' => '08123456789', 'access_level' => 'master_admin', 'is_master' => true, 'user_profile' => [ 'nik' => '1234567890', 'address' => 'Alamat baru', ], ]) ->assertOk() ->assertJsonPath('data.name', 'Nama Baru') ->assertJsonPath('data.user_profile.nik', '1234567890') ->assertJsonPath('data.access_level', 'staff') ->assertJsonPath('data.is_master', false); $user->refresh(); $this->assertSame('staff', $user->access_level->value); $this->assertFalse($user->is_master); } public function test_tenant_owner_creates_and_only_lists_groups_in_the_active_tenant(): void { [$owner, $tenant] = $this->staffWithTicketPermission('tenant_owner'); $otherTenant = Tenant::create([ 'tenant_code' => 'OTHER-GROUP', 'tenant_name' => 'Other Group Tenant', ]); $otherTenantGroup = MenuGroup::create([ 'tenant_id' => $otherTenant->id, 'name' => 'Other Tenant Group', 'is_active' => true, ]); $globalSystemGroup = MenuGroup::create([ 'tenant_id' => null, 'name' => 'Global System Group', 'is_system' => true, 'is_active' => true, ]); $menu = MenuList::create([ 'name' => 'Group Menus', 'slug' => 'users-group-menus', 'url' => '/group-menus', 'is_active' => true, ]); $owner->userMenuGroups()->first()->menuGroup->menus()->attach($menu->id, [ 'can_view' => true, 'can_create' => true, ]); Sanctum::actingAs($owner); $this->withHeader('X-Tenant-ID', $tenant->id) ->postJson('/api/menu-groups', [ 'name' => 'Teknisi Tenant', 'description' => 'Akses teknisi', 'is_active' => true, ]) ->assertCreated() ->assertJsonPath('data.tenant_id', $tenant->id) ->assertJsonPath('data.is_system', false) ->assertJsonPath('data.tenant.id', $tenant->id); $response = $this->withHeader('X-Tenant-ID', $tenant->id) ->getJson('/api/menu-groups?per_page=100') ->assertOk(); $this->assertNotContains( 'Other Tenant Group', collect($response->json('data.data'))->pluck('name')->all() ); $this->assertNotContains( 'Global System Group', collect($response->json('data.data'))->pluck('name')->all() ); $this->withHeader('X-Tenant-ID', $tenant->id) ->getJson("/api/menu-groups/{$otherTenantGroup->id}") ->assertForbidden(); $this->withHeader('X-Tenant-ID', $tenant->id) ->getJson("/api/menu-groups/{$globalSystemGroup->id}") ->assertForbidden(); } public function test_master_admin_can_create_global_and_tenant_menu_groups(): void { $master = User::factory()->create([ 'is_master' => true, 'access_level' => 'master_admin', ]); $tenant = Tenant::create([ 'tenant_code' => 'MASTER-SCOPE', 'tenant_name' => 'Master Scope Tenant', ]); Sanctum::actingAs($master); $this->postJson('/api/menu-groups', [ 'name' => 'Global System Group', 'is_active' => true, 'tenant_id' => null, ]) ->assertCreated() ->assertJsonPath('data.tenant_id', null) ->assertJsonPath('data.is_system', true); $this->postJson('/api/menu-groups', [ 'name' => 'Tenant Scoped Group', 'is_active' => true, 'tenant_id' => $tenant->id, ]) ->assertCreated() ->assertJsonPath('data.tenant_id', $tenant->id) ->assertJsonPath('data.is_system', false) ->assertJsonPath('data.tenant.tenant_name', 'Master Scope Tenant'); } private function staffWithTicketPermission(string $level = 'staff'): array { $tenant = Tenant::create([ 'tenant_code' => fake()->unique()->bothify('TEN-###'), 'tenant_name' => fake()->company(), ]); $user = User::factory()->create(['access_level' => $level]); $user->userTenants()->create([ 'tenant_id' => $tenant->id, 'is_default' => true, ]); $menu = MenuList::create([ 'name' => 'Tiket', 'slug' => 'ticketing-ticket', 'url' => '/tickets', 'is_active' => true, ]); $group = MenuGroup::create([ 'tenant_id' => $tenant->id, 'name' => 'Staff '.$tenant->id, 'is_active' => true, ]); $group->menus()->attach($menu->id, [ 'can_view' => true, 'can_update' => true, ]); $user->userMenuGroups()->create([ 'menu_group_id' => $group->id, 'tenant_id' => $tenant->id, ]); return [$user, $tenant]; } }