TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec420M365GenericEvidenceCaptureTest.php
Ahmed Darrazi 9405058433
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 5m46s
feat: complete m365 generic evidence coverage pack
2026-06-27 13:00:22 +02:00

213 lines
9.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\TenantConfiguration\GenericContentEvidenceCaptureService;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\TenantConfiguration\CanonicalKeyKind;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\IdentityState;
it('Spec420 captures Conditional Access evidence and blocks selected missing-contract types', function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
config()->set('graph_contracts.types.conditionalAccessPolicy.volatile_fields', ['modifiedDateTime']);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'scopes_granted' => ['Policy.Read.All', 'Directory.Read.All'],
]);
$graph = spec420CaptureGraphClient([
'conditionalAccessPolicy' => [
[
'id' => 'cap-1',
'displayName' => 'Require MFA',
'state' => 'enabled',
'modifiedDateTime' => '2026-06-27T10:00:00Z',
'clientSecret' => 'spec420-provider-client-secret',
'tokenClaims' => [
'access_token' => 'spec420-provider-access-token',
],
'conditions' => ['users' => ['includeUsers' => ['All']]],
],
],
]);
app()->instance(GraphClientInterface::class, $graph);
$run = spec420CaptureRun($user, $environment, $connection, [
'conditionalAccessPolicy',
'acceptedDomain',
'appPermissionPolicy',
'dlpCompliancePolicy',
]);
$result = app(GenericContentEvidenceCaptureService::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalTypes: ['conditionalAccessPolicy', 'acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy'],
);
expect($graph->calls)->toHaveCount(1)
->and($graph->calls[0]['policy_type'])->toBe('conditionalAccessPolicy')
->and($result['summary_counts'])->toMatchArray([
'total' => 4,
'processed' => 4,
'succeeded' => 1,
'skipped' => 3,
'failed' => 0,
'errors_recorded' => 0,
])
->and($result['run_outcome'])->toBe(OperationRunOutcome::Succeeded->value);
$outcomes = collect($result['outcomes'])->keyBy('canonical_type');
expect($outcomes['conditionalAccessPolicy']['outcome'])->toBe(CaptureOutcome::Captured->value)
->and($outcomes['conditionalAccessPolicy']['source_contract_key'])->toBe('conditionalAccessPolicy')
->and($outcomes['acceptedDomain']['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value)
->and($outcomes['appPermissionPolicy']['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value)
->and($outcomes['dlpCompliancePolicy']['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value);
$resource = TenantConfigurationResource::query()->sole();
$evidence = TenantConfigurationResourceEvidence::query()->sole();
expect($resource->canonical_type)->toBe('conditionalAccessPolicy')
->and($resource->workspace_id)->toBe((int) $environment->workspace_id)
->and($resource->managed_environment_id)->toBe((int) $environment->getKey())
->and($resource->provider_connection_id)->toBe((int) $connection->getKey())
->and($resource->canonical_key_kind)->toBe(CanonicalKeyKind::GraphObjectId)
->and($resource->latest_identity_state)->toBe(IdentityState::Stable)
->and($resource->latest_claim_state)->toBe(ClaimState::InternalOnly)
->and($resource->source_identity['strategy_identifier'])->toBe('graph.conditional_access_policy.v1')
->and($resource->source_metadata['source_contract_key'])->toBe('conditionalAccessPolicy')
->and($resource->source_metadata['registry_source_class'])->toBe('tcm')
->and($resource->source_metadata['registry_support_state'])->toBe('out_of_scope');
expect($evidence->resource_id)->toBe((int) $resource->getKey())
->and($evidence->operation_run_id)->toBe((int) $run->getKey())
->and($evidence->source_contract_key)->toBe('conditionalAccessPolicy')
->and($evidence->source_endpoint)->toBe('/identity/conditionalAccess/policies')
->and($evidence->coverage_level)->toBe(CoverageLevel::ContentBacked)
->and($evidence->evidence_state)->toBe(EvidenceState::ContentBacked)
->and($evidence->capture_outcome)->toBe(CaptureOutcome::Captured)
->and($evidence->raw_payload['id'])->toBe('cap-1')
->and($evidence->raw_payload['clientSecret'])->toBe('spec420-provider-client-secret')
->and($evidence->normalized_payload)->not->toHaveKey('modifiedDateTime')
->and($evidence->normalized_payload['clientSecret'])->toBe('[redacted]')
->and($evidence->normalized_payload['tokenClaims'])->toBe('[redacted]')
->and(json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR))->not->toContain('spec420-provider-client-secret')
->and(json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR))->not->toContain('spec420-provider-access-token')
->and($evidence->payload_hash)->toBeString()->toHaveLength(64)
->and($evidence->permission_context['scopes_granted'])->toBe(['Policy.Read.All', 'Directory.Read.All']);
});
it('Spec420 creates no fake evidence rows for selected missing-contract types', function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$graph = spec420CaptureGraphClient([]);
app()->instance(GraphClientInterface::class, $graph);
$run = spec420CaptureRun($user, $environment, $connection, [
'acceptedDomain',
'appPermissionPolicy',
'dlpCompliancePolicy',
]);
$result = app(GenericContentEvidenceCaptureService::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalTypes: ['acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy'],
);
expect($graph->calls)->toBe([])
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0)
->and($result['summary_counts'])->toMatchArray([
'total' => 3,
'processed' => 3,
'succeeded' => 0,
'skipped' => 3,
'failed' => 0,
])
->and($result['run_outcome'])->toBe(OperationRunOutcome::Blocked->value);
});
function spec420CaptureRun($user, $environment, ProviderConnection $connection, array $resourceTypes): OperationRun
{
return OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => $resourceTypes,
'required_capability' => 'evidence.manage',
],
]);
}
function spec420CaptureGraphClient(array $responses): GraphClientInterface
{
return new class($responses) implements GraphClientInterface
{
public array $calls = [];
public function __construct(private readonly array $responses) {}
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
$this->calls[] = ['policy_type' => $policyType, 'options' => $options];
return new GraphResponse(true, $this->responses[$policyType] ?? []);
}
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
{
return new GraphResponse(false, [], 501);
}
};
}