TenantAtlas/tests/Unit/ScopeTagResolverTest.php
Ahmed Darrazi 86bb4cdbd6 feat: Phase 1+2 - Assignments & Scope Tags foundation
Phase 1: Setup & Database (13 tasks completed)
- Add assignments JSONB column to backup_items table
- Add group_mapping JSONB column to restore_runs table
- Extend BackupItem model with 7 assignment accessor methods
- Extend RestoreRun model with 8 group mapping helper methods
- Add scopeWithAssignments() query scope to BackupItem
- Update graph_contracts.php with assignments endpoints
- Create 5 factories: BackupItem, RestoreRun, Tenant, BackupSet, Policy
- Add 30 unit tests (15 BackupItem, 15 RestoreRun) - all passing

Phase 2: Graph API Integration (16 tasks completed)
- Create AssignmentFetcher service with fallback strategy
- Create GroupResolver service with orphaned ID handling
- Create ScopeTagResolver service with 1-hour caching
- Implement fail-soft error handling for all services
- Add 17 unit tests (5 AssignmentFetcher, 6 GroupResolver, 6 ScopeTagResolver) - all passing
- Total: 71 assertions across all Phase 2 tests

Test Results:
- Phase 1: 30/30 tests passing (45 assertions)
- Phase 2: 17/17 tests passing (71 assertions)
- Total: 47/47 tests passing (116 assertions)
- Code formatted with Pint (PSR-12 compliant)

Next: Phase 3 - US1 Backup with Assignments (12 tasks)
2025-12-22 02:10:35 +01:00

138 lines
3.8 KiB
PHP

<?php
use App\Services\Graph\GraphException;
use App\Services\Graph\GraphLogger;
use App\Services\Graph\MicrosoftGraphClient;
use App\Services\Graph\ScopeTagResolver;
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->logger = Mockery::mock(GraphLogger::class);
$this->resolver = new ScopeTagResolver($this->graphClient, $this->logger);
});
test('resolves scope tags', function () {
$scopeTagIds = ['0', '123', '456'];
$allScopeTags = [
['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'],
['id' => '456', 'displayName' => 'Finance-Admins'],
['id' => '789', 'displayName' => 'IT-Admins'],
];
$this->graphClient
->shouldReceive('get')
->once()
->with('/deviceManagement/roleScopeTags', null, ['$select' => 'id,displayName'])
->andReturn(['value' => $allScopeTags]);
$this->logger
->shouldReceive('logDebug')
->once()
->with('Fetched scope tags', ['count' => 4]);
$result = $this->resolver->resolve($scopeTagIds);
expect($result)->toHaveCount(3)
->and(array_column($result, 'id'))->toContain('0', '123', '456')
->and(array_column($result, 'id'))->not->toContain('789');
});
test('caches results', function () {
$scopeTagIds = ['0', '123'];
$allScopeTags = [
['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'],
];
// First call - should hit Graph API
$this->graphClient
->shouldReceive('get')
->once()
->andReturn(['value' => $allScopeTags]);
$this->logger
->shouldReceive('logDebug')
->once();
$result1 = $this->resolver->resolve($scopeTagIds);
// Second call with different IDs - should use cached data (no new Graph call)
$result2 = $this->resolver->resolve(['0']);
expect($result1)->toHaveCount(2)
->and($result2)->toHaveCount(1)
->and($result2[0]['id'])->toBe('0');
});
test('returns empty array for empty input', function () {
$result = $this->resolver->resolve([]);
expect($result)->toBe([]);
});
test('handles graph exception gracefully', function () {
$scopeTagIds = ['0', '123'];
$this->graphClient
->shouldReceive('get')
->once()
->andThrow(new GraphException('Graph API error', 500, ['request_id' => 'request-id-123']));
$this->logger
->shouldReceive('logWarning')
->once()
->with('Failed to fetch scope tags', Mockery::on(function ($context) {
return isset($context['context']['request_id']);
}));
$result = $this->resolver->resolve($scopeTagIds);
expect($result)->toBe([]);
});
test('filters correctly when scope tag not in cache', function () {
$scopeTagIds = ['999']; // ID that doesn't exist
$allScopeTags = [
['id' => '0', 'displayName' => 'Default'],
['id' => '123', 'displayName' => 'HR-Admins'],
];
$this->graphClient
->shouldReceive('get')
->once()
->andReturn(['value' => $allScopeTags]);
$this->logger
->shouldReceive('logDebug')
->once();
$result = $this->resolver->resolve($scopeTagIds);
expect($result)->toBeEmpty();
});
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();
});