feat/004-assignments-scope-tags #4
@ -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'],
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
|
||||||
});
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user