feat/004-assignments-scope-tags #4
@ -5,6 +5,7 @@
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Graph\AssignmentFetcher;
|
||||
use App\Services\Graph\AssignmentFilterResolver;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use App\Services\Graph\ScopeTagResolver;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@ -14,6 +15,7 @@ class AssignmentBackupService
|
||||
public function __construct(
|
||||
private readonly AssignmentFetcher $assignmentFetcher,
|
||||
private readonly GroupResolver $groupResolver,
|
||||
private readonly AssignmentFilterResolver $assignmentFilterResolver,
|
||||
private readonly ScopeTagResolver $scopeTagResolver,
|
||||
) {}
|
||||
|
||||
@ -80,13 +82,29 @@ public function enrichWithAssignments(
|
||||
|
||||
// Extract group IDs and resolve for orphan detection
|
||||
$groupIds = $this->extractGroupIds($assignments);
|
||||
$resolvedGroups = [];
|
||||
$hasOrphanedGroups = false;
|
||||
|
||||
if (! empty($groupIds)) {
|
||||
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId, $graphOptions);
|
||||
$hasOrphanedGroups = collect($resolvedGroups)->contains('orphaned', true);
|
||||
$hasOrphanedGroups = collect($resolvedGroups)
|
||||
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||
}
|
||||
|
||||
$filterIds = collect($assignments)
|
||||
->pluck('target.deviceAndAppManagementAssignmentFilterId')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
|
||||
$filterNames = collect($filters)
|
||||
->pluck('displayName', 'id')
|
||||
->all();
|
||||
|
||||
$assignments = $this->enrichAssignments($assignments, $resolvedGroups, $filterNames);
|
||||
|
||||
// Update backup item with assignments and metadata
|
||||
$metadata['assignment_count'] = count($assignments);
|
||||
$metadata['assignments_fetch_failed'] = false;
|
||||
@ -135,11 +153,42 @@ private function extractGroupIds(array $assignments): array
|
||||
$target = $assignment['target'] ?? [];
|
||||
$odataType = $target['@odata.type'] ?? '';
|
||||
|
||||
if ($odataType === '#microsoft.graph.groupAssignmentTarget' && isset($target['groupId'])) {
|
||||
if (in_array($odataType, [
|
||||
'#microsoft.graph.groupAssignmentTarget',
|
||||
'#microsoft.graph.exclusionGroupAssignmentTarget',
|
||||
], true) && isset($target['groupId'])) {
|
||||
$groupIds[] = $target['groupId'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($groupIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $assignments
|
||||
* @param array<string, array{id:string,displayName:?string,orphaned:bool}> $groups
|
||||
* @param array<string, string> $filterNames
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function enrichAssignments(array $assignments, array $groups, array $filterNames): array
|
||||
{
|
||||
return array_map(function (array $assignment) use ($groups, $filterNames): array {
|
||||
$target = $assignment['target'] ?? [];
|
||||
$groupId = $target['groupId'] ?? null;
|
||||
|
||||
if ($groupId && isset($groups[$groupId])) {
|
||||
$target['group_display_name'] = $groups[$groupId]['displayName'] ?? null;
|
||||
$target['group_orphaned'] = $groups[$groupId]['orphaned'] ?? false;
|
||||
}
|
||||
|
||||
$filterId = $target['deviceAndAppManagementAssignmentFilterId'] ?? null;
|
||||
if ($filterId && isset($filterNames[$filterId])) {
|
||||
$target['assignment_filter_name'] = $filterNames[$filterId];
|
||||
}
|
||||
|
||||
$assignment['target'] = $target;
|
||||
|
||||
return $assignment;
|
||||
}, $assignments);
|
||||
}
|
||||
}
|
||||
|
||||
54
app/Services/Graph/AssignmentFilterResolver.php
Normal file
54
app/Services/Graph/AssignmentFilterResolver.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Graph;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class AssignmentFilterResolver
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MicrosoftGraphClient $graphClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $filterIds
|
||||
* @return array<int, array{id:string,displayName:?string}>
|
||||
*/
|
||||
public function resolve(array $filterIds, ?Tenant $tenant = null): array
|
||||
{
|
||||
if (empty($filterIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$allFilters = $this->fetchAllFilters($tenant);
|
||||
|
||||
return array_values(array_filter($allFilters, function (array $filter) use ($filterIds): bool {
|
||||
return in_array($filter['id'] ?? null, $filterIds, true);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id:string,displayName:?string}>
|
||||
*/
|
||||
private function fetchAllFilters(?Tenant $tenant = null): array
|
||||
{
|
||||
$cacheKey = $tenant ? "assignment_filters:tenant:{$tenant->id}" : 'assignment_filters:all';
|
||||
|
||||
return Cache::remember($cacheKey, 3600, function () use ($tenant): array {
|
||||
$options = ['query' => ['$select' => 'id,displayName']];
|
||||
|
||||
if ($tenant) {
|
||||
$options = array_merge($options, $tenant->graphOptions());
|
||||
}
|
||||
|
||||
$response = $this->graphClient->request(
|
||||
'GET',
|
||||
'/deviceManagement/assignmentFilters',
|
||||
$options
|
||||
);
|
||||
|
||||
return $response->data['value'] ?? [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Graph\AssignmentFetcher;
|
||||
use App\Services\Graph\AssignmentFilterResolver;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use App\Services\Graph\ScopeTagResolver;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@ -22,6 +23,7 @@ public function __construct(
|
||||
private readonly PolicySnapshotService $snapshotService,
|
||||
private readonly AssignmentFetcher $assignmentFetcher,
|
||||
private readonly GroupResolver $groupResolver,
|
||||
private readonly AssignmentFilterResolver $assignmentFilterResolver,
|
||||
private readonly ScopeTagResolver $scopeTagResolver,
|
||||
) {}
|
||||
|
||||
@ -59,7 +61,7 @@ public function capture(
|
||||
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
|
||||
|
||||
if (! empty($rawAssignments)) {
|
||||
$assignments = $rawAssignments;
|
||||
$resolvedGroups = [];
|
||||
|
||||
// Resolve groups for orphaned detection
|
||||
$groupIds = collect($rawAssignments)
|
||||
@ -75,6 +77,19 @@ public function capture(
|
||||
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||
}
|
||||
|
||||
$filterIds = collect($rawAssignments)
|
||||
->pluck('target.deviceAndAppManagementAssignmentFilterId')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
|
||||
$filterNames = collect($filters)
|
||||
->pluck('displayName', 'id')
|
||||
->all();
|
||||
|
||||
$assignments = $this->enrichAssignments($rawAssignments, $resolvedGroups, $filterNames);
|
||||
$captureMetadata['assignments_count'] = count($rawAssignments);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@ -230,7 +245,7 @@ public function ensureVersionHasAssignments(
|
||||
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
|
||||
|
||||
if (! empty($rawAssignments)) {
|
||||
$assignments = $rawAssignments;
|
||||
$resolvedGroups = [];
|
||||
|
||||
// Resolve groups
|
||||
$groupIds = collect($rawAssignments)
|
||||
@ -246,6 +261,19 @@ public function ensureVersionHasAssignments(
|
||||
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||
}
|
||||
|
||||
$filterIds = collect($rawAssignments)
|
||||
->pluck('target.deviceAndAppManagementAssignmentFilterId')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
|
||||
$filterNames = collect($filters)
|
||||
->pluck('displayName', 'id')
|
||||
->all();
|
||||
|
||||
$assignments = $this->enrichAssignments($rawAssignments, $resolvedGroups, $filterNames);
|
||||
$metadata['assignments_count'] = count($rawAssignments);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@ -291,6 +319,34 @@ public function ensureVersionHasAssignments(
|
||||
return $version->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $assignments
|
||||
* @param array<string, array{id:string,displayName:?string,orphaned:bool}> $groups
|
||||
* @param array<string, string> $filterNames
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function enrichAssignments(array $assignments, array $groups, array $filterNames): array
|
||||
{
|
||||
return array_map(function (array $assignment) use ($groups, $filterNames): array {
|
||||
$target = $assignment['target'] ?? [];
|
||||
$groupId = $target['groupId'] ?? null;
|
||||
|
||||
if ($groupId && isset($groups[$groupId])) {
|
||||
$target['group_display_name'] = $groups[$groupId]['displayName'] ?? null;
|
||||
$target['group_orphaned'] = $groups[$groupId]['orphaned'] ?? false;
|
||||
}
|
||||
|
||||
$filterId = $target['deviceAndAppManagementAssignmentFilterId'] ?? null;
|
||||
if ($filterId && isset($filterNames[$filterId])) {
|
||||
$target['assignment_filter_name'] = $filterNames[$filterId];
|
||||
}
|
||||
|
||||
$assignment['target'] = $target;
|
||||
|
||||
return $assignment;
|
||||
}, $assignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $scopeTagIds
|
||||
* @return array{ids:array<int, string>,names:array<int, string>}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Graph\AssignmentFetcher;
|
||||
use App\Services\Graph\AssignmentFilterResolver;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use App\Services\Graph\ScopeTagResolver;
|
||||
use Carbon\CarbonImmutable;
|
||||
@ -17,6 +18,7 @@ public function __construct(
|
||||
private readonly PolicySnapshotService $snapshotService,
|
||||
private readonly AssignmentFetcher $assignmentFetcher,
|
||||
private readonly GroupResolver $groupResolver,
|
||||
private readonly AssignmentFilterResolver $assignmentFilterResolver,
|
||||
private readonly ScopeTagResolver $scopeTagResolver,
|
||||
) {}
|
||||
|
||||
@ -92,7 +94,7 @@ public function captureFromGraph(
|
||||
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
|
||||
|
||||
if (! empty($rawAssignments)) {
|
||||
$assignments = $rawAssignments;
|
||||
$resolvedGroups = [];
|
||||
|
||||
// Resolve groups
|
||||
$groupIds = collect($rawAssignments)
|
||||
@ -102,11 +104,27 @@ public function captureFromGraph(
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
if (! empty($groupIds)) {
|
||||
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
|
||||
}
|
||||
|
||||
$assignmentMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
|
||||
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||
$assignmentMetadata['assignments_count'] = count($rawAssignments);
|
||||
|
||||
$filterIds = collect($rawAssignments)
|
||||
->pluck('target.deviceAndAppManagementAssignmentFilterId')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
|
||||
$filterNames = collect($filters)
|
||||
->pluck('displayName', 'id')
|
||||
->all();
|
||||
|
||||
$assignments = $this->enrichAssignments($rawAssignments, $resolvedGroups, $filterNames);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$assignmentMetadata['assignments_fetch_failed'] = true;
|
||||
@ -135,6 +153,34 @@ public function captureFromGraph(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $assignments
|
||||
* @param array<string, array{id:string,displayName:?string,orphaned:bool}> $groups
|
||||
* @param array<string, string> $filterNames
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function enrichAssignments(array $assignments, array $groups, array $filterNames): array
|
||||
{
|
||||
return array_map(function (array $assignment) use ($groups, $filterNames): array {
|
||||
$target = $assignment['target'] ?? [];
|
||||
$groupId = $target['groupId'] ?? null;
|
||||
|
||||
if ($groupId && isset($groups[$groupId])) {
|
||||
$target['group_display_name'] = $groups[$groupId]['displayName'] ?? null;
|
||||
$target['group_orphaned'] = $groups[$groupId]['orphaned'] ?? false;
|
||||
}
|
||||
|
||||
$filterId = $target['deviceAndAppManagementAssignmentFilterId'] ?? null;
|
||||
if ($filterId && isset($filterNames[$filterId])) {
|
||||
$target['assignment_filter_name'] = $filterNames[$filterId];
|
||||
}
|
||||
|
||||
$assignment['target'] = $target;
|
||||
|
||||
return $assignment;
|
||||
}, $assignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $scopeTagIds
|
||||
* @return array{ids:array<int, string>,names:array<int, string>}
|
||||
|
||||
@ -57,14 +57,19 @@
|
||||
$intent = $assignment['intent'] ?? 'apply';
|
||||
|
||||
$typeName = match($type) {
|
||||
'#microsoft.graph.groupAssignmentTarget' => 'Group',
|
||||
'#microsoft.graph.groupAssignmentTarget' => 'Include group',
|
||||
'#microsoft.graph.exclusionGroupAssignmentTarget' => 'Exclude group',
|
||||
'#microsoft.graph.allLicensedUsersAssignmentTarget' => 'All Users',
|
||||
'#microsoft.graph.allDevicesAssignmentTarget' => 'All Devices',
|
||||
default => 'Unknown'
|
||||
};
|
||||
|
||||
$groupId = $target['groupId'] ?? null;
|
||||
$hasOrphaned = $version->metadata['has_orphaned_assignments'] ?? false;
|
||||
$groupName = $target['group_display_name'] ?? null;
|
||||
$groupOrphaned = $target['group_orphaned'] ?? ($version->metadata['has_orphaned_assignments'] ?? false);
|
||||
$filterId = $target['deviceAndAppManagementAssignmentFilterId'] ?? null;
|
||||
$filterType = $target['deviceAndAppManagementAssignmentFilterType'] ?? 'none';
|
||||
$filterName = $target['assignment_filter_name'] ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
@ -73,9 +78,16 @@
|
||||
|
||||
@if($groupId)
|
||||
<span class="text-gray-600 dark:text-gray-400">:</span>
|
||||
@if($hasOrphaned)
|
||||
@if($groupOrphaned)
|
||||
<span class="text-warning-600 dark:text-warning-400">
|
||||
⚠️ Unknown Group (ID: {{ $groupId }})
|
||||
⚠️ Unknown group (ID: {{ $groupId }})
|
||||
</span>
|
||||
@elseif($groupName)
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
{{ $groupName }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-500">
|
||||
({{ $groupId }})
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
@ -84,6 +96,12 @@
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if($filterId && $filterType !== 'none')
|
||||
<span class="text-xs text-gray-500 dark:text-gray-500">
|
||||
Filter ({{ $filterType }}): {{ $filterName ?? $filterId }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<span class="ml-auto text-xs text-gray-500 dark:text-gray-500">({{ $intent }})</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@ -115,4 +133,3 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@ -39,6 +39,19 @@
|
||||
'target' => [
|
||||
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||
'groupId' => 'group-123',
|
||||
'assignment_filter_name' => 'Targeted Devices',
|
||||
'deviceAndAppManagementAssignmentFilterId' => 'filter-1',
|
||||
'deviceAndAppManagementAssignmentFilterType' => 'include',
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'assignment-2',
|
||||
'intent' => 'apply',
|
||||
'target' => [
|
||||
'@odata.type' => '#microsoft.graph.exclusionGroupAssignmentTarget',
|
||||
'groupId' => 'group-456',
|
||||
'deviceAndAppManagementAssignmentFilterId' => null,
|
||||
'deviceAndAppManagementAssignmentFilterType' => 'none',
|
||||
],
|
||||
],
|
||||
],
|
||||
@ -54,7 +67,10 @@
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSeeLivewire('policy-version-assignments-widget');
|
||||
$response->assertSee('1 assignment(s)');
|
||||
$response->assertSee('2 assignment(s)');
|
||||
$response->assertSee('Include group');
|
||||
$response->assertSee('Exclude group');
|
||||
$response->assertSee('Filter (include): Targeted Devices');
|
||||
});
|
||||
|
||||
it('displays empty state when version has no assignments', function () {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
use App\Models\Policy;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Graph\AssignmentFetcher;
|
||||
use App\Services\Graph\AssignmentFilterResolver;
|
||||
use App\Services\Graph\GroupResolver;
|
||||
use App\Services\Graph\ScopeTagResolver;
|
||||
use App\Services\Intune\PolicySnapshotService;
|
||||
@ -47,6 +48,8 @@
|
||||
'target' => [
|
||||
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||
'groupId' => 'group-123',
|
||||
'deviceAndAppManagementAssignmentFilterId' => 'filter-123',
|
||||
'deviceAndAppManagementAssignmentFilterType' => 'include',
|
||||
],
|
||||
],
|
||||
]);
|
||||
@ -64,6 +67,14 @@
|
||||
]);
|
||||
});
|
||||
|
||||
$this->mock(AssignmentFilterResolver::class, function ($mock) {
|
||||
$mock->shouldReceive('resolve')
|
||||
->once()
|
||||
->andReturn([
|
||||
['id' => 'filter-123', 'displayName' => 'Targeted Devices'],
|
||||
]);
|
||||
});
|
||||
|
||||
$versionService = app(VersionService::class);
|
||||
$version = $versionService->captureFromGraph(
|
||||
$this->tenant,
|
||||
@ -82,6 +93,9 @@
|
||||
'ids' => ['0'],
|
||||
'names' => ['Default'],
|
||||
]);
|
||||
|
||||
expect($version->assignments[0]['target']['group_display_name'])->toBe('Test Group');
|
||||
expect($version->assignments[0]['target']['assignment_filter_name'])->toBe('Targeted Devices');
|
||||
});
|
||||
|
||||
it('captures policy version without assignments when none exist', function () {
|
||||
|
||||
65
tests/Unit/AssignmentFilterResolverTest.php
Normal file
65
tests/Unit/AssignmentFilterResolverTest.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Graph\AssignmentFilterResolver;
|
||||
use App\Services\Graph\GraphResponse;
|
||||
use App\Services\Graph\MicrosoftGraphClient;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Cache::flush();
|
||||
$this->graphClient = Mockery::mock(MicrosoftGraphClient::class);
|
||||
$this->resolver = new AssignmentFilterResolver($this->graphClient);
|
||||
});
|
||||
|
||||
test('resolves assignment filters by id', function () {
|
||||
$filters = [
|
||||
['id' => 'filter-1', 'displayName' => 'Targeted Devices'],
|
||||
['id' => 'filter-2', 'displayName' => 'Excluded Devices'],
|
||||
];
|
||||
|
||||
$response = new GraphResponse(
|
||||
success: true,
|
||||
data: ['value' => $filters]
|
||||
);
|
||||
|
||||
$this->graphClient
|
||||
->shouldReceive('request')
|
||||
->once()
|
||||
->with('GET', '/deviceManagement/assignmentFilters', [
|
||||
'query' => [
|
||||
'$select' => 'id,displayName',
|
||||
],
|
||||
])
|
||||
->andReturn($response);
|
||||
|
||||
$result = $this->resolver->resolve(['filter-1']);
|
||||
|
||||
expect($result)->toHaveCount(1)
|
||||
->and($result[0]['id'])->toBe('filter-1')
|
||||
->and($result[0]['displayName'])->toBe('Targeted Devices');
|
||||
});
|
||||
|
||||
test('uses cache for repeated lookups', function () {
|
||||
$filters = [
|
||||
['id' => 'filter-1', 'displayName' => 'Targeted Devices'],
|
||||
];
|
||||
|
||||
$response = new GraphResponse(
|
||||
success: true,
|
||||
data: ['value' => $filters]
|
||||
);
|
||||
|
||||
$this->graphClient
|
||||
->shouldReceive('request')
|
||||
->once()
|
||||
->andReturn($response);
|
||||
|
||||
$result1 = $this->resolver->resolve(['filter-1']);
|
||||
$result2 = $this->resolver->resolve(['filter-1']);
|
||||
|
||||
expect($result1)->toBe($result2);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user