From eec93b510a625154501b413263ace415a3da88c2 Mon Sep 17 00:00:00 2001 From: ahmido Date: Sun, 15 Feb 2026 15:02:27 +0000 Subject: [PATCH] Spec 095: Graph contracts registry completeness + registry-backed call sites (#114) Implements Spec 095. What changed - Registers 4 Graph resources in the contract registry (plus required subresource template) - Refactors in-scope call sites to resolve Graph paths via the registry (no ad-hoc endpoints for these resources) - Adds/updates regression tests to prevent future drift (missing registry entries and endpoint string reintroduction) - Includes full SpecKit artifacts under specs/095-graph-contracts-registry-completeness/ Validation - Focused tests: - `vendor/bin/sail artisan test --compact tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php tests/Feature/SettingsCatalogDefinitionResolverTest.php` Notes - Livewire v4.0+ / Filament v5 compliant (no UI changes). - No new routes/pages; no RBAC model changes. Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/114 --- .github/agents/copilot-instructions.md | 3 +- app/Services/Graph/GraphContractRegistry.php | 83 +++++++++++ .../ConfigurationPolicyTemplateResolver.php | 8 +- app/Services/Intune/RbacHealthService.php | 5 +- app/Services/Intune/RbacOnboardingService.php | 23 +-- .../SettingsCatalogCategoryResolver.php | 8 +- .../SettingsCatalogDefinitionResolver.php | 8 +- config/graph_contracts.php | 29 ++++ .../checklists/requirements.md | 35 +++++ .../graph-deviceManagement-contracts.yaml | 86 +++++++++++ .../data-model.md | 22 +++ .../plan.md | 125 ++++++++++++++++ .../quickstart.md | 23 +++ .../research.md | 40 ++++++ .../spec.md | 125 ++++++++++++++++ .../tasks.md | 134 ++++++++++++++++++ ...aphContractRegistryCoverageSpec095Test.php | 74 ++++++++++ .../SettingsCatalogDefinitionResolverTest.php | 13 +- 18 files changed, 819 insertions(+), 25 deletions(-) create mode 100644 specs/095-graph-contracts-registry-completeness/checklists/requirements.md create mode 100644 specs/095-graph-contracts-registry-completeness/contracts/graph-deviceManagement-contracts.yaml create mode 100644 specs/095-graph-contracts-registry-completeness/data-model.md create mode 100644 specs/095-graph-contracts-registry-completeness/plan.md create mode 100644 specs/095-graph-contracts-registry-completeness/quickstart.md create mode 100644 specs/095-graph-contracts-registry-completeness/research.md create mode 100644 specs/095-graph-contracts-registry-completeness/spec.md create mode 100644 specs/095-graph-contracts-registry-completeness/tasks.md create mode 100644 tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index bd971cf..bcf1b99 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -28,6 +28,7 @@ ## Active Technologies - PostgreSQL (primary) + session (workspace context + last-tenant memory) (085-tenant-operate-hub) - PHP 8.4 (Laravel 12) + Filament v5, Livewire v4, Laravel Sail, Tailwind CSS v4 (085-tenant-operate-hub) - PostgreSQL (Sail), SQLite in tests (087-legacy-runs-removal) +- PHP 8.4.x + Laravel 12, Filament v5, Livewire v4, Microsoft Graph integration via `GraphClientInterface` (095-graph-contracts-registry-completeness) - PHP 8.4.15 (feat/005-bulk-operations) @@ -47,8 +48,8 @@ ## Code Style PHP 8.4.15: Follow standard conventions ## Recent Changes +- 095-graph-contracts-registry-completeness: Added PHP 8.4.x + Laravel 12, Filament v5, Livewire v4, Microsoft Graph integration via `GraphClientInterface` - 090-action-surface-contract-compliance: Added PHP 8.4.15 - 087-legacy-runs-removal: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4 -- 088-remove-tenant-graphoptions-legacy: Added PHP 8.4.15 (Laravel 12) + Filament v5, Livewire v4, Pest v4 diff --git a/app/Services/Graph/GraphContractRegistry.php b/app/Services/Graph/GraphContractRegistry.php index 59b25ea..0774b38 100644 --- a/app/Services/Graph/GraphContractRegistry.php +++ b/app/Services/Graph/GraphContractRegistry.php @@ -49,6 +49,89 @@ public function directoryRoleDefinitionsListPath(): string return '/'.ltrim($resource, '/'); } + public function configurationPolicyTemplatePolicyType(): string + { + return 'configurationPolicyTemplate'; + } + + public function configurationPolicyTemplateListPath(): string + { + $resource = $this->resourcePath($this->configurationPolicyTemplatePolicyType()) ?? 'deviceManagement/configurationPolicyTemplates'; + + return '/'.ltrim($resource, '/'); + } + + public function configurationPolicyTemplateItemPath(string $templateId): string + { + return sprintf('%s/%s', $this->configurationPolicyTemplateListPath(), urlencode($templateId)); + } + + public function configurationPolicyTemplateSettingTemplatesPath(string $templateId): string + { + $path = $this->subresourcePath( + $this->configurationPolicyTemplatePolicyType(), + 'settingTemplates', + ['{id}' => $templateId] + ); + + if (! is_string($path) || $path === '') { + return sprintf('%s/%s/settingTemplates', $this->configurationPolicyTemplateListPath(), urlencode($templateId)); + } + + return '/'.ltrim($path, '/'); + } + + public function settingsCatalogDefinitionPolicyType(): string + { + return 'settingsCatalogDefinition'; + } + + public function settingsCatalogDefinitionListPath(): string + { + $resource = $this->resourcePath($this->settingsCatalogDefinitionPolicyType()) ?? 'deviceManagement/configurationSettings'; + + return '/'.ltrim($resource, '/'); + } + + public function settingsCatalogDefinitionItemPath(string $definitionId): string + { + return sprintf('%s/%s', $this->settingsCatalogDefinitionListPath(), urlencode($definitionId)); + } + + public function settingsCatalogCategoryPolicyType(): string + { + return 'settingsCatalogCategory'; + } + + public function settingsCatalogCategoryListPath(): string + { + $resource = $this->resourcePath($this->settingsCatalogCategoryPolicyType()) ?? 'deviceManagement/configurationCategories'; + + return '/'.ltrim($resource, '/'); + } + + public function settingsCatalogCategoryItemPath(string $categoryId): string + { + return sprintf('%s/%s', $this->settingsCatalogCategoryListPath(), urlencode($categoryId)); + } + + public function rbacRoleAssignmentPolicyType(): string + { + return 'rbacRoleAssignment'; + } + + public function rbacRoleAssignmentListPath(): string + { + $resource = $this->resourcePath($this->rbacRoleAssignmentPolicyType()) ?? 'deviceManagement/roleAssignments'; + + return '/'.ltrim($resource, '/'); + } + + public function rbacRoleAssignmentItemPath(string $assignmentId): string + { + return sprintf('%s/%s', $this->rbacRoleAssignmentListPath(), urlencode($assignmentId)); + } + /** * @return array */ diff --git a/app/Services/Intune/ConfigurationPolicyTemplateResolver.php b/app/Services/Intune/ConfigurationPolicyTemplateResolver.php index 085d147..e81a8b8 100644 --- a/app/Services/Intune/ConfigurationPolicyTemplateResolver.php +++ b/app/Services/Intune/ConfigurationPolicyTemplateResolver.php @@ -4,6 +4,7 @@ use App\Models\Tenant; use App\Services\Graph\GraphClientInterface; +use App\Services\Graph\GraphContractRegistry; use App\Services\Providers\MicrosoftGraphOptionsResolver; use Illuminate\Support\Arr; @@ -26,6 +27,7 @@ class ConfigurationPolicyTemplateResolver public function __construct( private readonly GraphClientInterface $graphClient, + private readonly GraphContractRegistry $contracts, private readonly MicrosoftGraphOptionsResolver $graphOptionsResolver, ) {} @@ -161,7 +163,7 @@ public function getTemplate(Tenant $tenant, string $templateId, array $graphOpti $this->graphOptionsResolver->resolveForTenant($tenant), Arr::except($graphOptions, ['platform']), ); - $path = sprintf('/deviceManagement/configurationPolicyTemplates/%s', urlencode($templateId)); + $path = $this->contracts->configurationPolicyTemplateItemPath($templateId); $response = $this->graphClient->request('GET', $path, $context); if ($response->failed()) { @@ -201,7 +203,7 @@ public function listTemplatesByFamily(Tenant $tenant, string $templateFamily, ar ], ]); - $response = $this->graphClient->request('GET', '/deviceManagement/configurationPolicyTemplates', $context); + $response = $this->graphClient->request('GET', $this->contracts->configurationPolicyTemplateListPath(), $context); if ($response->failed()) { return $this->familyCache[$tenantKey][$cacheKey] = [ @@ -240,7 +242,7 @@ public function fetchTemplateSettingDefinitionIds(Tenant $tenant, string $templa ], ]); - $path = sprintf('/deviceManagement/configurationPolicyTemplates/%s/settingTemplates', urlencode($templateId)); + $path = $this->contracts->configurationPolicyTemplateSettingTemplatesPath($templateId); $response = $this->graphClient->request('GET', $path, $context); if ($response->failed()) { diff --git a/app/Services/Intune/RbacHealthService.php b/app/Services/Intune/RbacHealthService.php index bb3f7a2..d14a050 100644 --- a/app/Services/Intune/RbacHealthService.php +++ b/app/Services/Intune/RbacHealthService.php @@ -5,6 +5,7 @@ use App\Models\ProviderConnection; use App\Models\Tenant; use App\Services\Graph\GraphClientInterface; +use App\Services\Graph\GraphContractRegistry; use App\Services\Providers\ProviderConnectionResolver; use App\Services\Providers\ProviderGateway; use App\Support\Providers\ProviderReasonCodes; @@ -16,6 +17,7 @@ class RbacHealthService { public function __construct( private readonly GraphClientInterface $graph, + private readonly GraphContractRegistry $contracts, private readonly ?ProviderConnectionResolver $providerConnections = null, private readonly ?ProviderGateway $providerGateway = null, ) {} @@ -54,7 +56,8 @@ public function check(Tenant $tenant): array } if ($tenant->rbac_role_assignment_id) { - $response = $this->graph->request('GET', "deviceManagement/roleAssignments/{$tenant->rbac_role_assignment_id}", $context); + $assignmentPath = $this->contracts->rbacRoleAssignmentItemPath($tenant->rbac_role_assignment_id); + $response = $this->graph->request('GET', $assignmentPath, $context); if ($response->successful()) { $assignment = $response->data; diff --git a/app/Services/Intune/RbacOnboardingService.php b/app/Services/Intune/RbacOnboardingService.php index e00df36..db4a289 100644 --- a/app/Services/Intune/RbacOnboardingService.php +++ b/app/Services/Intune/RbacOnboardingService.php @@ -6,6 +6,7 @@ use App\Models\Tenant; use App\Models\User; use App\Services\Graph\GraphClientInterface; +use App\Services\Graph\GraphContractRegistry; use App\Services\Providers\ProviderConnectionResolver; use App\Services\Providers\ProviderGateway; use App\Support\Providers\ProviderReasonCodes; @@ -20,6 +21,7 @@ class RbacOnboardingService public function __construct( private readonly GraphClientInterface $graph, + private readonly GraphContractRegistry $contracts, private readonly AuditLogger $auditLogger, private readonly ?ProviderConnectionResolver $providerConnections = null, private readonly ?ProviderGateway $providerGateway = null, @@ -408,8 +410,9 @@ private function ensureRoleAssignment(string $roleDefinitionId, string $groupId, $desiredScopes = $scope === 'scope_group' && $scopeGroupId ? [$scopeGroupId] : ['/']; + $roleAssignmentsPath = $this->contracts->rbacRoleAssignmentListPath(); - $assignments = $this->graph->request('GET', 'deviceManagement/roleAssignments', [ + $assignments = $this->graph->request('GET', $roleAssignmentsPath, [ 'query' => [ '$select' => 'id,displayName,resourceScopes,members', '$expand' => 'roleDefinition($select=id,displayName)', @@ -421,7 +424,8 @@ private function ensureRoleAssignment(string $roleDefinitionId, string $groupId, $error = $this->extractErrorMessage($assignments->errors, $assignments->data); throw new RuntimeException(sprintf( - 'step=listRoleAssignments path=/deviceManagement/roleAssignments status=%s error=%s', + 'step=listRoleAssignments path=%s status=%s error=%s', + $roleAssignmentsPath, $status, $error )); @@ -450,7 +454,8 @@ private function ensureRoleAssignment(string $roleDefinitionId, string $groupId, return ['id' => $matching['id'] ?? null, 'action' => 'role_assignment_exists']; } - $update = $this->graph->request('PATCH', "deviceManagement/roleAssignments/{$matching['id']}", [ + $updatePath = $this->contracts->rbacRoleAssignmentItemPath((string) $matching['id']); + $update = $this->graph->request('PATCH', $updatePath, [ 'json' => ['resourceScopes' => $desiredScopes], ] + $context); @@ -469,8 +474,8 @@ private function ensureRoleAssignment(string $roleDefinitionId, string $groupId, } throw new RuntimeException(sprintf( - 'step=updateRoleAssignment path=/deviceManagement/roleAssignments/%s status=%s error=%s', - $matching['id'], + 'step=updateRoleAssignment path=%s status=%s error=%s', + $updatePath, $update->status ?? 'unknown', $error )); @@ -479,7 +484,7 @@ private function ensureRoleAssignment(string $roleDefinitionId, string $groupId, return ['id' => $matching['id'] ?? null, 'action' => 'role_assignment_updated']; } - $create = $this->graph->request('POST', 'deviceManagement/roleAssignments', [ + $create = $this->graph->request('POST', $roleAssignmentsPath, [ 'json' => [ 'displayName' => "TenantPilot RBAC - {$roleDefinitionId}", 'description' => 'TenantPilot automated RBAC setup', @@ -520,7 +525,8 @@ private function ensureRoleAssignment(string $roleDefinitionId, string $groupId, } $details = sprintf( - 'step=createRoleAssignment path=/deviceManagement/roleAssignments status=%s error=%s', + 'step=createRoleAssignment path=%s status=%s error=%s', + $roleAssignmentsPath, $create->status ?? 'unknown', $error ); @@ -561,7 +567,8 @@ private function hydrateAssignmentMembers(array $assignments, array $context): a return $assignment; } - $membersResponse = $this->graph->request('GET', "deviceManagement/roleAssignments/{$assignment['id']}", [ + $membersPath = $this->contracts->rbacRoleAssignmentItemPath((string) $assignment['id']); + $membersResponse = $this->graph->request('GET', $membersPath, [ 'query' => [ '$select' => 'id,displayName,resourceScopes,members', '$expand' => 'roleDefinition($select=id,displayName)', diff --git a/app/Services/Intune/SettingsCatalogCategoryResolver.php b/app/Services/Intune/SettingsCatalogCategoryResolver.php index c21acf9..b950a92 100644 --- a/app/Services/Intune/SettingsCatalogCategoryResolver.php +++ b/app/Services/Intune/SettingsCatalogCategoryResolver.php @@ -4,6 +4,7 @@ use App\Models\SettingsCatalogCategory; use App\Services\Graph\GraphClientInterface; +use App\Services\Graph\GraphContractRegistry; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; @@ -14,7 +15,8 @@ class SettingsCatalogCategoryResolver private const CACHE_TTL = 3600; // 1 hour in memory public function __construct( - private readonly GraphClientInterface $graphClient + private readonly GraphClientInterface $graphClient, + private readonly GraphContractRegistry $contracts, ) {} /** @@ -138,12 +140,12 @@ private function fetchFromGraph(array $categoryIds): array $categories = []; // Fetch each category individually - // Endpoint: /deviceManagement/configurationCategories/{categoryId} foreach ($categoryIds as $categoryId) { try { + $path = $this->contracts->settingsCatalogCategoryItemPath($categoryId); $response = $this->graphClient->request( 'GET', - "/deviceManagement/configurationCategories/{$categoryId}" + $path ); if ($response->successful() && isset($response->data)) { diff --git a/app/Services/Intune/SettingsCatalogDefinitionResolver.php b/app/Services/Intune/SettingsCatalogDefinitionResolver.php index 8566be3..cd8d654 100644 --- a/app/Services/Intune/SettingsCatalogDefinitionResolver.php +++ b/app/Services/Intune/SettingsCatalogDefinitionResolver.php @@ -4,6 +4,7 @@ use App\Models\SettingsCatalogDefinition; use App\Services\Graph\GraphClientInterface; +use App\Services\Graph\GraphContractRegistry; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -15,7 +16,8 @@ class SettingsCatalogDefinitionResolver private const MEMORY_CACHE_PREFIX = 'settings_catalog_def_'; public function __construct( - private GraphClientInterface $graphClient + private GraphClientInterface $graphClient, + private GraphContractRegistry $contracts, ) {} /** @@ -162,7 +164,6 @@ private function fetchFromGraph(array $definitionIds): array // Note: Microsoft Graph API does not support "in" operator for $filter. // We fetch each definition individually. - // Endpoint: /deviceManagement/configurationSettings/{definitionId} foreach ($definitionIds as $definitionId) { // Skip template IDs with placeholders - these are not real definition IDs if (str_contains($definitionId, '{') || str_contains($definitionId, '}')) { @@ -172,9 +173,10 @@ private function fetchFromGraph(array $definitionIds): array } try { + $path = $this->contracts->settingsCatalogDefinitionItemPath($definitionId); $response = $this->graphClient->request( 'GET', - "/deviceManagement/configurationSettings/{$definitionId}" + $path ); if ($response->successful() && isset($response->data)) { diff --git a/config/graph_contracts.php b/config/graph_contracts.php index 2544b04..56f3752 100644 --- a/config/graph_contracts.php +++ b/config/graph_contracts.php @@ -38,6 +38,35 @@ 'allowed_select' => ['id', 'complianceState'], 'allowed_expand' => [], ], + 'configurationPolicyTemplate' => [ + 'resource' => 'deviceManagement/configurationPolicyTemplates', + 'allowed_select' => ['id', 'displayName', 'displayVersion', 'templateFamily'], + 'allowed_expand' => [], + 'subresources' => [ + 'settingTemplates' => [ + 'path' => 'deviceManagement/configurationPolicyTemplates/{id}/settingTemplates', + 'collection' => true, + 'paging' => true, + 'allowed_select' => [], + 'allowed_expand' => ['settingDefinitions'], + ], + ], + ], + 'settingsCatalogDefinition' => [ + 'resource' => 'deviceManagement/configurationSettings', + 'allowed_select' => ['id', 'displayName', 'description', 'helpText', 'categoryId', 'uxBehavior'], + 'allowed_expand' => [], + ], + 'settingsCatalogCategory' => [ + 'resource' => 'deviceManagement/configurationCategories', + 'allowed_select' => ['id', 'displayName', 'description'], + 'allowed_expand' => [], + ], + 'rbacRoleAssignment' => [ + 'resource' => 'deviceManagement/roleAssignments', + 'allowed_select' => ['id', 'displayName', 'resourceScopes', 'members'], + 'allowed_expand' => ['roleDefinition($select=id,displayName)'], + ], 'deviceConfiguration' => [ 'resource' => 'deviceManagement/deviceConfigurations', 'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version', 'lastModifiedDateTime'], diff --git a/specs/095-graph-contracts-registry-completeness/checklists/requirements.md b/specs/095-graph-contracts-registry-completeness/checklists/requirements.md new file mode 100644 index 0000000..5632ef8 --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Graph Contracts Registry Completeness + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-02-15 +**Feature**: [specs/095-graph-contracts-registry-completeness/spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Validation pass: Spec is ready for `/speckit.plan`. +- Note: The spec includes internal contract registry identifiers to keep requirements testable and unambiguous; it still avoids implementation/stack details and external endpoint strings. diff --git a/specs/095-graph-contracts-registry-completeness/contracts/graph-deviceManagement-contracts.yaml b/specs/095-graph-contracts-registry-completeness/contracts/graph-deviceManagement-contracts.yaml new file mode 100644 index 0000000..fb2c28a --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/contracts/graph-deviceManagement-contracts.yaml @@ -0,0 +1,86 @@ +openapi: 3.0.0 +info: + title: TenantPilot — Microsoft Graph deviceManagement contracts (Spec 095) + version: 0.1.0 + description: > + Minimal external API contract documentation for the Microsoft Graph resources + governed by Spec 095 (Graph Contracts Registry Completeness). +servers: + - url: https://graph.microsoft.com/v1.0 +paths: + /deviceManagement/configurationPolicyTemplates: + get: + summary: List configuration policy templates + responses: + '200': + description: OK + /deviceManagement/configurationPolicyTemplates/{templateId}/settingTemplates: + get: + summary: List setting templates for a configuration policy template + parameters: + - name: templateId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /deviceManagement/configurationSettings/{settingId}: + get: + summary: Get a configuration setting definition + parameters: + - name: settingId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /deviceManagement/configurationCategories/{categoryId}: + get: + summary: Get a configuration category + parameters: + - name: categoryId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + /deviceManagement/roleAssignments: + get: + summary: List role assignments + responses: + '200': + description: OK + post: + summary: Create a role assignment + responses: + '201': + description: Created + /deviceManagement/roleAssignments/{roleAssignmentId}: + get: + summary: Get a role assignment + parameters: + - name: roleAssignmentId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + patch: + summary: Update a role assignment + parameters: + - name: roleAssignmentId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK diff --git a/specs/095-graph-contracts-registry-completeness/data-model.md b/specs/095-graph-contracts-registry-completeness/data-model.md new file mode 100644 index 0000000..e743bfb --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/data-model.md @@ -0,0 +1,22 @@ +# Phase 1 — Data Model: Graph Contracts Registry Completeness + +## Summary + +This feature introduces **no new database entities** and does not modify any existing schema. + +## Affected “Entities” (Configuration-only) + +Although no database model changes occur, the feature affects the following configuration concepts: + +- **Graph Contract Type**: A named entry in `config/graph_contracts.php` representing a Microsoft Graph resource. +- **Graph Contract Subresource**: A named sub-path template belonging to a contract type (used to model nested resources). + +## Ownership & Scope + +- **Ownership**: Workspace scope. +- **Persistence**: Configuration only; no new tables/records. + +## Validation Rules + +- Contract type identifiers must be stable and used consistently in code. +- Resource paths must be representable via the registry and reusable by call sites. diff --git a/specs/095-graph-contracts-registry-completeness/plan.md b/specs/095-graph-contracts-registry-completeness/plan.md new file mode 100644 index 0000000..229ab92 --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/plan.md @@ -0,0 +1,125 @@ +# Implementation Plan: Graph Contracts Registry Completeness + +**Branch**: `095-graph-contracts-registry-completeness` | **Date**: 2026-02-15 | **Spec**: [specs/095-graph-contracts-registry-completeness/spec.md](spec.md) +**Input**: Feature specification from [specs/095-graph-contracts-registry-completeness/spec.md](spec.md) + +## Summary + +This change closes governance gaps in the Microsoft Graph contract registry by explicitly registering four Graph resources already used by the product (templates, settings catalog definitions, categories, role assignments), refactoring a small set of known call sites to use registry-backed paths, and adding regression tests to prevent future “untracked” Graph usage. + +Clarified constraints: +- Enforce registry-backed paths only for these four resources and the five known call sites. +- Acceptance evidence is automated Pest tests only (no live tenant required). +- Do not expand scope if additional missing resources are discovered. + +## Technical Context + +**Language/Version**: PHP 8.4.x +**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Microsoft Graph integration via `GraphClientInterface` +**Storage**: PostgreSQL (via Laravel Sail) +**Testing**: Pest v4 (Laravel test runner via Sail) +**Target Platform**: Docker (Laravel Sail) for local dev; container-based deploy (Dokploy) +**Project Type**: Web application (Laravel) +**Performance Goals**: N/A (no runtime hot path changes intended) +**Constraints**: +- No new dependencies. +- No new UI/routes. +- Do not require a live tenant for acceptance. +- Keep change bounded to the four resources + five known call sites. +**Scale/Scope**: Small refactor + config change + targeted regression tests. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- Inventory-first: PASS (no inventory/snapshot changes). +- Read/write separation: PASS (no new write workflows). +- Single contract path to Graph: PASS (this feature strengthens the contract registry and prevents ad-hoc endpoints). +- Deterministic capabilities: N/A (no capability derivation changes). +- RBAC-UX: PASS (no authorization model or UI surfaces changed). +- Workspace/tenant isolation: PASS (no new cross-tenant reads/writes; registry changes do not imply access). +- Run observability: PASS (no new long-running operations; tests-only acceptance). +- Data minimization & safe logging: PASS (no new payload logging). +- Badge semantics (BADGE-001): N/A (no badges). +- Filament UI Action Surface Contract: N/A (no Filament resources/pages modified). + +## Project Structure + +### Documentation (this feature) + +```text +specs/095-graph-contracts-registry-completeness/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +└── checklists/ +``` + +### Source Code (repository root) + +```text +app/ +├── Services/ +│ ├── Graph/ +│ └── Intune/ +config/ +├── graph_contracts.php +tests/ +└── Feature/ +``` + +**Structure Decision**: Laravel monolith. Changes are limited to `config/graph_contracts.php`, small helpers under `app/Services/Graph`, a handful of service call sites under `app/Services/Intune`, and a new targeted Pest test. + +## Phase 0 — Outline & Research + +### Unknowns / Items to Validate + +None required to proceed; the spec is bounded and based on known call sites. + +### Research Outputs + +- Create [specs/095-graph-contracts-registry-completeness/research.md](research.md) documenting: + - Contract registry patterns used in this repo. + - Drift-check enumeration behavior (top-level resources). + - Test strategy for preventing endpoint string regressions. + +## Phase 1 — Design & Contracts + +### Data Model + +- No new database entities. +- Create [specs/095-graph-contracts-registry-completeness/data-model.md](data-model.md) documenting “no new entities” explicitly. + +### Contracts + +- Create minimal external API contract documentation under `contracts/` describing the four affected Microsoft Graph endpoints. +- Output: `contracts/graph-deviceManagement-contracts.yaml`. + +### Quickstart + +- Create [specs/095-graph-contracts-registry-completeness/quickstart.md](quickstart.md) showing how to run the focused tests via Sail. + +### Agent Context Update + +- Run `.specify/scripts/bash/update-agent-context.sh copilot`. + +### Constitution Re-check (post design) + +- Expected: still PASS (no UI, no RBAC, no long-running ops). + +## Phase 2 — Implementation Plan (no code yet) + +1. Add/verify contract registry entries for the four resources in `config/graph_contracts.php`. +2. Ensure contract registry supports a subresource template for “Configuration Policy Template → setting templates”. +3. Refactor the five in-scope call sites to resolve Graph paths via the registry (no hardcoded endpoint substrings for these resources). +4. Add regression tests: + - Registry completeness for the four resources + required subresource template. + - String-guard checks for the five in-scope files to prevent reintroducing hardcoded endpoints. +5. Run formatting: `vendor/bin/sail bin pint --dirty`. +6. Run focused tests via Sail (acceptance evidence): `vendor/bin/sail artisan test --compact` with the new/updated test file(s). + +## Complexity Tracking + +No constitution violations expected; no complexity exemptions required. diff --git a/specs/095-graph-contracts-registry-completeness/quickstart.md b/specs/095-graph-contracts-registry-completeness/quickstart.md new file mode 100644 index 0000000..ac488d3 --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/quickstart.md @@ -0,0 +1,23 @@ +# Quickstart: Graph Contracts Registry Completeness + +## Goal + +Validate that the Microsoft Graph contract registry includes the four specified resources and that the in-scope call sites use registry-backed paths. + +## Prerequisites + +- Laravel Sail is available. + +## Run the focused tests (acceptance evidence) + +- Run the new/updated tests: + - `vendor/bin/sail artisan test --compact` + +## Formatting + +- Apply formatting to changed files: + - `vendor/bin/sail bin pint --dirty` + +## Notes + +- A live tenant is not required for acceptance evidence for this spec. diff --git a/specs/095-graph-contracts-registry-completeness/research.md b/specs/095-graph-contracts-registry-completeness/research.md new file mode 100644 index 0000000..e6b296d --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/research.md @@ -0,0 +1,40 @@ +# Phase 0 — Research: Graph Contracts Registry Completeness + +## Decisions + +### Decision: Scope of enforcement +- **Decision**: Enforce “registry-backed paths” only for the four specified Graph resources and the five known call sites. +- **Rationale**: Keeps the change bounded and reviewable while addressing the concrete governance gap. +- **Alternatives considered**: + - Enforce across the entire codebase (rejected: scope explosion and higher regression risk). + +### Decision: Acceptance evidence +- **Decision**: Pest tests passing are sufficient acceptance evidence. +- **Rationale**: Reproducible in CI/local without requiring a live tenant or delegated auth. +- **Alternatives considered**: + - Require drift-check command output (rejected: can require tenant setup and auth). + +### Decision: Handling additional discoveries +- **Decision**: Do not expand scope beyond the four specified resources. +- **Rationale**: Prevents “scope creep by implementation”. Additional gaps can be handled via a follow-up spec. +- **Alternatives considered**: + - Expand to additional resources found during implementation (rejected: unbounded). + +## Best Practices / Patterns (Repo-specific) + +- **Contract registry**: Graph endpoint resources are centrally declared in `config/graph_contracts.php` and should be considered the source of truth. +- **Graph client**: All Graph calls route through `GraphClientInterface`. +- **Governance goal**: Feature code should avoid ad-hoc endpoint strings for governed resources. + +## Testing Strategy + +- **Registry completeness test**: Assert the four resources are registered and the template subresource is representable. +- **Regression guard**: Assert the five in-scope files do not contain hardcoded endpoint substrings for the governed resources. + +## Notes + +- This feature makes no changes to RBAC, UI, database schema, or operations observability. + +## Follow-ups (out of scope) + +- None identified yet. diff --git a/specs/095-graph-contracts-registry-completeness/spec.md b/specs/095-graph-contracts-registry-completeness/spec.md new file mode 100644 index 0000000..9adc76a --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/spec.md @@ -0,0 +1,125 @@ +# Feature Specification: Graph Contracts Registry Completeness + +**Feature Branch**: `095-graph-contracts-registry-completeness` +**Created**: 2026-02-15 +**Status**: Draft +**Input**: Ensure Microsoft Graph resources already used by the product are explicitly registered in the app’s contract registry, and ensure call sites use the registry rather than ad-hoc paths. Add regression tests so these resources can’t silently become “untracked” again. + +## Spec Scope Fields *(mandatory)* + +- **Scope**: workspace +- **Primary Routes**: None (no new user-facing pages or routes) +- **Data Ownership**: No new persistent records; registry metadata only +- **RBAC**: No new permissions; behavior must not expand access beyond existing authorization and tenant isolation + +## Clarifications + +### Session 2026-02-15 + +- Q: Which enforcement scope do you want for “no freeform Graph paths”? → A: Enforce registry-backed paths only for the 4 specified resources (and the call sites that touch them). +- Q: What evidence should be required for acceptance? → A: Pest regression tests passing is sufficient evidence. +- Q: Which code locations should be considered “in scope” for the regression guard? → A: ConfigurationPolicyTemplateResolver, SettingsCatalogDefinitionResolver, SettingsCatalogCategoryResolver, RbacOnboardingService, and RbacHealthService. +- Q: If we discover additional unregistered Graph resources during implementation, what should we do? → A: Do not expand scope; register only the 4 specified resources (extra findings become a follow-up spec). + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Drift coverage is complete (Priority: P1) + +As a maintainer, I need the contract drift check to cover the Graph resources we already depend on, so that operational “drift” detection is trustworthy and doesn’t miss critical resources. + +**Why this priority**: Drift-check coverage gaps undermine safety and auditability across multiple other workflows. + +**Independent Test**: A test can assert that the registry explicitly includes the required resources (the same registry enumeration the drift check relies on). + +**Acceptance Scenarios**: + +1. **Given** the contract registry, **When** it is evaluated for registered resources, **Then** it includes: + - Configuration Policy Templates + - Configuration Settings (settings catalog definitions) + - Configuration Categories + - RBAC Role Assignments +2. **Given** the registry entry for configuration policy templates, **When** nested template setting templates are needed, **Then** the registry can represent that subresource using an approved template (including an item identifier placeholder). + +--- + +### User Story 2 - Graph calls are registry-backed (Priority: P2) + +As a maintainer, I need Graph calls for these resources to use the contract registry rather than freeform strings, so that endpoints are governed consistently (naming, review, and drift-check alignment). + +**Why this priority**: Prevents silent expansion of Graph surface area and makes review/audit easier. + +**Independent Test**: A test can detect reintroduction of hardcoded endpoint substrings in the relevant call sites. + +**Acceptance Scenarios**: + +1. **Given** code paths that fetch settings catalog definitions or categories, **When** they call Graph, **Then** they source the resource path from the contract registry rather than embedding hardcoded endpoint substrings. +2. **Given** code paths that create/update role assignments, **When** they call Graph, **Then** they source the role-assignments resource path from the contract registry rather than embedding a hardcoded endpoint. + +--- + +### User Story 3 - Regressions are prevented (Priority: P3) + +As a maintainer, I need regression tests that fail if these resources become unregistered or if call sites revert to ad-hoc endpoints, so that future refactors cannot silently break governance. + +**Why this priority**: The risk is gradual “drift” over time; a regression guard is the lowest-cost long-term control. + +**Independent Test**: A test suite can fail on (a) missing registry entries, and (b) hardcoded endpoint usage in targeted files. + +**Acceptance Scenarios**: + +1. **Given** a future change accidentally removes a required resource from the registry, **When** tests run, **Then** they fail with a clear message identifying which resource is missing. +2. **Given** a future change introduces a hardcoded endpoint string for one of the covered resources, **When** tests run, **Then** they fail and indicate which endpoint substring was reintroduced. + +### Edge Cases + +- Graph permissions or tenant isolation limitations cause the resource to be inaccessible for a given tenant; contract registration must not imply access. +- The Graph API returns paginated results for list endpoints; registry coverage must not degrade pagination handling. +- The Graph API evolves (resource moved/renamed/deprecated); contract drift check and tests should reveal mismatch quickly. +- A subresource path exists for a registered resource but is not represented in the registry; this must be treated as a governance gap. + +## Requirements *(mandatory)* + +**Constitution alignment (required):** This feature updates the contract registry for Microsoft Graph resources and adds regression tests to ensure completeness and to prevent ad-hoc expansions of Graph calls. + +### Functional Requirements + +- **FR-001**: The system MUST explicitly register the following Graph resources in the contract registry: + - Configuration Policy Templates + - Configuration Settings (settings catalog definitions) + - Configuration Categories + - RBAC Role Assignments +- **FR-002**: The system MUST represent the “setting templates” subresource under Configuration Policy Templates as part of the registered contract. +- **FR-003**: The system MUST ensure that Graph calls for the above resources are sourced from the contract registry (not embedded as freeform endpoint strings in call sites). +- **FR-003a**: The “no freeform path” enforcement scope MUST be limited to the four specified resources and the known call sites that access them. +- **FR-004**: The system MUST include regression tests that fail when any of the required resources are not registered. +- **FR-005**: The system MUST include regression tests that detect reintroduction of ad-hoc endpoint strings in the targeted call sites for these resources. +- **FR-005a**: The regression guard “targeted call sites” MUST include: ConfigurationPolicyTemplateResolver, SettingsCatalogDefinitionResolver, SettingsCatalogCategoryResolver, RbacOnboardingService, and RbacHealthService. +- **FR-006**: The implementation scope MUST remain limited to the four specified resources; any additional missing resources discovered during implementation MUST be handled via a follow-up spec. + +### Registry Identifiers (internal) + +To make the requirements testable and to avoid ambiguity, the contract registry MUST contain stable internal identifiers for these four resources (under the registry’s “types” section): + +| Resource | Registry identifier | +|----------|---------------------| +| Configuration Policy Templates | `configurationPolicyTemplate` | +| Configuration Settings (settings catalog definitions) | `settingsCatalogDefinition` | +| Configuration Categories | `settingsCatalogCategory` | +| RBAC Role Assignments | `rbacRoleAssignment` | + +Additionally, the Configuration Policy Template contract MUST include a named subresource for “setting templates” that is templated by the parent template identifier (for example: subresource key `settingTemplates`). + +### Assumptions + +- Existing authorization and tenant isolation rules already constrain whether any given tenant can access these resources; contract registration does not add or expand permissions. +- The contract drift check enumerates registered top-level resources; nested subresources are still registered for governance consistency and reuse. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of the specified Graph resources are discoverable from the contract registry. +- **SC-002**: Regression tests fail within 1 test run if any specified resource becomes unregistered. +- **SC-003**: Regression tests fail within 1 test run if targeted call sites reintroduce hardcoded endpoint substrings for the specified resources. +- **SC-004**: Maintainers can demonstrate drift-check coverage for these resources without manual inspection (e.g., via automated verification output or test evidence). +- **SC-004a**: Acceptance evidence MUST be satisfied by automated tests (Pest) without requiring a live tenant or manual drift-check output. diff --git a/specs/095-graph-contracts-registry-completeness/tasks.md b/specs/095-graph-contracts-registry-completeness/tasks.md new file mode 100644 index 0000000..ceb8a5c --- /dev/null +++ b/specs/095-graph-contracts-registry-completeness/tasks.md @@ -0,0 +1,134 @@ +# Tasks: Graph Contracts Registry Completeness + +**Input**: Design documents from `/specs/095-graph-contracts-registry-completeness/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ + +**Tests**: Required (Pest). Acceptance evidence is automated tests only. + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Ensure the feature workspace and reference docs are in place. + +- [X] T001 Confirm spec artifacts are present and up to date in specs/095-graph-contracts-registry-completeness/spec.md and specs/095-graph-contracts-registry-completeness/plan.md +- [X] T002 [P] Review current contract registry structure in config/graph_contracts.php +- [X] T003 [P] Review current contract helper patterns in app/Services/Graph/GraphContractRegistry.php +- [X] T021 If any additional unregistered Graph resources are discovered during review/implementation, record them in specs/095-graph-contracts-registry-completeness/research.md under a “Follow-ups (out of scope)” section and do not expand this spec’s implementation scope + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Establish baseline patterns used by subsequent story work. + +- [X] T004 Review existing contract coverage test patterns in tests/Feature/Graph/GraphContractRegistryCoverageSpec081Test.php + +**Checkpoint**: Foundation ready — user story work can begin. + +--- + +## Phase 3: User Story 1 — Drift coverage is complete (Priority: P1) 🎯 MVP + +**Goal**: The contract registry explicitly models the four required resources and the template subresource. + +**Independent Test**: A single Pest test file can assert the registry contains the required resources and subresource template. + +- [X] T005 [P] [US1] Create a new coverage test file in tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php +- [X] T006 [US1] Add contract type entries for the four required resources in config/graph_contracts.php +- [X] T007 [US1] Add a subresource template for “Configuration Policy Template → setting templates” in config/graph_contracts.php +- [X] T008 [US1] Implement registry assertions for the four resources + subresource template in tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php +- [X] T009 [US1] Verify the MVP by running the focused test command documented in specs/095-graph-contracts-registry-completeness/quickstart.md + +**Checkpoint**: US1 complete when the new coverage test passes and the registry contains the four resources + template subresource. + +--- + +## Phase 4: User Story 2 — Graph calls are registry-backed (Priority: P2) + +**Goal**: The five in-scope call sites resolve paths via the contract registry (no hardcoded endpoint substrings for these resources). + +**Independent Test**: A regression guard can fail if hardcoded endpoint substrings are reintroduced in the five in-scope files. + +- [X] T010 [US2] Add/adjust registry path helpers needed by call sites in app/Services/Graph/GraphContractRegistry.php +- [X] T011 [P] [US2] Refactor Graph template calls to use registry-backed paths in app/Services/Intune/ConfigurationPolicyTemplateResolver.php +- [X] T012 [P] [US2] Refactor settings catalog definition calls to use registry-backed paths in app/Services/Intune/SettingsCatalogDefinitionResolver.php +- [X] T013 [P] [US2] Refactor settings catalog category calls to use registry-backed paths in app/Services/Intune/SettingsCatalogCategoryResolver.php +- [X] T014 [P] [US2] Refactor role assignment create/update/list calls to use registry-backed paths in app/Services/Intune/RbacOnboardingService.php +- [X] T015 [P] [US2] Refactor role assignment health/read calls to use registry-backed paths in app/Services/Intune/RbacHealthService.php + +**Checkpoint**: US2 complete when all five call sites use registry-backed paths for the four governed resources. + +--- + +## Phase 5: User Story 3 — Regressions are prevented (Priority: P3) + +**Goal**: Automated tests fail if registry entries are removed or if call sites revert to ad-hoc endpoint strings. + +**Independent Test**: Pest tests fail within a single run for either (a) missing registry entries or (b) hardcoded endpoints in in-scope files. + +- [X] T016 [P] [US3] Add string-regression guard assertions for the five in-scope files in tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php +- [X] T017 [US3] Ensure regression guard failure messages identify the missing resource or offending file in tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php +- [X] T018 [US3] Run the acceptance tests (focused) documented in specs/095-graph-contracts-registry-completeness/quickstart.md + +**Checkpoint**: US3 complete when tests fail on simulated regressions and pass on the final implementation. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +- [X] T019 [P] Apply formatting to changed files using `vendor/bin/sail bin pint --dirty` (config/graph_contracts.php and app/Services/Graph/GraphContractRegistry.php) +- [X] T020 Run a targeted regression suite that includes the new coverage test and any directly impacted existing tests (tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php and tests/Feature/SettingsCatalogDefinitionResolverTest.php) + +--- + +## Dependencies & Execution Order + +### Dependency Graph (User Story Order) + +```mermaid +graph LR + Setup[Phase 1: Setup] --> Foundational[Phase 2: Foundational] + Foundational --> US1[US1: Registry coverage] + US1 --> US2[US2: Call sites registry-backed] + US2 --> US3[US3: Regression guards] + US3 --> Polish[Phase 6: Polish] +``` + +### User Story Dependencies + +- US1 has no dependencies beyond Foundational. +- US2 depends on US1 (registry entries must exist before call sites can reference them). +- US3 depends on US1 and US2 (guards validate the final state). + +--- + +## Parallel Execution Examples + +### User Story 1 + +- Can be split as: + - T005 (create test skeleton) can proceed in parallel with T006/T007 planning, but assertions should be written before implementation. + +### User Story 2 + +- After T010 is complete, these can run in parallel (different files): + - T011, T012, T013, T014, T015 + +### User Story 3 + +- After US2 is complete, T016 can be implemented independently and validated via T018. + +--- + +## Implementation Strategy + +### MVP First (US1 only) + +1. Complete Phase 1–2 +2. Complete US1 (T005–T009) +3. Stop and validate via the focused test + +### Incremental Delivery + +- US1 → US2 → US3, running acceptance tests after each checkpoint. diff --git a/tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php b/tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php new file mode 100644 index 0000000..bdb520e --- /dev/null +++ b/tests/Feature/Graph/GraphContractRegistryCoverageSpec095Test.php @@ -0,0 +1,74 @@ + 'deviceManagement/configurationPolicyTemplates', + 'settingsCatalogDefinition' => 'deviceManagement/configurationSettings', + 'settingsCatalogCategory' => 'deviceManagement/configurationCategories', + 'rbacRoleAssignment' => 'deviceManagement/roleAssignments', + ]; + + foreach ($requiredResources as $contractType => $resourcePath) { + $resource = config("graph_contracts.types.{$contractType}.resource"); + + expect($resource) + ->toBeString("Spec095 missing graph contract resource for {$contractType}") + ->toBe($resourcePath, "Spec095 graph contract resource mismatch for {$contractType}"); + } + + $settingTemplatesPath = config('graph_contracts.types.configurationPolicyTemplate.subresources.settingTemplates.path'); + + expect($settingTemplatesPath) + ->toBeString('Spec095 missing settingTemplates subresource for configurationPolicyTemplate'); + + expect(str_contains((string) $settingTemplatesPath, '{id}')) + ->toBeTrue('Spec095 settingTemplates subresource path must include the parent {id} placeholder'); +}); + +it('Spec095 keeps registry path helpers for governed resources', function (): void { + $contracts = app(GraphContractRegistry::class); + + expect($contracts->configurationPolicyTemplateListPath()) + ->toBe('/deviceManagement/configurationPolicyTemplates'); + + expect($contracts->configurationPolicyTemplateItemPath('template/abc')) + ->toBe('/deviceManagement/configurationPolicyTemplates/template%2Fabc'); + + expect($contracts->configurationPolicyTemplateSettingTemplatesPath('template/abc')) + ->toBe('/deviceManagement/configurationPolicyTemplates/template%2Fabc/settingTemplates'); + + expect($contracts->settingsCatalogDefinitionItemPath('definition/abc')) + ->toBe('/deviceManagement/configurationSettings/definition%2Fabc'); + + expect($contracts->settingsCatalogCategoryItemPath('category/abc')) + ->toBe('/deviceManagement/configurationCategories/category%2Fabc'); + + expect($contracts->rbacRoleAssignmentItemPath('assignment/abc')) + ->toBe('/deviceManagement/roleAssignments/assignment%2Fabc'); +}); + +it('Spec095 guards against hardcoded governed endpoint strings in scoped call sites', function (): void { + $scopedGuards = [ + 'app/Services/Intune/ConfigurationPolicyTemplateResolver.php' => ['deviceManagement/configurationPolicyTemplates'], + 'app/Services/Intune/SettingsCatalogDefinitionResolver.php' => ['deviceManagement/configurationSettings'], + 'app/Services/Intune/SettingsCatalogCategoryResolver.php' => ['deviceManagement/configurationCategories'], + 'app/Services/Intune/RbacOnboardingService.php' => ['deviceManagement/roleAssignments'], + 'app/Services/Intune/RbacHealthService.php' => ['deviceManagement/roleAssignments'], + ]; + + foreach ($scopedGuards as $file => $forbiddenSubstrings) { + $contents = file_get_contents(base_path($file)); + + expect($contents) + ->not->toBeFalse("Spec095 regression guard could not read {$file}"); + + foreach ($forbiddenSubstrings as $forbiddenSubstring) { + expect(str_contains((string) $contents, $forbiddenSubstring)) + ->toBeFalse("Spec095 regression guard found hardcoded '{$forbiddenSubstring}' in {$file}; use GraphContractRegistry helper paths instead."); + } + } +}); diff --git a/tests/Feature/SettingsCatalogDefinitionResolverTest.php b/tests/Feature/SettingsCatalogDefinitionResolverTest.php index 9d7d0a5..f30844f 100644 --- a/tests/Feature/SettingsCatalogDefinitionResolverTest.php +++ b/tests/Feature/SettingsCatalogDefinitionResolverTest.php @@ -2,6 +2,7 @@ use App\Models\SettingsCatalogDefinition; use App\Services\Graph\GraphClientInterface; +use App\Services\Graph\GraphContractRegistry; use App\Services\Graph\GraphResponse; use App\Services\Intune\SettingsCatalogDefinitionResolver; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -31,7 +32,7 @@ // Should NOT call Graph API $mockClient->shouldNotReceive('request'); - $resolver = new SettingsCatalogDefinitionResolver($mockClient); + $resolver = new SettingsCatalogDefinitionResolver($mockClient, new GraphContractRegistry); // Act $result = $resolver->resolve([$definitionId]); @@ -56,7 +57,7 @@ ->once() ->andReturn($mockResponse); - $resolver = new SettingsCatalogDefinitionResolver($mockClient); + $resolver = new SettingsCatalogDefinitionResolver($mockClient, new GraphContractRegistry); // Act $result = $resolver->resolve([$definitionId]); @@ -84,7 +85,7 @@ $mockClient = Mockery::mock(GraphClientInterface::class); $mockClient->shouldNotReceive('request'); - $resolver = new SettingsCatalogDefinitionResolver($mockClient); + $resolver = new SettingsCatalogDefinitionResolver($mockClient, new GraphContractRegistry); // Act $result = $resolver->resolveOne($definitionId); @@ -118,7 +119,7 @@ ->with('GET', "/deviceManagement/configurationSettings/{$uncachedId}") ->andReturn($mockResponse); - $resolver = new SettingsCatalogDefinitionResolver($mockClient); + $resolver = new SettingsCatalogDefinitionResolver($mockClient, new GraphContractRegistry); // Act $result = $resolver->resolve([$cachedId, $uncachedId]); @@ -144,7 +145,7 @@ $mockClient = Mockery::mock(GraphClientInterface::class); $mockClient->shouldNotReceive('request'); - $resolver = new SettingsCatalogDefinitionResolver($mockClient); + $resolver = new SettingsCatalogDefinitionResolver($mockClient, new GraphContractRegistry); // Act & Assert (should not throw) expect(fn () => $resolver->warmCache([$definitionId]))->not->toThrow(Exception::class); @@ -164,7 +165,7 @@ ->once() ->andThrow(new Exception('Graph API error')); - $resolver = new SettingsCatalogDefinitionResolver($mockClient); + $resolver = new SettingsCatalogDefinitionResolver($mockClient, new GraphContractRegistry); // Act & Assert (should not throw) expect(fn () => $resolver->warmCache($definitionIds))->not->toThrow(Exception::class);