TenantAtlas/tests/Feature/TermsAndConditionsPolicyTypeTest.php
ahmido 83f1814254 feat/024-terms-and-conditions (#30)
Added termsAndConditions to the supported policy list and Graph contract so Intune sync/backup/restore paths (and scope tag handling) treat Terms & Conditions like other enrollment policies, ensuring listings, snapshots, assignments CRUD, and restore modes flow naturally (tenantpilot.php (lines 168-225), graph_contracts.php (lines 520-560), InteractsWithODataTypes.php (lines 10-30)).
Exposed a dedicated TermsAndConditionsNormalizer and tagged it in AppServiceProvider so the Filament UI shows readable rows (display name, title, acceptance statement, body, scope tags) and the diff engine flattens them consistently (TermsAndConditionsNormalizer.php (lines 1-94), AppServiceProvider.php (lines 43-58)).
Added Pest coverage for the new type that checks config/contract entries, assignment restore behavior, normalized output, and PolicySync ingestion (TermsAndConditionsPolicyTypeTest.php (lines 70-200)).
Tests:

TermsAndConditionsPolicyTypeTest.php
./vendor/bin/pint --dirty

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #30
2026-01-04 03:01:11 +00:00

219 lines
7.3 KiB
PHP

<?php
use App\Models\Policy;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphLogger;
use App\Services\Graph\GraphResponse;
use App\Services\Intune\PolicyNormalizer;
use App\Services\Intune\PolicySyncService;
use App\Services\Intune\RestoreService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\mock;
uses(RefreshDatabase::class);
class TermsAndConditionsRestoreGraphClient implements GraphClientInterface
{
/**
* @var array<int, array{method:string,path:string,payload:array|null}>
*/
public array $requestCalls = [];
/**
* @param array<int, GraphResponse> $requestResponses
*/
public function __construct(
private readonly GraphResponse $applyPolicyResponse,
private array $requestResponses = [],
) {}
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(true, ['payload' => []]);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return $this->applyPolicyResponse;
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(true, []);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
$this->requestCalls[] = [
'method' => strtoupper($method),
'path' => $path,
'payload' => $options['json'] ?? null,
];
return array_shift($this->requestResponses) ?? new GraphResponse(true, []);
}
}
it('includes terms and conditions policy type in supported types', function () {
$byType = collect(config('tenantpilot.supported_policy_types', []))
->keyBy('type');
expect($byType)->toHaveKey('termsAndConditions');
expect($byType['termsAndConditions']['endpoint'] ?? null)->toBe('deviceManagement/termsAndConditions');
});
it('defines terms and conditions graph contract with assignments paths', function () {
$contract = config('graph_contracts.types.termsAndConditions');
expect($contract)->toBeArray();
expect($contract['resource'] ?? null)->toBe('deviceManagement/termsAndConditions');
expect($contract['assignments_list_path'] ?? null)->toBe('/deviceManagement/termsAndConditions/{id}/assignments');
expect($contract['assignments_payload_key'] ?? null)->toBe('termsAndConditionsAssignments');
});
it('restores terms and conditions assignments via assignments endpoint', function () {
$client = new TermsAndConditionsRestoreGraphClient(
applyPolicyResponse: new GraphResponse(true, []),
requestResponses: [
new GraphResponse(true, ['value' => []]), // existing assignments list
new GraphResponse(true, []), // create assignments
],
);
app()->instance(GraphClientInterface::class, $client);
$tenant = Tenant::factory()->create(['tenant_id' => 'tenant-1']);
$policy = Policy::factory()->create([
'tenant_id' => $tenant->id,
'external_id' => 'tc-1',
'policy_type' => 'termsAndConditions',
'platform' => 'all',
]);
$backupSet = \App\Models\BackupSet::factory()->create([
'tenant_id' => $tenant->id,
'status' => 'completed',
'item_count' => 1,
]);
$backupItem = \App\Models\BackupItem::factory()->create([
'tenant_id' => $tenant->id,
'backup_set_id' => $backupSet->id,
'policy_id' => $policy->id,
'policy_identifier' => $policy->external_id,
'policy_type' => $policy->policy_type,
'platform' => $policy->platform,
'payload' => [
'id' => $policy->external_id,
'@odata.type' => '#microsoft.graph.termsAndConditions',
],
'assignments' => [
[
'id' => 'assignment-1',
'intent' => 'apply',
'target' => [
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
'groupId' => 'source-group-1',
],
],
],
]);
$user = User::factory()->create(['email' => 'tester@example.com']);
$this->actingAs($user);
$service = app(RestoreService::class);
$service->execute(
tenant: $tenant,
backupSet: $backupSet,
selectedItemIds: [$backupItem->id],
dryRun: false,
actorEmail: $user->email,
actorName: $user->name,
groupMapping: [
'source-group-1' => 'target-group-1',
],
);
$postCalls = collect($client->requestCalls)
->filter(fn (array $call) => $call['method'] === 'POST')
->values();
expect($postCalls)->toHaveCount(1);
expect($postCalls[0]['path'])->toBe('/deviceManagement/termsAndConditions/tc-1/assignments');
$payload = $postCalls[0]['payload'] ?? [];
expect($payload['target']['groupId'] ?? null)->toBe('target-group-1');
});
it('normalizes terms and conditions key fields', function () {
$normalized = app(PolicyNormalizer::class)->normalize([
'@odata.type' => '#microsoft.graph.termsAndConditions',
'displayName' => 'Terms and Conditions Alpha',
'title' => 'Alpha terms',
'description' => 'Long form description',
'acceptanceStatement' => 'I agree',
'bodyText' => str_repeat('Line.', 100),
'version' => 3,
'roleScopeTagIds' => ['0', '1'],
], 'termsAndConditions', 'all');
$entries = $normalized['settings'][0]['entries'] ?? [];
$byKey = collect($entries)->keyBy('key');
expect($byKey['Display name']['value'] ?? null)->toBe('Terms and Conditions Alpha');
expect($byKey['Title']['value'] ?? null)->toBe('Alpha terms');
expect($byKey['Acceptance statement']['value'] ?? null)->toBe('I agree');
expect($byKey['Version']['value'] ?? null)->toBe(3);
expect($byKey['Scope tag IDs']['value'] ?? null)->toBe(['0', '1']);
});
it('syncs terms and conditions from graph', function () {
$tenant = Tenant::factory()->create(['status' => 'active']);
$logger = mock(GraphLogger::class);
$logger->shouldReceive('logRequest')
->zeroOrMoreTimes()
->andReturnNull();
$logger->shouldReceive('logResponse')
->zeroOrMoreTimes()
->andReturnNull();
mock(GraphClientInterface::class)
->shouldReceive('listPolicies')
->once()
->with('termsAndConditions', mockery::type('array'))
->andReturn(new GraphResponse(
success: true,
data: [
[
'id' => 'tc-1',
'displayName' => 'T&C',
'@odata.type' => '#microsoft.graph.termsAndConditions',
],
],
));
$service = app(PolicySyncService::class);
$service->syncPolicies($tenant, [
['type' => 'termsAndConditions', 'platform' => 'all'],
]);
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'termsAndConditions')->count())
->toBe(1);
});