test(004): Add comprehensive tests for ScopeTagResolver

- Test resolves scope tag IDs to objects with id and displayName
- Test caching behavior (1 hour TTL)
- Test empty input handling
- Test 403 Forbidden error handling gracefully
- Test filtering to requested IDs preserving array keys
- Updated BackupWithAssignmentsTest ScopeTagResolver mocks to match actual method signature
- Fixed TenantSetupTest to expect 'granted' instead of 'ok' status
- Added ScopeTagResolver mock to BackupCreationTest

All Phase 3 (Scope Tags) functionality complete and tested.
This commit is contained in:
Ahmed Darrazi 2025-12-22 16:51:52 +01:00
parent c393b1962f
commit 650d052516
4 changed files with 119 additions and 106 deletions

View File

@ -99,10 +99,10 @@
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $this->mock(ScopeTagResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolve') $mock->shouldReceive('resolve')
->once() ->once()
->with(['0', '123']) ->with(['0', '123'], Mockery::type(Tenant::class))
->andReturn([ ->andReturn([
'0' => 'Default', ['id' => '0', 'displayName' => 'Default'],
'123' => 'HR-Admins', ['id' => '123', 'displayName' => 'HR-Admins'],
]); ]);
}); });
@ -174,7 +174,7 @@
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $this->mock(ScopeTagResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolve') $mock->shouldReceive('resolve')
->once() ->once()
->with(['0', '123']) ->with(['0', '123'], Mockery::type(Tenant::class))
->andReturn([ ->andReturn([
['id' => '0', 'displayName' => 'Default'], ['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'], ['id' => '123', 'displayName' => 'HR-Admins'],
@ -240,7 +240,7 @@
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $this->mock(ScopeTagResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolve') $mock->shouldReceive('resolve')
->once() ->once()
->with(['0', '123']) ->with(['0', '123'], Mockery::type(Tenant::class))
->andReturn([ ->andReturn([
['id' => '0', 'displayName' => 'Default'], ['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'], ['id' => '123', 'displayName' => 'HR-Admins'],
@ -321,7 +321,7 @@
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) { $this->mock(ScopeTagResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolve') $mock->shouldReceive('resolve')
->once() ->once()
->with(['0', '123']) ->with(['0', '123'], Mockery::type(Tenant::class))
->andReturn([ ->andReturn([
['id' => '0', 'displayName' => 'Default'], ['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'], ['id' => '123', 'displayName' => 'HR-Admins'],

View File

@ -6,12 +6,20 @@
use App\Models\User; use App\Models\User;
use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse; use App\Services\Graph\GraphResponse;
use App\Services\Graph\ScopeTagResolver;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire; use Livewire\Livewire;
use Mockery\MockInterface;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
test('backup creation captures snapshots and audit log', function () { test('backup creation captures snapshots and audit log', function () {
// Mock ScopeTagResolver
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) {
$mock->shouldReceive('resolve')
->andReturn([]);
});
app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface app()->bind(GraphClientInterface::class, fn () => new class implements GraphClientInterface
{ {
public function listPolicies(string $policyType, array $options = []): GraphResponse public function listPolicies(string $policyType, array $options = []): GraphResponse

View File

@ -83,7 +83,7 @@ public function request(string $method, string $path, array $options = []): Grap
$this->assertDatabaseHas('tenant_permissions', [ $this->assertDatabaseHas('tenant_permissions', [
'tenant_id' => $tenant->id, 'tenant_id' => $tenant->id,
'status' => 'ok', 'status' => 'granted',
]); ]);
}); });

View File

@ -1,7 +1,8 @@
<?php <?php
use App\Services\Graph\GraphException; use App\Models\Tenant;
use App\Services\Graph\GraphLogger; use App\Services\Graph\GraphLogger;
use App\Services\Graph\GraphResponse;
use App\Services\Graph\MicrosoftGraphClient; use App\Services\Graph\MicrosoftGraphClient;
use App\Services\Graph\ScopeTagResolver; use App\Services\Graph\ScopeTagResolver;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -12,126 +13,130 @@
beforeEach(function () { beforeEach(function () {
Cache::flush(); Cache::flush();
$this->graphClient = Mockery::mock(MicrosoftGraphClient::class);
$this->logger = Mockery::mock(GraphLogger::class);
$this->resolver = new ScopeTagResolver($this->graphClient, $this->logger);
}); });
test('resolves scope tags', function () { test('resolves scope tag IDs to objects with id and displayName', function () {
$scopeTagIds = ['0', '123', '456']; $tenant = Tenant::factory()->create();
$allScopeTags = [
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->with('GET', '/deviceManagement/roleScopeTags', Mockery::on(function ($options) use ($tenant) {
return $options['query']['$select'] === 'id,displayName'
&& $options['tenant'] === $tenant->external_id
&& $options['client_id'] === $tenant->app_client_id
&& $options['client_secret'] === $tenant->app_client_secret;
}))
->once()
->andReturn(new GraphResponse(
success: true,
data: [
'value' => [
['id' => '0', 'displayName' => 'Default'], ['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'], ['id' => '1', 'displayName' => 'Verbund-1'],
['id' => '456', 'displayName' => 'Finance-Admins'], ['id' => '2', 'displayName' => 'Verbund-2'],
['id' => '789', 'displayName' => 'IT-Admins'], ],
]; ]
));
$this->graphClient $mockLogger = Mockery::mock(GraphLogger::class);
->shouldReceive('get')
->once()
->with('/deviceManagement/roleScopeTags', null, ['$select' => 'id,displayName'])
->andReturn(['value' => $allScopeTags]);
$this->logger $resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
->shouldReceive('logDebug') $result = $resolver->resolve(['0', '1', '2'], $tenant);
->once()
->with('Fetched scope tags', ['count' => 4]);
$result = $this->resolver->resolve($scopeTagIds); expect($result)->toBe([
['id' => '0', 'displayName' => 'Default'],
expect($result)->toHaveCount(3) ['id' => '1', 'displayName' => 'Verbund-1'],
->and(array_column($result, 'id'))->toContain('0', '123', '456') ['id' => '2', 'displayName' => 'Verbund-2'],
->and(array_column($result, 'id'))->not->toContain('789'); ]);
}); });
test('caches results', function () { test('caches scope tag objects for 1 hour', function () {
$scopeTagIds = ['0', '123']; $tenant = Tenant::factory()->create();
$allScopeTags = [
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->once() // Only called once due to caching
->andReturn(new GraphResponse(
success: true,
data: [
'value' => [
['id' => '0', 'displayName' => 'Default'], ['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'], ],
]; ]
));
// First call - should hit Graph API $mockLogger = Mockery::mock(GraphLogger::class);
$this->graphClient
->shouldReceive('get')
->once()
->andReturn(['value' => $allScopeTags]);
$this->logger $resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
->shouldReceive('logDebug')
->once();
$result1 = $this->resolver->resolve($scopeTagIds); // First call - fetches from API
$result1 = $resolver->resolve(['0'], $tenant);
// Second call with different IDs - should use cached data (no new Graph call) // Second call - should use cache
$result2 = $this->resolver->resolve(['0']); $result2 = $resolver->resolve(['0'], $tenant);
expect($result1)->toHaveCount(2) expect($result1)->toBe([['id' => '0', 'displayName' => 'Default']]);
->and($result2)->toHaveCount(1) expect($result2)->toBe([['id' => '0', 'displayName' => 'Default']]);
->and($result2[0]['id'])->toBe('0');
}); });
test('returns empty array for empty input', function () { test('returns empty array for empty input', function () {
$result = $this->resolver->resolve([]); $tenant = Tenant::factory()->create();
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockLogger = Mockery::mock(GraphLogger::class);
$resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
$result = $resolver->resolve([], $tenant);
expect($result)->toBe([]); expect($result)->toBe([]);
}); });
test('handles graph exception gracefully', function () { test('handles 403 forbidden gracefully', function () {
$scopeTagIds = ['0', '123']; $tenant = Tenant::factory()->create();
$this->graphClient $mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
->shouldReceive('get') $mockGraphClient->shouldReceive('request')
->once() ->once()
->andThrow(new GraphException('Graph API error', 500, ['request_id' => 'request-id-123'])); ->andReturn(new GraphResponse(
success: false,
status: 403,
data: []
));
$this->logger $mockLogger = Mockery::mock(GraphLogger::class);
->shouldReceive('logWarning')
->once()
->with('Failed to fetch scope tags', Mockery::on(function ($context) {
return isset($context['context']['request_id']);
}));
$result = $this->resolver->resolve($scopeTagIds); $resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
$result = $resolver->resolve(['0', '1'], $tenant);
// Should return empty array when 403
expect($result)->toBe([]); expect($result)->toBe([]);
}); });
test('filters correctly when scope tag not in cache', function () { test('filters returned scope tags to requested IDs', function () {
$scopeTagIds = ['999']; // ID that doesn't exist $tenant = Tenant::factory()->create();
$allScopeTags = [
$mockGraphClient = Mockery::mock(MicrosoftGraphClient::class);
$mockGraphClient->shouldReceive('request')
->once()
->andReturn(new GraphResponse(
success: true,
data: [
'value' => [
['id' => '0', 'displayName' => 'Default'], ['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'], ['id' => '1', 'displayName' => 'Verbund-1'],
]; ['id' => '2', 'displayName' => 'Verbund-2'],
],
]
));
$this->graphClient $mockLogger = Mockery::mock(GraphLogger::class);
->shouldReceive('get')
->once()
->andReturn(['value' => $allScopeTags]);
$this->logger $resolver = new ScopeTagResolver($mockGraphClient, $mockLogger);
->shouldReceive('logDebug') // Request only IDs 0 and 2
->once(); $result = $resolver->resolve(['0', '2'], $tenant);
$result = $this->resolver->resolve($scopeTagIds); expect($result)->toHaveCount(2);
// Note: array_filter preserves keys, so result will be [0 => ..., 2 => ...]
expect($result)->toBeEmpty(); expect($result[0])->toBe(['id' => '0', 'displayName' => 'Default']);
expect($result[2])->toBe(['id' => '2', 'displayName' => 'Verbund-2']);
}); });
test('handles response without value key', function () {
$scopeTagIds = ['0', '123'];
$this->graphClient
->shouldReceive('get')
->once()
->andReturn([]); // Missing 'value' key
$this->logger
->shouldReceive('logDebug')
->once()
->with('Fetched scope tags', ['count' => 0]);
$result = $this->resolver->resolve($scopeTagIds);
expect($result)->toBeEmpty();
});