*/ public array $requestCalls = []; /** * @param array $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); });