## Summary - move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling - update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location - add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation` - integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404` ## Remaining Rollout Checks - validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout - confirm web, queue, and scheduler processes all start from the expected working directory in staging/production - verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #213
240 lines
8.0 KiB
PHP
240 lines
8.0 KiB
PHP
<?php
|
|
|
|
use App\Models\BackupItem;
|
|
use App\Models\Tenant;
|
|
use App\Services\AssignmentBackupService;
|
|
use App\Services\Graph\AssignmentFetcher;
|
|
use App\Services\Graph\AssignmentFilterResolver;
|
|
use App\Services\Graph\GroupResolver;
|
|
use App\Services\Graph\ScopeTagResolver;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Mockery\MockInterface;
|
|
|
|
uses(RefreshDatabase::class);
|
|
it('enriches assignment filter names when filter data is stored at root', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'tenant_id' => 'tenant-123',
|
|
'external_id' => 'tenant-123',
|
|
]);
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft');
|
|
|
|
$backupItem = BackupItem::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'metadata' => [],
|
|
'assignments' => null,
|
|
]);
|
|
|
|
$policyPayload = [
|
|
'roleScopeTagIds' => ['0'],
|
|
];
|
|
|
|
$this->mock(AssignmentFetcher::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('supportsStandardAssignments')
|
|
->once()
|
|
->with('settingsCatalogPolicy', null)
|
|
->andReturnTrue();
|
|
|
|
$mock->shouldReceive('fetch')
|
|
->once()
|
|
->andReturn([
|
|
[
|
|
'id' => 'assignment-1',
|
|
'intent' => 'apply',
|
|
'deviceAndAppManagementAssignmentFilterId' => 'filter-123',
|
|
'deviceAndAppManagementAssignmentFilterType' => 'include',
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
|
'groupId' => 'group-123',
|
|
],
|
|
],
|
|
]);
|
|
});
|
|
|
|
$this->mock(GroupResolver::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('resolveGroupIds')
|
|
->once()
|
|
->andReturn([
|
|
'group-123' => [
|
|
'id' => 'group-123',
|
|
'displayName' => 'Test Group',
|
|
'orphaned' => false,
|
|
],
|
|
]);
|
|
});
|
|
|
|
$this->mock(AssignmentFilterResolver::class, function (MockInterface $mock) use ($tenant) {
|
|
$mock->shouldReceive('resolve')
|
|
->once()
|
|
->with(['filter-123'], $tenant)
|
|
->andReturn([
|
|
['id' => 'filter-123', 'displayName' => 'Targeted Devices'],
|
|
]);
|
|
});
|
|
|
|
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) use ($tenant) {
|
|
$mock->shouldReceive('resolve')
|
|
->once()
|
|
->with(['0'], $tenant)
|
|
->andReturn([
|
|
['id' => '0', 'displayName' => 'Default'],
|
|
]);
|
|
});
|
|
|
|
$service = app(AssignmentBackupService::class);
|
|
$updated = $service->enrichWithAssignments(
|
|
backupItem: $backupItem,
|
|
tenant: $tenant,
|
|
policyType: 'settingsCatalogPolicy',
|
|
policyId: 'policy-123',
|
|
policyPayload: $policyPayload,
|
|
includeAssignments: true
|
|
);
|
|
|
|
expect($updated->assignments)->toHaveCount(1)
|
|
->and($updated->assignments[0]['target']['deviceAndAppManagementAssignmentFilterId'])->toBe('filter-123')
|
|
->and($updated->assignments[0]['target']['deviceAndAppManagementAssignmentFilterType'])->toBe('include')
|
|
->and($updated->assignments[0]['target']['assignment_filter_name'])->toBe('Targeted Devices');
|
|
});
|
|
|
|
it('marks role definitions as not using standard policy assignments', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'tenant_id' => 'tenant-123',
|
|
'external_id' => 'tenant-123',
|
|
]);
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft');
|
|
|
|
$backupItem = BackupItem::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'metadata' => [
|
|
'assignments_fetch_failed' => true,
|
|
'assignments_fetch_error' => 'old error',
|
|
],
|
|
'assignments' => null,
|
|
]);
|
|
|
|
$this->mock(AssignmentFetcher::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('supportsStandardAssignments')
|
|
->once()
|
|
->with('intuneRoleDefinition', '#microsoft.graph.roleDefinition')
|
|
->andReturnFalse();
|
|
|
|
$mock->shouldReceive('fetch')->never();
|
|
});
|
|
|
|
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) use ($tenant) {
|
|
$mock->shouldReceive('resolve')
|
|
->once()
|
|
->with(['0'], $tenant)
|
|
->andReturn([
|
|
['id' => '0', 'displayName' => 'Default'],
|
|
]);
|
|
});
|
|
|
|
$this->mock(GroupResolver::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('resolveGroupIds')->never();
|
|
});
|
|
|
|
$this->mock(AssignmentFilterResolver::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('resolve')->never();
|
|
});
|
|
|
|
$service = app(AssignmentBackupService::class);
|
|
$updated = $service->enrichWithAssignments(
|
|
backupItem: $backupItem,
|
|
tenant: $tenant,
|
|
policyType: 'intuneRoleDefinition',
|
|
policyId: 'role-def-1',
|
|
policyPayload: [
|
|
'@odata.type' => '#microsoft.graph.roleDefinition',
|
|
'roleScopeTagIds' => ['0'],
|
|
],
|
|
includeAssignments: true,
|
|
);
|
|
|
|
expect($updated->assignments)->toBe([])
|
|
->and(data_get($updated->metadata, 'assignments_not_applicable'))->toBeTrue()
|
|
->and(data_get($updated->metadata, 'assignment_capture_reason'))->toBe('separate_role_assignments')
|
|
->and(data_get($updated->metadata, 'assignments_fetch_failed'))->toBeFalse()
|
|
->and(data_get($updated->metadata, 'assignments_fetch_error'))->toBeNull();
|
|
});
|
|
|
|
it('records orphaned assignment metadata when resolved groups are missing in the tenant', function () {
|
|
$tenant = Tenant::factory()->create([
|
|
'tenant_id' => 'tenant-123',
|
|
'external_id' => 'tenant-123',
|
|
]);
|
|
|
|
ensureDefaultProviderConnection($tenant, 'microsoft');
|
|
|
|
$backupItem = BackupItem::factory()->create([
|
|
'tenant_id' => $tenant->id,
|
|
'metadata' => [],
|
|
'assignments' => null,
|
|
]);
|
|
|
|
$this->mock(AssignmentFetcher::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('supportsStandardAssignments')
|
|
->once()
|
|
->with('settingsCatalogPolicy', null)
|
|
->andReturnTrue();
|
|
|
|
$mock->shouldReceive('fetch')
|
|
->once()
|
|
->andReturn([
|
|
[
|
|
'id' => 'assignment-1',
|
|
'intent' => 'apply',
|
|
'target' => [
|
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
|
'groupId' => 'group-123',
|
|
],
|
|
],
|
|
]);
|
|
});
|
|
|
|
$this->mock(GroupResolver::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('resolveGroupIds')
|
|
->once()
|
|
->andReturn([
|
|
'group-123' => [
|
|
'id' => 'group-123',
|
|
'displayName' => 'Missing Group',
|
|
'orphaned' => true,
|
|
],
|
|
]);
|
|
});
|
|
|
|
$this->mock(AssignmentFilterResolver::class, function (MockInterface $mock) {
|
|
$mock->shouldReceive('resolve')
|
|
->once()
|
|
->with([], \Mockery::type(Tenant::class))
|
|
->andReturn([]);
|
|
});
|
|
|
|
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) use ($tenant) {
|
|
$mock->shouldReceive('resolve')
|
|
->once()
|
|
->with(['0'], $tenant)
|
|
->andReturn([
|
|
['id' => '0', 'displayName' => 'Default'],
|
|
]);
|
|
});
|
|
|
|
$updated = app(AssignmentBackupService::class)->enrichWithAssignments(
|
|
backupItem: $backupItem,
|
|
tenant: $tenant,
|
|
policyType: 'settingsCatalogPolicy',
|
|
policyId: 'policy-123',
|
|
policyPayload: [
|
|
'roleScopeTagIds' => ['0'],
|
|
],
|
|
includeAssignments: true,
|
|
);
|
|
|
|
expect($updated->assignments)->toHaveCount(1)
|
|
->and(data_get($updated->metadata, 'assignments_fetch_failed'))->toBeFalse()
|
|
->and(data_get($updated->metadata, 'has_orphaned_assignments'))->toBeTrue();
|
|
});
|