276 lines
9.9 KiB
PHP
276 lines
9.9 KiB
PHP
<?php
|
|
|
|
use App\Models\InventoryItem;
|
|
use App\Models\InventoryLink;
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphResponse;
|
|
use App\Services\Inventory\DependencyQueryService;
|
|
use App\Services\Inventory\InventorySyncService;
|
|
|
|
class FakeGraphClientForDeps implements GraphClientInterface
|
|
{
|
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [
|
|
[
|
|
'id' => 'pol-1',
|
|
'displayName' => 'Test Policy',
|
|
'assignments' => [
|
|
['target' => ['groupId' => 'group-1']],
|
|
['target' => ['groupId' => 'group-2']],
|
|
],
|
|
'roleScopeTagIds' => ['scope-tag-1', 'scope-tag-2'],
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [], 200);
|
|
}
|
|
|
|
public function getOrganization(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [], 200);
|
|
}
|
|
|
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [], 200);
|
|
}
|
|
|
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [], 200);
|
|
}
|
|
|
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [], 200);
|
|
}
|
|
}
|
|
|
|
it('extracts edges during inventory sync and marks missing appropriately', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
$this->app->bind(GraphClientInterface::class, fn () => new FakeGraphClientForDeps);
|
|
|
|
$svc = app(InventorySyncService::class);
|
|
|
|
$run = $svc->syncNow($tenant, [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'categories' => [],
|
|
'include_foundations' => false,
|
|
'include_dependencies' => true,
|
|
]);
|
|
|
|
expect($run->status)->toBe('success');
|
|
|
|
$edges = InventoryLink::query()->where('tenant_id', $tenant->getKey())->get();
|
|
// 2 assigned_to + 2 scoped_by = 4
|
|
expect($edges->count())->toBe(4);
|
|
});
|
|
|
|
it('respects 50-edge limit for outbound extraction', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
// Fake client returning 60 group assignments
|
|
$this->app->bind(GraphClientInterface::class, function () {
|
|
return new class implements GraphClientInterface
|
|
{
|
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
|
{
|
|
$assignments = [];
|
|
for ($i = 1; $i <= 60; $i++) {
|
|
$assignments[] = ['target' => ['groupId' => 'g-'.$i]];
|
|
}
|
|
|
|
return new GraphResponse(true, [[
|
|
'id' => 'pol-2',
|
|
'displayName' => 'Big Assignments',
|
|
'assignments' => $assignments,
|
|
]]);
|
|
}
|
|
|
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function getOrganization(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
};
|
|
});
|
|
|
|
$svc = app(InventorySyncService::class);
|
|
$svc->syncNow($tenant, [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'categories' => [],
|
|
'include_foundations' => false,
|
|
'include_dependencies' => true,
|
|
]);
|
|
|
|
$count = InventoryLink::query()->where('tenant_id', $tenant->getKey())->count();
|
|
expect($count)->toBe(50);
|
|
});
|
|
|
|
it('persists unsupported reference warnings on the sync run record', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
$this->app->bind(GraphClientInterface::class, function () {
|
|
return new class implements GraphClientInterface
|
|
{
|
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true, [[
|
|
'id' => 'pol-warn-1',
|
|
'displayName' => 'Unsupported Assignment Target',
|
|
'assignments' => [
|
|
['target' => ['filterId' => 'filter-only-no-group']],
|
|
],
|
|
]]);
|
|
}
|
|
|
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function getOrganization(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
|
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
|
{
|
|
return new GraphResponse(true);
|
|
}
|
|
};
|
|
});
|
|
|
|
$svc = app(InventorySyncService::class);
|
|
$run = $svc->syncNow($tenant, [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'categories' => [],
|
|
'include_foundations' => false,
|
|
'include_dependencies' => true,
|
|
]);
|
|
|
|
$warnings = $run->error_context['warnings'] ?? null;
|
|
expect($warnings)->toBeArray()->toHaveCount(1);
|
|
expect($warnings[0]['type'] ?? null)->toBe('unsupported_reference');
|
|
|
|
expect(InventoryLink::query()->where('tenant_id', $tenant->getKey())->count())->toBe(0);
|
|
});
|
|
|
|
it('orders inbound/outbound edges by created_at desc and applies limit-only behavior', function () {
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
/** @var InventoryItem $item */
|
|
$item = InventoryItem::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
]);
|
|
|
|
$svc = app(DependencyQueryService::class);
|
|
|
|
InventoryLink::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'source_type' => 'inventory_item',
|
|
'source_id' => $item->external_id,
|
|
'target_type' => 'foundation_object',
|
|
'target_id' => '11111111-1111-1111-1111-111111111111',
|
|
'relationship_type' => 'assigned_to',
|
|
'created_at' => now()->subMinutes(10),
|
|
'updated_at' => now()->subMinutes(10),
|
|
]);
|
|
InventoryLink::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'source_type' => 'inventory_item',
|
|
'source_id' => $item->external_id,
|
|
'target_type' => 'foundation_object',
|
|
'target_id' => '22222222-2222-2222-2222-222222222222',
|
|
'relationship_type' => 'assigned_to',
|
|
'created_at' => now()->subMinutes(5),
|
|
'updated_at' => now()->subMinutes(5),
|
|
]);
|
|
InventoryLink::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'source_type' => 'inventory_item',
|
|
'source_id' => $item->external_id,
|
|
'target_type' => 'foundation_object',
|
|
'target_id' => '33333333-3333-3333-3333-333333333333',
|
|
'relationship_type' => 'assigned_to',
|
|
'created_at' => now()->subMinutes(1),
|
|
'updated_at' => now()->subMinutes(1),
|
|
]);
|
|
|
|
$outbound = $svc->getOutboundEdges($item, null, 2);
|
|
expect($outbound)->toHaveCount(2);
|
|
expect($outbound[0]->target_id)->toBe('33333333-3333-3333-3333-333333333333');
|
|
expect($outbound[1]->target_id)->toBe('22222222-2222-2222-2222-222222222222');
|
|
expect($outbound[0]->created_at->greaterThan($outbound[1]->created_at))->toBeTrue();
|
|
|
|
InventoryLink::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'source_type' => 'inventory_item',
|
|
'source_id' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
|
'target_type' => 'inventory_item',
|
|
'target_id' => $item->external_id,
|
|
'relationship_type' => 'depends_on',
|
|
'created_at' => now()->subMinutes(9),
|
|
'updated_at' => now()->subMinutes(9),
|
|
]);
|
|
InventoryLink::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'source_type' => 'inventory_item',
|
|
'source_id' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
|
'target_type' => 'inventory_item',
|
|
'target_id' => $item->external_id,
|
|
'relationship_type' => 'depends_on',
|
|
'created_at' => now()->subMinutes(2),
|
|
'updated_at' => now()->subMinutes(2),
|
|
]);
|
|
InventoryLink::factory()->create([
|
|
'tenant_id' => $tenant->getKey(),
|
|
'source_type' => 'inventory_item',
|
|
'source_id' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
|
'target_type' => 'inventory_item',
|
|
'target_id' => $item->external_id,
|
|
'relationship_type' => 'depends_on',
|
|
'created_at' => now()->subMinutes(1),
|
|
'updated_at' => now()->subMinutes(1),
|
|
]);
|
|
|
|
$inbound = $svc->getInboundEdges($item, null, 2);
|
|
expect($inbound)->toHaveCount(2);
|
|
expect($inbound[0]->source_id)->toBe('cccccccc-cccc-cccc-cccc-cccccccccccc');
|
|
expect($inbound[1]->source_id)->toBe('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
|
|
expect($inbound[0]->created_at->greaterThan($inbound[1]->created_at))->toBeTrue();
|
|
});
|