resolve(spec424SecurityDefaultsUnitResourceType()); expect($decision->outcome)->toBe(CaptureOutcome::Captured) ->and($decision->contractKey)->toBe('securityDefaults') ->and($decision->sourceEndpoint)->toBe('/policies/identitySecurityDefaultsEnforcementPolicy') ->and($decision->sourceVersion)->toBe('v1.0') ->and($decision->sourceSchemaHash)->toBeString()->not->toBe('') ->and($decision->sourceMetadata['source_contract_key'])->toBe('securityDefaults') ->and($decision->sourceMetadata['registry_source_class'])->toBe('graph_v1_fallback') ->and($decision->sourceMetadata['registry_support_state'])->toBe('fallback_supported'); }); it('Spec424 blocks Security Defaults capture when the graph contract resource is missing', function (): void { config()->set('graph_contracts.types.securityDefaults', []); $decision = (new CoverageSourceContractResolver(new GraphContractRegistry)) ->resolve(spec424SecurityDefaultsUnitResourceType()); expect($decision->outcome)->toBe(CaptureOutcome::BlockedMissingContract) ->and($decision->reasonCode)->toBe('missing_graph_contract_resource') ->and($decision->contractKey)->toBeNull() ->and($decision->sourceEndpoint)->toBeNull() ->and($decision->sourceMetadata['reason_code'])->toBe('missing_graph_contract_resource'); }); it('Spec424 declares a bounded Security Defaults graph contract', function (): void { $contract = config('graph_contracts.types.securityDefaults'); expect($contract['resource'])->toBe('policies/identitySecurityDefaultsEnforcementPolicy') ->and($contract['graph_version'])->toBe('v1.0') ->and($contract['response_shape'])->toBe('singleton') ->and($contract['allowed_select'])->toBe(['id', 'displayName', 'description', 'isEnabled']) ->and($contract['allowed_expand'])->toBe([]) ->and($contract['volatile_fields'])->toBe(['@odata.context', '@odata.etag']) ->and($contract['read_permissions'])->toBe(['Policy.Read.All']) ->and($contract)->not->toHaveKey('create_method') ->and($contract)->not->toHaveKey('update_method'); }); it('Spec424 list policies calls the Security Defaults v1.0 endpoint without using beta', function (): void { config()->set('graph.base_url', 'https://graph.microsoft.com'); config()->set('graph.version', 'beta'); Http::fake([ 'https://graph.microsoft.com/*' => Http::response([ 'id' => 'securityDefaults', 'displayName' => 'Security Defaults', 'description' => 'Tenant-wide defaults', 'isEnabled' => true, ], 200), ]); $logger = mock(GraphLogger::class); $logger->shouldReceive('logRequest')->zeroOrMoreTimes()->andReturnNull(); $logger->shouldReceive('logResponse')->zeroOrMoreTimes()->andReturnNull(); $response = (new MicrosoftGraphClient( logger: $logger, contracts: app(GraphContractRegistry::class), ))->listPolicies('securityDefaults', [ 'access_token' => 'spec424-test-token', 'top' => 999, ]); expect($response->successful())->toBeTrue() ->and($response->data['id'])->toBe('securityDefaults'); Http::assertSent(function (Request $request): bool { $url = $request->url(); if (! str_contains($url, '/v1.0/policies/identitySecurityDefaultsEnforcementPolicy')) { return false; } if (str_contains($url, '/beta/')) { return false; } parse_str((string) parse_url($url, PHP_URL_QUERY), $query); expect($query['$select'] ?? null)->toBe('id,displayName,description,isEnabled'); expect($query)->not->toHaveKey('$top'); return true; }); }); it('Spec424 live graph contract check probes the Security Defaults singleton without top', function (): void { $client = new class implements GraphClientInterface { public array $requests = []; public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function request(string $method, string $path, array $options = []): GraphResponse { $this->requests[] = [ 'method' => $method, 'path' => $path, 'options' => $options, ]; return new GraphResponse(true, []); } }; app()->instance(GraphClientInterface::class, $client); $this->artisan('graph:contract:check')->assertSuccessful(); $request = collect($client->requests) ->firstWhere('path', 'policies/identitySecurityDefaultsEnforcementPolicy'); expect($request)->not->toBeNull() ->and($request['options']['query'])->toHaveKey('$select', 'id,displayName,description,isEnabled') ->and($request['options']['query'])->not->toHaveKey('$top') ->and($request['options']['graph_version'] ?? null)->toBe('v1.0'); }); it('Spec424 resolves Security Defaults identity only from a stable Graph id', function (): void { $resourceType = spec424SecurityDefaultsUnitResourceType(); $stable = app(CanonicalIdentityResolver::class)->resolve($resourceType, [ 'id' => 'securityDefaults', 'displayName' => 'Security Defaults', 'isEnabled' => true, ], [ 'source_contract_key' => 'securityDefaults', 'source_version' => 'v1.0', ]); $missing = app(CanonicalIdentityResolver::class)->resolve($resourceType, [ 'displayName' => 'Security Defaults', 'isEnabled' => true, ], [ 'source_contract_key' => 'securityDefaults', 'source_version' => 'v1.0', ]); $sourceIdOnly = app(CanonicalIdentityResolver::class)->resolve($resourceType, [ 'sourceId' => 'securityDefaults-provider-alias', 'displayName' => 'Security Defaults', 'isEnabled' => true, ], [ 'source_contract_key' => 'securityDefaults', 'source_version' => 'v1.0', ]); expect($stable->identityState)->toBe(IdentityState::Stable) ->and($stable->keyKind)->toBe(CanonicalKeyKind::GraphObjectId) ->and($stable->sourceResourceId)->toBe('securityDefaults') ->and($stable->strategyIdentifier)->toBe('graph.security_defaults.v1') ->and($missing->identityState)->toBe(IdentityState::MissingExternalId) ->and($missing->keyKind)->toBe(CanonicalKeyKind::Unsupported) ->and($missing->diagnostics['reason_code'])->toBe('missing_external_id') ->and($missing->canonicalResourceKey)->not->toContain('displayName') ->and($sourceIdOnly->identityState)->toBe(IdentityState::MissingExternalId) ->and($sourceIdOnly->keyKind)->toBe(CanonicalKeyKind::Unsupported) ->and($sourceIdOnly->diagnostics['reason_code'])->toBe('missing_external_id') ->and($sourceIdOnly->sourceResourceId)->toStartWith('missing:'); }); function spec424SecurityDefaultsUnitResourceType(): TenantConfigurationResourceType { $definition = collect(ResourceTypeRegistry::defaultDefinitions()) ->firstWhere('canonical_type', 'securityDefaults'); expect($definition)->not->toBeNull('Missing default resource type definition for securityDefaults.'); return new TenantConfigurationResourceType($definition); }