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 <ahmed.darrazi@live.de> Reviewed-on: #114
This commit is contained in:
parent
bda1d90fc4
commit
eec93b510a
3
.github/agents/copilot-instructions.md
vendored
3
.github/agents/copilot-instructions.md
vendored
@ -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
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
|
||||
@ -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<string, mixed>
|
||||
*/
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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.
|
||||
125
specs/095-graph-contracts-registry-completeness/plan.md
Normal file
125
specs/095-graph-contracts-registry-completeness/plan.md
Normal file
@ -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.
|
||||
@ -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.
|
||||
40
specs/095-graph-contracts-registry-completeness/research.md
Normal file
40
specs/095-graph-contracts-registry-completeness/research.md
Normal file
@ -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.
|
||||
125
specs/095-graph-contracts-registry-completeness/spec.md
Normal file
125
specs/095-graph-contracts-registry-completeness/spec.md
Normal file
@ -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.
|
||||
134
specs/095-graph-contracts-registry-completeness/tasks.md
Normal file
134
specs/095-graph-contracts-registry-completeness/tasks.md
Normal file
@ -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.
|
||||
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Services\Graph\GraphContractRegistry;
|
||||
|
||||
it('Spec095 keeps graph contract entries for governed deviceManagement resources', function (): void {
|
||||
$requiredResources = [
|
||||
'configurationPolicyTemplate' => '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.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user