create(); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $policyData = [ 'id' => $item->external_id, 'assignments' => [ ['target' => ['groupId' => 'group-1']], ['target' => ['groupId' => 'group-2']], ], 'roleScopeTagIds' => ['scope-tag-1', 'scope-tag-2'], ]; $svc = app(DependencyExtractionService::class); $warnings1 = $svc->extractForPolicyData($item, $policyData); $warnings2 = $svc->extractForPolicyData($item, $policyData); // re-run, should be idempotent expect($warnings1)->toBeArray()->toBeEmpty(); expect($warnings2)->toBeArray()->toBeEmpty(); $edges = InventoryLink::query()->where('tenant_id', $tenant->getKey())->get(); expect($edges)->toHaveCount(4); // Ensure uniqueness by tuple (source, target, type) $tuples = $edges->map(fn ($e) => implode('|', [ $e->source_type, $e->source_id, $e->target_type, (string) $e->target_id, $e->relationship_type, ]))->unique(); expect($tuples->count())->toBe(4); }); it('handles unsupported references by recording warnings (no edges)', function () { $tenant = Tenant::factory()->create(); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $policyData = [ 'id' => $item->external_id, 'assignments' => [ ['target' => ['filterId' => 'filter-only-no-group']], // no groupId shape → missing ], ]; Log::spy(); $svc = app(DependencyExtractionService::class); $warnings = $svc->extractForPolicyData($item, $policyData); expect($warnings)->toBeArray()->toHaveCount(1); expect($warnings[0]['type'] ?? null)->toBe('unsupported_reference'); expect($warnings[0]['policy_id'] ?? null)->toBe($item->external_id); expect(InventoryLink::query()->count())->toBe(0); Log::shouldHaveReceived('info') ->withArgs(fn (string $message, array $context) => $message === 'Unsupported reference shape encountered' && ($context['type'] ?? null) === 'unsupported_reference' && ($context['policy_id'] ?? null) === $item->external_id) ->once(); }); it('deduplicates edges before upsert to avoid conflict errors', function () { $tenant = \App\Models\Tenant::factory()->create(); $item = \App\Models\InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'policy_type' => 'settingsCatalogPolicy', 'external_id' => 'pol-dup-1', ]); $svc = app(\App\Services\Inventory\DependencyExtractionService::class); $policyData = [ 'id' => 'pol-dup-1', 'assignments' => [ ['target' => ['groupId' => 'group-1', '@odata.type' => '#microsoft.graph.groupAssignmentTarget']], ['target' => ['groupId' => 'group-1', '@odata.type' => '#microsoft.graph.groupAssignmentTarget']], ], 'roleScopeTagIds' => ['0', '0'], ]; $warnings = $svc->extractForPolicyData($item, $policyData); expect($warnings)->toBeArray()->toBeEmpty(); $edges = \App\Models\InventoryLink::query() ->where('tenant_id', $tenant->getKey()) ->where('source_id', 'pol-dup-1') ->get(); expect($edges->where('relationship_type', 'assigned_to_include'))->toHaveCount(1); expect($edges->where('relationship_type', 'scoped_by'))->toHaveCount(1); });