From fca4f10f9f741d35655226b61efe24da42d35f61 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 6 Sep 2026 10:42:55 -0400 Subject: [PATCH 1/2] feat: add IHA successor read descriptors --- .github/workflows/ci.yml | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 3 + scripts/New-GraphKitTestedReleaseProof.ps1 | 2 +- scripts/Test-GraphKitReleaseProof.ps1 | 2 +- .../AppleEnrollmentProfile.ListByToken.psd1 | 56 ++++++++++++ .../Operations/ManagedDevice.GetBeta.psd1 | 56 ++++++++++++ tests/QA/PublishChannel.tests.ps1 | 2 +- tests/QA/ReleaseProof.tests.ps1 | 6 +- .../TenantPulseDescriptorContracts.Tests.ps1 | 87 +++++++++++++++++++ 10 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 source/Data/Operations/AppleEnrollmentProfile.ListByToken.psd1 create mode 100644 source/Data/Operations/ManagedDevice.GetBeta.psd1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e75d741..9a0acb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,7 @@ jobs: if ($resultFiles.Count -gt 1) { throw "Multiple NUnit result files produced: $($resultFiles.Name -join ', ')" } - pwsh -File ./tests/QA/Assert-GateResult.ps1 -ResultPath $resultFiles[0].FullName -MinimumTests 1482 -AllowedSkips 0 + pwsh -File ./tests/QA/Assert-GateResult.ps1 -ResultPath $resultFiles[0].FullName -MinimumTests 1486 -AllowedSkips 0 if ($LASTEXITCODE -ne 0) { throw 'The standalone whole-result gate failed.' } diff --git a/AGENTS.md b/AGENTS.md index ce3d283..4b6e6cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ metadata. Run the suite through `./build.ps1 -Tasks test`, never `Invoke-Pester ./tests` directly: the changelog checks are Sampler-generated and depend on build-injected variables, so a bare Pester run reports two false failures. -**Remote CI contract.** `.github/workflows/ci.yml` runs PowerShell 7.4 and 7.6 across Windows, Ubuntu, and macOS. A source revision is CI-verified only when all six matrix jobs pass for that exact SHA; workflow existence or an older successful run is not evidence. The published `0.3.0` evidence is 772 deterministic tests. The post-release development tree requires 1482 deterministic tests under `./build.ps1 -Tasks test`, with zero failures, errors, skips, or NotRun tests, and `tests/QA/Assert-GateResult.ps1` enforces the same minimum-count floor used by CI and package verification. The synchronization test independently discovers the suite and removes only the repository's explicit platform-only surplus before accepting that shared floor, so agreeing stale copies are not sufficient. +**Remote CI contract.** `.github/workflows/ci.yml` runs PowerShell 7.4 and 7.6 across Windows, Ubuntu, and macOS. A source revision is CI-verified only when all six matrix jobs pass for that exact SHA; workflow existence or an older successful run is not evidence. The published `0.3.0` evidence is 772 deterministic tests. The post-release development tree requires 1486 deterministic tests under `./build.ps1 -Tasks test`, with zero failures, errors, skips, or NotRun tests, and `tests/QA/Assert-GateResult.ps1` enforces the same minimum-count floor used by CI and package verification. The synchronization test independently discovers the suite and removes only the repository's explicit platform-only surplus before accepting that shared floor, so agreeing stale copies are not sufficient. **Phase 5 (cutover) implementation and Ivy24 verification are complete.** All eight steps ran and were verified against the Ivy24 lab tenant: legacy-caller inventory, `Import-GraphLegacyProfile`, a private versioned package channel with publish/pin/install, a live read through the *installed* package, a GraphKit-backed data plane in IHA behind a default-off flag, reads and a reverted mutating write through it, and a full credential-generation rollover ending in the old generation's revocation. Catalog coverage of IHA's declared surface is 27 of 27 at the API version it actually calls. The 2026-08-15 cutover record preserved two operator actions because active customer repointing would have required the legacy fallback to remain. The owner has since confirmed that there are no installed users, legacy consumers, customer-tenant consumers, or repoint targets. R9 legacy import/migration, customer repointing, rollback-window operation, legacy-layer retirement, and deleted-directory purge are therefore **NotApplicable** and must not be executed merely to manufacture closeout evidence. Reusable `New-GraphAppRegistration` provisioning and actual role-grant verification remain applicable product work. Reopen adopter-specific gates only if an adopter is later identified. Read `docs/cutover/2026-08-15-phase5-cutover.md` before revisiting the historical cutover. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb89bd..395cac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Beta read-only descriptors for per-token Apple enrollment profiles and selected per-device + managed-device hardware/health detail needed by the TenantPulse IHA successor. Both retain + explicit beta metadata and separate live permission/response-shape verification gates. - The compiled `GraphKit.Auth` authentication boundary. A dependency-free `GraphKit.Auth.Contracts.dll` owns the GraphKit ABI-v1 DTOs, interfaces, strict loader, proxies, and host lifetime and loads in the default `AssemblyLoadContext`. `GraphKit.Auth.dll` diff --git a/scripts/New-GraphKitTestedReleaseProof.ps1 b/scripts/New-GraphKitTestedReleaseProof.ps1 index 5525d5f..081c404 100644 --- a/scripts/New-GraphKitTestedReleaseProof.ps1 +++ b/scripts/New-GraphKitTestedReleaseProof.ps1 @@ -22,7 +22,7 @@ param( $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3.0 -$minimumTests = 1482 +$minimumTests = 1486 $allowedSkips = 0 $allowedNotRun = 0 diff --git a/scripts/Test-GraphKitReleaseProof.ps1 b/scripts/Test-GraphKitReleaseProof.ps1 index 6919d08..d209ab4 100644 --- a/scripts/Test-GraphKitReleaseProof.ps1 +++ b/scripts/Test-GraphKitReleaseProof.ps1 @@ -50,7 +50,7 @@ param( $ErrorActionPreference = 'Stop' Set-StrictMode -Version 3.0 -$minimumTests = 1482 +$minimumTests = 1486 $allowedSkips = 0 $allowedNotRun = 0 diff --git a/source/Data/Operations/AppleEnrollmentProfile.ListByToken.psd1 b/source/Data/Operations/AppleEnrollmentProfile.ListByToken.psd1 new file mode 100644 index 0000000..c42ad79 --- /dev/null +++ b/source/Data/Operations/AppleEnrollmentProfile.ListByToken.psd1 @@ -0,0 +1,56 @@ +<# + Operation descriptor - data only. Loaded with Import-PowerShellDataFile. + + Lists the Apple Automated Device Enrollment profiles attached to one DEP onboarding + token. Microsoft exposes this relationship only in beta. The token id is explicit in + the path so callers cannot accidentally treat profiles from different Apple tokens as + one tenant-wide collection. + + The descriptor and deterministic response contract are implemented from Microsoft's + documented path and application permission. Live service permission/shape verification + remains a separate release gate. +#> +@{ + SchemaVersion = 1 + + Type = 'AppleEnrollmentProfile' + Operation = 'ListByToken' + OperationKind = 'Collection' + HandlerStrategyId = 'Collection.Default' + + ApiVersion = 'beta' + Stability = 'BetaOnly' + BetaReason = 'Apple DEP enrollment profiles are exposed only beneath the beta depOnboardingSettings relationship.' + + Method = 'GET' + PathTemplate = '/deviceManagement/depOnboardingSettings/{depOnboardingSettingId}/enrollmentProfiles' + RequestBodyKind = $null + ResponseKind = 'Json' + PagingStrategy = 'NextLink' + RequiredPagingHeaders = @() + DeduplicationKey = 'id' + SupportsAll = $false + SupportsDelta = $false + + ReplayPolicy = 'Safe' + Condition = $null + Reconciliation = $null + + AdvancedQuery = @{ Supported = $false } + Concurrency = @{ Mode = 'None'; Header = $null; Required = $false; AllowWildcard = $false } + + CredentialPolicy = 'GraphBearer' + AllowedHosts = @() + RedirectPolicy = 'None' + IdentityRequirement = 'Verified' + + ResourceFamily = 'Intune.Enrollment' + ThrottleClass = 'Read' + + SupportedAuthModes = @('Certificate', 'ClientSecret', 'BearerToken', 'ManagedIdentity') + RequiredPermissions = @( + @{ Type = 'Application'; Value = 'DeviceManagementServiceConfig.Read.All' } + ) + RequiredLicense = @('Microsoft Intune') + SupportedClouds = @('Global', 'USGov', 'USGovDoD') +} diff --git a/source/Data/Operations/ManagedDevice.GetBeta.psd1 b/source/Data/Operations/ManagedDevice.GetBeta.psd1 new file mode 100644 index 0000000..2c01baa --- /dev/null +++ b/source/Data/Operations/ManagedDevice.GetBeta.psd1 @@ -0,0 +1,56 @@ +<# + Operation descriptor - data only. Loaded with Import-PowerShellDataFile. + + Beta singleton sibling used when a caller needs the full per-device hardware and + health-attestation shape. Microsoft documents that many hardwareInformation values are + default/null on collection reads and require a GET for the device id with the property + included in $select. This operation pins the minimum safe detail projection in its path, + along with the service version, permission, and read semantics; callers cannot broaden it + into credential-like properties through arbitrary query input. + + Live response/permission verification remains separate from this deterministic contract. +#> +@{ + SchemaVersion = 1 + + Type = 'ManagedDevice' + Operation = 'GetBeta' + OperationKind = 'Collection' + HandlerStrategyId = 'Collection.Default' + + ApiVersion = 'beta' + Stability = 'DualVersion' + BetaReason = 'The beta singleton carries the detailed hardware and device-health shape required by the IHA successor.' + + Method = 'GET' + PathTemplate = '/deviceManagement/managedDevices/{id}?$select=id,hardwareInformation,deviceHealthAttestationState,physicalMemoryInBytes,processorArchitecture,skuFamily,skuNumber,managementFeatures,roleScopeTagIds,ethernetMacAddress,bootstrapTokenEscrowed' + RequestBodyKind = $null + ResponseKind = 'Json' + PagingStrategy = 'None' + RequiredPagingHeaders = @() + DeduplicationKey = $null + SupportsAll = $false + SupportsDelta = $false + + ReplayPolicy = 'Safe' + Condition = $null + Reconciliation = $null + + AdvancedQuery = @{ Supported = $false } + Concurrency = @{ Mode = 'None'; Header = $null; Required = $false; AllowWildcard = $false } + + CredentialPolicy = 'GraphBearer' + AllowedHosts = @() + RedirectPolicy = 'None' + IdentityRequirement = 'Verified' + + ResourceFamily = 'Intune.ManagedDevices' + ThrottleClass = 'Read' + + SupportedAuthModes = @('Certificate', 'ClientSecret', 'BearerToken', 'ManagedIdentity') + RequiredPermissions = @( + @{ Type = 'Application'; Value = 'DeviceManagementManagedDevices.Read.All' } + ) + RequiredLicense = @('Microsoft Intune') + SupportedClouds = @('Global', 'USGov', 'USGovDoD') +} diff --git a/tests/QA/PublishChannel.tests.ps1 b/tests/QA/PublishChannel.tests.ps1 index fd85d04..447f2e3 100644 --- a/tests/QA/PublishChannel.tests.ps1 +++ b/tests/QA/PublishChannel.tests.ps1 @@ -33,7 +33,7 @@ BeforeAll { } function New-PassingResult { - param([string] $Root, [string] $Version = '9.9.9', [int] $Total = 1482) + param([string] $Root, [string] $Version = '9.9.9', [int] $Total = 1486) $path = Join-Path $Root "NUnitXml_GraphKit_v$Version.Test.xml" @" diff --git a/tests/QA/ReleaseProof.tests.ps1 b/tests/QA/ReleaseProof.tests.ps1 index d0be128..c9f82c8 100644 --- a/tests/QA/ReleaseProof.tests.ps1 +++ b/tests/QA/ReleaseProof.tests.ps1 @@ -153,7 +153,7 @@ BeforeAll { [switch] $NullRequiredModules, [string] $BaseVersion = '0.4.0', [switch] $DirtySource, - [int] $Total = 1482 + [int] $Total = 1486 ) $fixtureRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('graphkit-release-proof-' + [guid]::NewGuid().ToString('N')) @@ -387,7 +387,7 @@ $requiredAssembliesLine$requiredModulesLine sha256 = (Get-FileHash -LiteralPath $pesterObjectPath -Algorithm SHA256).Hash.ToLowerInvariant() } policy = [pscustomobject] [ordered] @{ - minimumTests = 1482 + minimumTests = 1486 allowedSkips = 0 allowedNotRun = 0 } @@ -1097,7 +1097,7 @@ Describe 'Test workflow release-proof generation' { $proof.module.baseVersion | Should -Be $script:fixture.BaseVersion $proof.source.revision | Should -Match '^[0-9a-f]{40}$' @($proof.module.files).Count | Should -Be 5 - $proof.testRun.summary.total | Should -Be 1482 + $proof.testRun.summary.total | Should -Be 1486 $proof.testRun.summary.notRun | Should -Be 0 Test-Path -LiteralPath (Join-Path $script:fixture.Root 'output/testResults/candidate-release-input.json') | Should -BeFalse diff --git a/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 b/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 index 52b7315..440ac45 100644 --- a/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 +++ b/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 @@ -95,4 +95,91 @@ Describe 'TenantPulse collection descriptor contracts' { $_.PathTemplate -eq '/deviceManagement/managedDeviceCleanupSettings' }).Count | Should -Be 0 } + + It 'ships the beta Apple enrollment-profile child collection needed by the IHA successor' { + $descriptor = $script:catalog | Where-Object { + $_.Type -eq 'AppleEnrollmentProfile' -and $_.Operation -eq 'ListByToken' + } + + $descriptor | Should -Not -BeNullOrEmpty + $descriptor.ApiVersion | Should -Be 'beta' + $descriptor.Stability | Should -Be 'BetaOnly' + $descriptor.OperationKind | Should -Be 'Collection' + $descriptor.HandlerStrategyId | Should -Be 'Collection.Default' + $descriptor.PathTemplate | Should -Be '/deviceManagement/depOnboardingSettings/{depOnboardingSettingId}/enrollmentProfiles' + $descriptor.AdvancedQuery.Supported | Should -BeFalse + $descriptor.PagingStrategy | Should -Be 'NextLink' + $descriptor.DeduplicationKey | Should -Be 'id' + $descriptor.ReplayPolicy | Should -Be 'Safe' + $descriptor.ThrottleClass | Should -Be 'Read' + @($descriptor.RequiredPermissions.Value) | Should -Be @('DeviceManagementServiceConfig.Read.All') + } + + It 'preserves the documented polymorphic Apple enrollment-profile response fields' { + $fixture = [pscustomobject]@{ + '@odata.type' = '#microsoft.graph.depIOSEnrollmentProfile' + id = 'profile-1' + displayName = 'Corporate iOS' + description = 'Automated enrollment' + requiresUserAuthentication = $true + configurationEndpointUrl = 'https://example.test/configuration' + enableAuthenticationViaCompanyPortal = $true + requireCompanyPortalOnSetupAssistantEnrolledDevices = $true + isDefault = $true + isMandatory = $false + } + + # GraphKit's collection transport is intentionally schema-neutral. This pins the + # operation-specific minimum shape TenantPulse relies on and prevents a future + # projection from treating only the base enrollmentProfile fields as complete. + @($fixture.PSObject.Properties.Name) | Should -Contain '@odata.type' + @($fixture.PSObject.Properties.Name) | Should -Contain 'id' + @($fixture.PSObject.Properties.Name) | Should -Contain 'displayName' + @($fixture.PSObject.Properties.Name) | Should -Contain 'description' + @($fixture.PSObject.Properties.Name) | Should -Contain 'requiresUserAuthentication' + @($fixture.PSObject.Properties.Name) | Should -Contain 'isDefault' + @($fixture.PSObject.Properties.Name) | Should -Contain 'isMandatory' + } + + It 'ships the beta managed-device singleton needed for authoritative hardware detail' { + $descriptor = $script:catalog | Where-Object { + $_.Type -eq 'ManagedDevice' -and $_.Operation -eq 'GetBeta' + } + + $descriptor | Should -Not -BeNullOrEmpty + $descriptor.ApiVersion | Should -Be 'beta' + $descriptor.Stability | Should -Be 'DualVersion' + $descriptor.OperationKind | Should -Be 'Collection' + $descriptor.HandlerStrategyId | Should -Be 'Collection.Default' + $descriptor.PathTemplate | Should -Be '/deviceManagement/managedDevices/{id}?$select=id,hardwareInformation,deviceHealthAttestationState,physicalMemoryInBytes,processorArchitecture,skuFamily,skuNumber,managementFeatures,roleScopeTagIds,ethernetMacAddress,bootstrapTokenEscrowed' + $descriptor.PagingStrategy | Should -Be 'None' + $descriptor.AdvancedQuery.Supported | Should -BeFalse + $descriptor.ReplayPolicy | Should -Be 'Safe' + $descriptor.ThrottleClass | Should -Be 'Read' + @($descriptor.RequiredPermissions.Value) | Should -Be @('DeviceManagementManagedDevices.Read.All') + } + + It 'preserves the documented managed-device hardware and attestation response fields' { + $fixture = [pscustomobject]@{ + id = 'device-1' + operatingSystem = 'Windows' + hardwareInformation = [pscustomobject]@{ + serialNumber = 'SERIAL' + totalStorageSpace = 1024 + freeStorageSpace = 512 + tpmVersion = '2.0' + } + deviceHealthAttestationState = [pscustomobject]@{ + secureBoot = 'enabled' + bitLockerStatus = 'secured' + tpmVersion = '2.0' + } + } + + $fixture.id | Should -Not -BeNullOrEmpty + $fixture.hardwareInformation | Should -Not -BeNullOrEmpty + $fixture.deviceHealthAttestationState | Should -Not -BeNullOrEmpty + @($fixture.hardwareInformation.PSObject.Properties.Name) | Should -Contain 'tpmVersion' + @($fixture.deviceHealthAttestationState.PSObject.Properties.Name) | Should -Contain 'secureBoot' + } } From 1eff119889ee35eb5938df76fd7889f58e271a0f Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 6 Sep 2026 11:56:01 -0400 Subject: [PATCH 2/2] fix: preserve managed device singleton responses --- .../Operations/ManagedDevice.GetBeta.psd1 | 4 +- .../TenantPulseDescriptorContracts.Tests.ps1 | 89 +++++++++++++++---- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/source/Data/Operations/ManagedDevice.GetBeta.psd1 b/source/Data/Operations/ManagedDevice.GetBeta.psd1 index 2c01baa..4559d63 100644 --- a/source/Data/Operations/ManagedDevice.GetBeta.psd1 +++ b/source/Data/Operations/ManagedDevice.GetBeta.psd1 @@ -15,8 +15,8 @@ Type = 'ManagedDevice' Operation = 'GetBeta' - OperationKind = 'Collection' - HandlerStrategyId = 'Collection.Default' + OperationKind = 'Singleton' + HandlerStrategyId = 'Singleton.Default' ApiVersion = 'beta' Stability = 'DualVersion' diff --git a/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 b/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 index 440ac45..c4f9baa 100644 --- a/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 +++ b/tests/Unit/Operations/TenantPulseDescriptorContracts.Tests.ps1 @@ -6,6 +6,29 @@ BeforeAll { Import-Module (Join-Path $built.FullName 'GraphKit.psd1') -Force $script:catalog = @(Get-GraphOperation -List) + + $script:Context = [PSCustomObject]@{ + Cloud = 'Global' + GraphBaseUri = [uri] 'https://graph.microsoft.com' + ProfileId = 'contract-test' + TenantId = [guid] '00000000-0000-0000-0000-000000000001' + ClientId = '00000000-0000-0000-0000-000000000010' + IdentityState = 'VerifiedForToken' + TokenSource = [PSCustomObject]@{ AuthMode = 'Certificate' } + } + + function New-ContractTestEnvelope { + param([object[]] $Data) + + [PSCustomObject]@{ + PSTypeName = 'GraphKit.OperationResult' + Data = $Data + Outcome = 'Succeeded' + Certainty = 'Known' + Telemetry = @() + Provenance = $null + } + } } Describe 'TenantPulse collection descriptor contracts' { @@ -116,7 +139,7 @@ Describe 'TenantPulse collection descriptor contracts' { } It 'preserves the documented polymorphic Apple enrollment-profile response fields' { - $fixture = [pscustomobject]@{ + $script:AppleEnrollmentProfileFixture = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.depIOSEnrollmentProfile' id = 'profile-1' displayName = 'Corporate iOS' @@ -129,16 +152,28 @@ Describe 'TenantPulse collection descriptor contracts' { isMandatory = $false } - # GraphKit's collection transport is intentionally schema-neutral. This pins the - # operation-specific minimum shape TenantPulse relies on and prevents a future - # projection from treating only the base enrollmentProfile fields as complete. - @($fixture.PSObject.Properties.Name) | Should -Contain '@odata.type' - @($fixture.PSObject.Properties.Name) | Should -Contain 'id' - @($fixture.PSObject.Properties.Name) | Should -Contain 'displayName' - @($fixture.PSObject.Properties.Name) | Should -Contain 'description' - @($fixture.PSObject.Properties.Name) | Should -Contain 'requiresUserAuthentication' - @($fixture.PSObject.Properties.Name) | Should -Contain 'isDefault' - @($fixture.PSObject.Properties.Name) | Should -Contain 'isMandatory' + Mock Invoke-GraphPaging -ModuleName GraphKit { + New-ContractTestEnvelope -Data @($script:AppleEnrollmentProfileFixture) + } + Mock Invoke-GraphHandlerStrategy -ModuleName GraphKit { throw 'unexpected singleton strategy call' } + + $rows = @(Get-GraphObject ` + -Context $script:Context ` + -Type AppleEnrollmentProfile ` + -Operation ListByToken ` + -Parameters @{ depOnboardingSettingId = 'token-1' }) + + $rows | Should -HaveCount 1 + $rows[0].PSObject.TypeNames | Should -Contain 'GraphKit.AppleEnrollmentProfile' + $rows[0].'@odata.type' | Should -BeExactly '#microsoft.graph.depIOSEnrollmentProfile' + $rows[0].id | Should -BeExactly 'profile-1' + $rows[0].displayName | Should -BeExactly 'Corporate iOS' + $rows[0].description | Should -BeExactly 'Automated enrollment' + $rows[0].requiresUserAuthentication | Should -BeTrue + $rows[0].isDefault | Should -BeTrue + $rows[0].isMandatory | Should -BeFalse + Should-Invoke Invoke-GraphPaging -ModuleName GraphKit -Times 1 -Exactly + Should-NotInvoke Invoke-GraphHandlerStrategy -ModuleName GraphKit } It 'ships the beta managed-device singleton needed for authoritative hardware detail' { @@ -149,8 +184,8 @@ Describe 'TenantPulse collection descriptor contracts' { $descriptor | Should -Not -BeNullOrEmpty $descriptor.ApiVersion | Should -Be 'beta' $descriptor.Stability | Should -Be 'DualVersion' - $descriptor.OperationKind | Should -Be 'Collection' - $descriptor.HandlerStrategyId | Should -Be 'Collection.Default' + $descriptor.OperationKind | Should -Be 'Singleton' + $descriptor.HandlerStrategyId | Should -Be 'Singleton.Default' $descriptor.PathTemplate | Should -Be '/deviceManagement/managedDevices/{id}?$select=id,hardwareInformation,deviceHealthAttestationState,physicalMemoryInBytes,processorArchitecture,skuFamily,skuNumber,managementFeatures,roleScopeTagIds,ethernetMacAddress,bootstrapTokenEscrowed' $descriptor.PagingStrategy | Should -Be 'None' $descriptor.AdvancedQuery.Supported | Should -BeFalse @@ -160,7 +195,7 @@ Describe 'TenantPulse collection descriptor contracts' { } It 'preserves the documented managed-device hardware and attestation response fields' { - $fixture = [pscustomobject]@{ + $script:ManagedDeviceDetailFixture = [pscustomobject]@{ id = 'device-1' operatingSystem = 'Windows' hardwareInformation = [pscustomobject]@{ @@ -176,10 +211,26 @@ Describe 'TenantPulse collection descriptor contracts' { } } - $fixture.id | Should -Not -BeNullOrEmpty - $fixture.hardwareInformation | Should -Not -BeNullOrEmpty - $fixture.deviceHealthAttestationState | Should -Not -BeNullOrEmpty - @($fixture.hardwareInformation.PSObject.Properties.Name) | Should -Contain 'tpmVersion' - @($fixture.deviceHealthAttestationState.PSObject.Properties.Name) | Should -Contain 'secureBoot' + Mock Invoke-GraphHandlerStrategy -ModuleName GraphKit { + New-ContractTestEnvelope -Data @($script:ManagedDeviceDetailFixture) + } + Mock Invoke-GraphPaging -ModuleName GraphKit { throw 'unexpected paging call' } + + $rows = @(Get-GraphObject ` + -Context $script:Context ` + -Type ManagedDevice ` + -Operation GetBeta ` + -Parameters @{ id = 'device-1' }) + + $rows | Should -HaveCount 1 + $rows[0].PSObject.TypeNames | Should -Contain 'GraphKit.ManagedDevice' + $rows[0].id | Should -BeExactly 'device-1' + $rows[0].hardwareInformation.tpmVersion | Should -BeExactly '2.0' + $rows[0].deviceHealthAttestationState.secureBoot | Should -BeExactly 'enabled' + Should-Invoke Invoke-GraphHandlerStrategy -ModuleName GraphKit -Times 1 -Exactly -ParameterFilter { + $Descriptor.OperationKind -eq 'Singleton' -and + $Descriptor.HandlerStrategyId -eq 'Singleton.Default' + } + Should-NotInvoke Invoke-GraphPaging -ModuleName GraphKit } }