actingAs($user); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); // Zero state $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant); $this->get($url)->assertOk()->assertSee('No dependencies found'); // Create a missing edge and assert badge appears InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'missing', 'target_id' => null, 'relationship_type' => 'assigned_to', 'metadata' => [ 'last_known_name' => 'Ghost Target', 'raw_ref' => ['example' => 'ref'], ], ]); $this->get($url) ->assertOk() ->assertSee('Missing') ->assertSee('Last known: Ghost Target'); }); it('direction filter limits to outbound or inbound', function () { [$user, $tenant] = createUserWithTenant(); $this->actingAs($user); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); // Outbound only InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'foundation_object', 'target_id' => (string) Str::uuid(), 'relationship_type' => 'assigned_to', ]); // Inbound only InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => (string) Str::uuid(), 'target_type' => 'inventory_item', 'target_id' => $item->external_id, 'relationship_type' => 'depends_on', ]); $urlOutbound = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant).'?direction=outbound'; $this->get($urlOutbound)->assertOk()->assertDontSee('No dependencies found'); $urlInbound = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant).'?direction=inbound'; $this->get($urlInbound)->assertOk()->assertDontSee('No dependencies found'); }); it('relationship filter limits edges by type', function () { [$user, $tenant] = createUserWithTenant(); $this->actingAs($user); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); // Two outbound edges with different relationship types. InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'missing', 'target_id' => null, 'relationship_type' => 'assigned_to', 'metadata' => ['last_known_name' => 'Assigned Target'], ]); InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'missing', 'target_id' => null, 'relationship_type' => 'scoped_by', 'metadata' => ['last_known_name' => 'Scoped Target'], ]); $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant) .'?direction=outbound&relationship_type=scoped_by'; $this->get($url) ->assertOk() ->assertSee('Scoped Target') ->assertDontSee('Assigned Target'); }); it('does not show edges from other tenants (tenant isolation)', function () { [$user, $tenant] = createUserWithTenant(); $this->actingAs($user); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $otherTenant = Tenant::factory()->create(); // Same source_id, but different tenant_id: must not be rendered. InventoryLink::factory()->create([ 'tenant_id' => $otherTenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'missing', 'target_id' => null, 'relationship_type' => 'assigned_to', 'metadata' => ['last_known_name' => 'Other Tenant Edge'], ]); $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant); $this->get($url) ->assertOk() ->assertDontSee('Other Tenant Edge'); }); it('shows masked identifier when last known name is missing', function () { [$user, $tenant] = createUserWithTenant(); $this->actingAs($user); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'foundation_object', 'target_id' => '12345678-1234-1234-1234-123456789012', 'relationship_type' => 'assigned_to', 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'aad_group', ], ]); $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant); $this->get($url) ->assertOk() ->assertSee('Group (external): 123456…'); }); it('resolves scope tag and assignment filter names from local inventory when available and labels groups as external', function () { [$user, $tenant] = createUserWithTenant(); $this->actingAs($user); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $scopeTag = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'policy_type' => 'roleScopeTag', 'external_id' => '6', 'display_name' => 'Finance', ]); $assignmentFilter = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'policy_type' => 'assignmentFilter', 'external_id' => '62fb77f0-0000-0000-0000-000000000000', 'display_name' => 'VIP Devices', ]); InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'foundation_object', 'target_id' => $scopeTag->external_id, 'relationship_type' => 'scoped_by', 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'scope_tag', ], ]); InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'foundation_object', 'target_id' => $assignmentFilter->external_id, 'relationship_type' => 'uses_assignment_filter', 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'assignment_filter', ], ]); InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'foundation_object', 'target_id' => '428f24c0-0000-0000-0000-000000000000', 'relationship_type' => 'assigned_to_include', 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'aad_group', ], ]); $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant); $this->get($url) ->assertOk() ->assertSee('Scope Tag: Finance (6…)') ->assertSee('Assignment Filter: VIP Devices (62fb77…)') ->assertSee('Group (external): 428f24…'); }); it('does not call Graph client while rendering inventory item dependencies view (FR-006 guard)', function () { [$user, $tenant] = createUserWithTenant(); $this->actingAs($user); $graph = \Mockery::mock(GraphClientInterface::class); $graph->shouldNotReceive('listPolicies'); $graph->shouldNotReceive('getPolicy'); $graph->shouldNotReceive('getOrganization'); $graph->shouldNotReceive('applyPolicy'); $graph->shouldNotReceive('getServicePrincipalPermissions'); $graph->shouldNotReceive('request'); app()->instance(GraphClientInterface::class, $graph); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $scopeTag = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'policy_type' => 'roleScopeTag', 'external_id' => '6', 'display_name' => 'Finance', ]); InventoryLink::factory()->create([ 'tenant_id' => $tenant->getKey(), 'source_type' => 'inventory_item', 'source_id' => $item->external_id, 'target_type' => 'foundation_object', 'target_id' => $scopeTag->external_id, 'relationship_type' => 'scoped_by', 'metadata' => [ 'last_known_name' => null, 'foundation_type' => 'scope_tag', ], ]); $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant); $this->get($url) ->assertOk() ->assertSee('Scope Tag: Finance'); }); it('blocks guest access to inventory item dependencies view', function () { $tenant = Tenant::factory()->create(); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $url = InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant); $this->get($url)->assertRedirect(); });