diff --git a/docs/extend/steps.md b/docs/extend/steps.md index 4f80e4e4..c9afffc0 100644 --- a/docs/extend/steps.md +++ b/docs/extend/steps.md @@ -147,6 +147,28 @@ so hosts do not need `Providers.StepRegistry` for step pack steps: Modules named `IdLE.Steps.*` that export `Get-IdleStepMetadataCatalog` but carry no marker are still discovered. This fallback is deprecated. +### Provenance + +Plans and execution results record which module provided each step handler and each provider +(e.g. for audits: "which code disabled this account?"): + +```powershell +$plan.Steps[0].Source # @{ Module = 'IdLE.Steps.Common'; Version = '1.0.0' } +$plan.ProviderSources['Identity'] +$result.Steps[0].Source # handler that actually ran +$result.ProviderSources['Identity'] +``` + +- Handlers without a module (e.g. host functions registered via `Providers.StepRegistry`) and handlers that + are not resolvable at planning time are recorded with `Module = $null` and `Version = $null`. +- `Source` records names and versions as loaded; it is not an integrity check of the module files. +- In execution results, `Source` is `$null` for steps whose handler did not run (`NotApplicable`, + `Blocked`, `PreconditionSkipped`). +- `ProviderSources` lists only provider objects (entries exposing `GetCapabilities()`). The module of a + provider is the module that defines its `GetCapabilities` method. +- The plan export contains the same information (`steps[].source`, `plan.providers`); see the + [plan export specification](../reference/specs/plan-export.md). + Metadata exists to make steps: - understandable diff --git a/docs/reference/specs/plan-export.md b/docs/reference/specs/plan-export.md index a11bc4ba..d0137c34 100644 --- a/docs/reference/specs/plan-export.md +++ b/docs/reference/specs/plan-export.md @@ -1,18 +1,21 @@ # IdLE Plan Export Specification -Schema-Version: 1.0 +Schema-Version: 1.1 ## Overview + This document specifies the canonical, machine-readable representation of an IdLE plan export. It defines structure, required fields, and normative rules for producers and consumers. The exported plan is a **contract artifact**, not an internal object dump. ## Scope + This specification focuses on format and semantics. Operational guidance is documented separately in the User Guide. ## Format + The export is a JSON document encoded in UTF-8. --- @@ -47,7 +50,7 @@ The plan export MUST NOT contain: ```json { - "schemaVersion": "1.0", + "schemaVersion": "1.1", "engine": { "name": "IdLE" }, @@ -167,7 +170,14 @@ This bound prevents unbounded snapshot artifacts while keeping the marker audita "plan": { "id": "plan-001", "mode": null, - "steps": [] + "steps": [], + "providers": [ + { + "alias": "Identity", + "module": "IdLE.Provider.AD", + "version": "1.0.0" + } + ] } ``` @@ -179,12 +189,20 @@ This bound prevents unbounded snapshot artifacts while keeping the marker audita | createdAt | (Optional) ISO-8601 UTC timestamp | | mode | Plan lifecycle state (`null` when not explicitly set on the plan object) | | steps | Ordered list of step objects | +| providers | (Since 1.1) Provenance of each provider used for planning, ordered by alias. Only provider objects are listed; provider configuration is never exported. | + +### providers entries + +| Field | Description | +| ------ | ------------ | +| alias | Provider alias in the host's `Providers` map | +| module | Module that defines the provider, or `null` if unknown (e.g. a provider created in a host script) | +| version | Version of that module, or `null` | --- ## Step Object - Each step is an **atomic, independent unit**. ```json @@ -202,6 +220,10 @@ Each step is an **atomic, independent unit**. }, "expectedState": { "MailboxExists": true + }, + "source": { + "module": "IdLE.Steps.Mailbox", + "version": "1.0.0" } } ``` @@ -212,6 +234,10 @@ Each step is an **atomic, independent unit**. - Steps are ordered and deterministic - `stepType` is a logical identifier, not a module path - `provider` is declarative, not an implementation reference +- `source` (since 1.1) records the module that provides the step handler at planning time + (`module`, `version`). `module` and `version` are `null` if the handler has no module or is + only registered at execution time. `source` is informational for audits and MUST NOT be used to select + the handler at execution time. --- diff --git a/docs/use/plan-export.md b/docs/use/plan-export.md index 86bcc405..f916d794 100644 --- a/docs/use/plan-export.md +++ b/docs/use/plan-export.md @@ -43,6 +43,7 @@ A plan export typically includes: - the planned step list in execution order - step configuration after template resolution (as applicable) - provider alias references (but **not** live provider objects) +- provenance: which module (and version) provides each step handler and provider - capability information (if available) :::info @@ -56,7 +57,7 @@ Providers and authentication are always supplied by the host at execution time. ```json { - "schemaVersion": "1.0", + "schemaVersion": "1.1", "engine": { "name": "IdLE" }, @@ -91,9 +92,20 @@ Providers and authentication are always supplied by the host at execution time. "mailboxType": "User" }, "expectedState": null, + "source": { + "module": "IdLE.Steps.Mailbox", + "version": "1.0.0" + }, "warnings": [] } ], + "providers": [ + { + "alias": "ExchangeOnline", + "module": "IdLE.Provider.ExchangeOnline", + "version": "1.0.0" + } + ], "warnings": [] }, "metadata": { diff --git a/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 b/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 index a17419ea..6fb75268 100644 --- a/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 +++ b/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 @@ -29,6 +29,26 @@ function ConvertTo-IdlePlanExportObject { return [ordered] @{} } + # Maps an internal source record (@{ Module; Version }) to the export shape. + # Returns $null when no source was recorded (e.g. plans built by older engine versions). + function ConvertTo-IdleSourceExportMap { + [CmdletBinding()] + param( + [Parameter()] + [AllowNull()] + [object] $Source + ) + + if ($null -eq $Source) { + return $null + } + + $map = [ordered] @{} + $map.module = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $Source -Names @('Module')) + $map.version = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $Source -Names @('Version')) + return $map + } + function Get-FirstPropertyValue { [CmdletBinding()] param( @@ -275,6 +295,7 @@ function ConvertTo-IdlePlanExportObject { $stepMap.condition = $conditionMap $stepMap.inputs = $redactedInputs $stepMap.expectedState = $redactedExpectedState + $stepMap.source = ConvertTo-IdleSourceExportMap -Source (Get-FirstPropertyValue -Object $step -Names @('Source')) # Per-step planning warnings (e.g. unresolved precondition context paths). $rawStepWarnings = Get-FirstPropertyValue -Object $step -Names @('Warnings', 'PlanningWarnings', 'StepWarnings') @@ -314,10 +335,26 @@ function ConvertTo-IdlePlanExportObject { $warningList += $warningMap } + # ---- Provider sources (provenance) -------------------------------------- + # Ordered by alias (as recorded at planning time); provider configuration is never exported. + $rawProviderSources = Get-FirstPropertyValue -Object $Plan -Names @('ProviderSources') + $providerList = @() + if ($rawProviderSources -is [System.Collections.IDictionary]) { + foreach ($alias in $rawProviderSources.Keys) { + $providerMap = New-OrderedMap + $providerMap.alias = [string]$alias + $sourceMap = ConvertTo-IdleSourceExportMap -Source $rawProviderSources[$alias] + $providerMap.module = $sourceMap.module + $providerMap.version = $sourceMap.version + $providerList += $providerMap + } + } + $planMap = New-OrderedMap $planMap.id = $planId $planMap.mode = $mode $planMap.steps = $stepList + $planMap.providers = $providerList $planMap.warnings = $warningList # ---- Metadata block ------------------------------------------------------ @@ -328,7 +365,7 @@ function ConvertTo-IdlePlanExportObject { # ---- Root --------------------------------------------------------------- $root = New-OrderedMap - $root.schemaVersion = '1.0' + $root.schemaVersion = '1.1' $root.engine = $engineMap $root.request = $requestMap $root.plan = $planMap diff --git a/src/IdLE.Core/Private/ConvertTo-IdleSourceInfo.ps1 b/src/IdLE.Core/Private/ConvertTo-IdleSourceInfo.ps1 new file mode 100644 index 00000000..17758160 --- /dev/null +++ b/src/IdLE.Core/Private/ConvertTo-IdleSourceInfo.ps1 @@ -0,0 +1,34 @@ +function ConvertTo-IdleSourceInfo { + <# + .SYNOPSIS + Creates the data-only provenance record for a step handler or provider. + + .DESCRIPTION + Returns @{ Module; Version } for the given module. If the module is unknown (e.g. a host-defined + function or provider object without a module), Module and Version are $null. + + .PARAMETER Module + The module that defines the handler or provider, or $null if unknown. + + .OUTPUTS + Ordered hashtable with keys Module, Version. + #> + [CmdletBinding()] + param( + [Parameter()] + [AllowNull()] + [System.Management.Automation.PSModuleInfo] $Module + ) + + if ($null -eq $Module) { + return [ordered]@{ + Module = $null + Version = $null + } + } + + return [ordered]@{ + Module = $Module.Name + Version = $Module.Version.ToString() + } +} diff --git a/src/IdLE.Core/Private/Get-IdleProviderSources.ps1 b/src/IdLE.Core/Private/Get-IdleProviderSources.ps1 new file mode 100644 index 00000000..1193cbf5 --- /dev/null +++ b/src/IdLE.Core/Private/Get-IdleProviderSources.ps1 @@ -0,0 +1,60 @@ +function Get-IdleProviderSources { + <# + .SYNOPSIS + Returns the provenance of every provider in the Providers map (alias -> source record). + + .DESCRIPTION + Providers are map entries that expose GetCapabilities(). Other entries (StepRegistry, StepMetadata, + AuthSessionBroker, placeholders) are not providers and are skipped. + + The module of a provider is the module that defines its GetCapabilities script method. Provider + objects created outside a module (e.g. in a host script) have no module. + + .PARAMETER Providers + The Providers map passed to planning or execution. + + .OUTPUTS + Ordered hashtable mapping provider alias (sorted) to a source record (see ConvertTo-IdleSourceInfo). + #> + [CmdletBinding()] + param( + [Parameter()] + [AllowNull()] + [object] $Providers + ) + + $sources = [ordered]@{} + + if ($null -eq $Providers) { + return $sources + } + + $entries = @{} + if ($Providers -is [System.Collections.IDictionary]) { + foreach ($key in $Providers.Keys) { + $entries[[string]$key] = $Providers[$key] + } + } + else { + foreach ($prop in $Providers.PSObject.Properties) { + $entries[$prop.Name] = $prop.Value + } + } + + foreach ($alias in @($entries.Keys | Sort-Object)) { + $provider = $entries[$alias] + if ($null -eq $provider) { + continue + } + + $method = $provider.PSObject.Methods['GetCapabilities'] + if ($null -eq $method) { + continue + } + + $module = if ($method -is [System.Management.Automation.PSScriptMethod]) { $method.Script.Module } else { $null } + $sources[$alias] = ConvertTo-IdleSourceInfo -Module $module + } + + return $sources +} diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index 0d137808..7e8eb6e9 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -497,8 +497,10 @@ function Invoke-IdlePlanObject { } ) + $handlerSource = $null try { $impl = Resolve-IdleStepHandler -StepType ([string]$stepType) -StepRegistry $stepRegistry + $handlerSource = ConvertTo-IdleSourceInfo -Module $impl.Module $supportedParams = Get-IdleCommandParameterNames -Handler $impl @@ -554,6 +556,8 @@ function Invoke-IdlePlanObject { } } + $null = $result | Add-Member -MemberType NoteProperty -Name Source -Value $handlerSource -Force + $stepResults += $result if ($result.Status -eq 'Failed') { @@ -594,6 +598,7 @@ function Invoke-IdlePlanObject { Status = 'Failed' Error = $err.Exception.Message Attempts = 1 + Source = $handlerSource } $context.EventSink.WriteEvent( @@ -783,8 +788,10 @@ function Invoke-IdlePlanObject { } ) + $handlerSource = $null try { $impl = Resolve-IdleStepHandler -StepType ([string]$ofType) -StepRegistry $stepRegistry + $handlerSource = ConvertTo-IdleSourceInfo -Module $impl.Module $supportedParams = Get-IdleCommandParameterNames -Handler $impl @@ -838,6 +845,8 @@ function Invoke-IdlePlanObject { } } + $null = $result | Add-Member -MemberType NoteProperty -Name Source -Value $handlerSource -Force + $onFailureStepResults += $result if ($result.Status -eq 'Failed') { @@ -877,6 +886,7 @@ function Invoke-IdlePlanObject { Status = 'Failed' Error = $err.Exception.Message Attempts = 1 + Source = $handlerSource } $context.EventSink.WriteEvent( @@ -933,14 +943,22 @@ function Invoke-IdlePlanObject { $null } + # Steps whose handler did not run (NotApplicable, Blocked, PreconditionSkipped) have no source. + foreach ($stepResult in @($stepResults) + @($onFailure.Steps)) { + if ($null -ne $stepResult -and $stepResult.PSObject.Properties.Name -notcontains 'Source') { + $null = $stepResult | Add-Member -MemberType NoteProperty -Name Source -Value $null -Force + } + } + return [pscustomobject]@{ - PSTypeName = 'IdLE.ExecutionResult' - Status = $runStatus - CorrelationId = $corr - Actor = $actor - Steps = @($stepResults) - OnFailure = $onFailure - Events = $events - Providers = $redactedProviders + PSTypeName = 'IdLE.ExecutionResult' + Status = $runStatus + CorrelationId = $corr + Actor = $actor + Steps = @($stepResults) + OnFailure = $onFailure + Events = $events + Providers = $redactedProviders + ProviderSources = Get-IdleProviderSources -Providers $effectiveProviders } } diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 43c473df..cef20b61 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -136,5 +136,33 @@ function New-IdlePlanObject { Assert-IdlePlanCapabilitiesSatisfied -Steps $allStepsForCapabilities -Providers $Providers + # Provenance (audit): record which module provides each step handler and each provider. + # Planning-time view; execution results record the handlers that actually ran. + # StepRegistry validation stays at execution time; planning only records what it can resolve. + $stepRegistry = $null + try { + $stepRegistry = Get-IdleStepRegistry -Providers $Providers + } + catch { + Write-Verbose "Step registry not available at planning time; step handler sources are recorded as unknown: $($_.Exception.Message)" + } + + foreach ($planStep in $allStepsForCapabilities) { + $handlerModule = $null + if ($null -ne $stepRegistry) { + try { + $handlerModule = (Resolve-IdleStepHandler -StepType ([string]$planStep.Type) -StepRegistry $stepRegistry).Module + } + catch { + # The handler may only be registered at execution time (Invoke-IdlePlan -Providers); record an unknown source. + Write-Verbose "No step handler resolvable at planning time for step type '$($planStep.Type)': $($_.Exception.Message)" + } + } + + $null = $planStep | Add-Member -MemberType NoteProperty -Name Source -Value (ConvertTo-IdleSourceInfo -Module $handlerModule) -Force + } + + $null = $plan | Add-Member -MemberType NoteProperty -Name ProviderSources -Value (Get-IdleProviderSources -Providers $Providers) -Force + return $plan } diff --git a/tests/Core/Export-IdlePlan.Tests.ps1 b/tests/Core/Export-IdlePlan.Tests.ps1 index bea1602f..f9db7a34 100644 --- a/tests/Core/Export-IdlePlan.Tests.ps1 +++ b/tests/Core/Export-IdlePlan.Tests.ps1 @@ -128,7 +128,7 @@ Describe 'Export-IdlePlan' { } } Context 'Contract invariants' { - It 'always includes schemaVersion 1.0' { + It 'always includes schemaVersion 1.1' { $cid = '11111111-1111-1111-1111-111111111111' $wfPath = New-IdleTestWorkflowFile -FileName 'joiner-export-empty.psd1' -Content @' @@ -148,7 +148,7 @@ Describe 'Export-IdlePlan' { $json = $plan | Export-IdlePlan | ConvertFrom-Json - $json.schemaVersion | Should -Be '1.0' + $json.schemaVersion | Should -Be '1.1' } } } diff --git a/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 b/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 index 0d26acbf..6fc61d26 100644 --- a/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 +++ b/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 @@ -115,7 +115,9 @@ Describe 'Invoke-IdlePlan - ExecutionOptions' { $req = New-IdleTestRequest -LifecycleEvent 'Joiner' $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req - { Invoke-IdlePlan -Plan $plan -ExecutionOptions 'invalid' } | Should -Throw -ExpectedMessage '*Cannot convert*Hashtable*' + # Assert on type and error id instead of the message: PowerShell localizes binding errors. + { Invoke-IdlePlan -Plan $plan -ExecutionOptions 'invalid' } | + Should -Throw -ExceptionType ([System.Management.Automation.ParameterBindingException]) -ErrorId 'ParameterArgumentTransformationError,Invoke-IdlePlan' } It 'rejects ExecutionOptions with ScriptBlocks' { diff --git a/tests/Core/Provenance.Tests.ps1 b/tests/Core/Provenance.Tests.ps1 new file mode 100644 index 00000000..8a79e6a7 --- /dev/null +++ b/tests/Core/Provenance.Tests.ps1 @@ -0,0 +1,173 @@ +Set-StrictMode -Version Latest + +BeforeAll { + . (Join-Path (Split-Path -Path $PSScriptRoot -Parent) '_testHelpers.ps1') + Import-IdleTestModule + + function global:Invoke-IdleProvenanceTestHostStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)] [object] $Context, + [Parameter(Mandatory)] [object] $Step + ) + + return [pscustomobject]@{ + PSTypeName = 'IdLE.StepResult' + Name = [string]$Step.Name + Type = [string]$Step.Type + Status = 'Completed' + Error = $null + } + } + + function global:Invoke-IdleProvenanceTestFailingStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)] [object] $Context, + [Parameter(Mandatory)] [object] $Step + ) + + throw 'boom' + } + + $script:CommonVersion = (Get-Module -Name 'IdLE.Steps.Common' -All | Select-Object -First 1).Version.ToString() + $script:MockVersion = (Get-Module -Name 'IdLE.Provider.Mock' -All | Select-Object -First 1).Version.ToString() + + $script:HostProvider = [pscustomobject]@{ Name = 'HostProvider' } + $script:HostProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { @() } -Force + + function New-ProvenanceTestPlan { + param( + [Parameter()] + [hashtable] $Providers = @{} + ) + + $wfPath = New-IdleTestWorkflowFile -FileName 'provenance.psd1' -Content @' +@{ + Name = 'Provenance' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'Emit'; Type = 'IdLE.Step.EmitEvent'; With = @{ Message = 'Hello' } } + @{ + Name = 'Skipped' + Type = 'IdLE.Step.EmitEvent' + Condition = @{ Equals = @{ Path = 'Plan.LifecycleEvent'; Value = 'Leaver' } } + } + @{ Name = 'Host'; Type = 'Test.IdLE.Step.Host' } + ) + OnFailureSteps = @( + @{ Name = 'Cleanup'; Type = 'IdLE.Step.EmitEvent'; With = @{ Message = 'Cleanup' } } + ) +} +'@ + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' + return New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $Providers + } + + function New-ProvenanceTestProviders { + param( + [Parameter(Mandatory)] + [string] $HostHandler + ) + + return @{ + Identity = New-IdleMockIdentityProvider + Host = $script:HostProvider + Dummy = $true + StepRegistry = @{ 'Test.IdLE.Step.Host' = $HostHandler } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('Test.IdLE.Step.Host') + } + } +} + +AfterAll { + Remove-Item -Path 'Function:\Invoke-IdleProvenanceTestHostStep' -ErrorAction SilentlyContinue + Remove-Item -Path 'Function:\Invoke-IdleProvenanceTestFailingStep' -ErrorAction SilentlyContinue +} + +Describe 'Provenance of step handlers and providers' { + Context 'Plan' { + It 'records handler sources with module and version' { + $plan = New-ProvenanceTestPlan -Providers (New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestHostStep') + + $source = $plan.Steps[0].Source + $source.Module | Should -Be 'IdLE.Steps.Common' + $source.Version | Should -Be $script:CommonVersion + + $plan.OnFailureSteps[0].Source.Module | Should -Be 'IdLE.Steps.Common' + } + + It 'records host handlers without a module as unknown' { + $plan = New-ProvenanceTestPlan -Providers (New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestHostStep') + + $source = $plan.Steps[2].Source + $source.Module | Should -BeNullOrEmpty + $source.Version | Should -BeNullOrEmpty + } + + It 'records an unknown source when the handler is only registered at execution time' { + $providers = New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestHostStep' + $providers.Remove('StepRegistry') + + $plan = New-ProvenanceTestPlan -Providers $providers + + $plan.Steps[2].Source.Module | Should -BeNullOrEmpty + } + + It 'records provider sources per alias (providers only, sorted)' { + $plan = New-ProvenanceTestPlan -Providers (New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestHostStep') + + @($plan.ProviderSources.Keys) | Should -Be @('Host', 'Identity') + + $plan.ProviderSources['Identity'].Module | Should -Be 'IdLE.Provider.Mock' + $plan.ProviderSources['Identity'].Version | Should -Be $script:MockVersion + + $plan.ProviderSources['Host'].Module | Should -BeNullOrEmpty + } + } + + Context 'Execution result' { + It 'records the source of each executed handler and $null for steps that did not run' { + $providers = New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestHostStep' + $plan = New-ProvenanceTestPlan -Providers $providers + + $result = Invoke-IdlePlan -Plan $plan -Providers $providers + + $result.Status | Should -Be 'Completed' + $result.Steps[0].Source.Module | Should -Be 'IdLE.Steps.Common' + $result.Steps[1].Status | Should -Be 'NotApplicable' + $result.Steps[1].Source | Should -BeNullOrEmpty + $result.Steps[2].Source.Module | Should -BeNullOrEmpty + + $result.ProviderSources['Identity'].Module | Should -Be 'IdLE.Provider.Mock' + $result.ProviderSources.Contains('StepRegistry') | Should -BeFalse + } + + It 'records sources for failed steps and OnFailure steps' { + $providers = New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestFailingStep' + $plan = New-ProvenanceTestPlan -Providers $providers + + $result = Invoke-IdlePlan -Plan $plan -Providers $providers + + $result.Status | Should -Be 'Failed' + $result.Steps[2].Status | Should -Be 'Failed' + $result.OnFailure.Steps[0].Source.Module | Should -Be 'IdLE.Steps.Common' + } + } + + Context 'Plan export' { + It 'exports step and provider sources' { + $plan = New-ProvenanceTestPlan -Providers (New-ProvenanceTestProviders -HostHandler 'Invoke-IdleProvenanceTestHostStep') + + $json = $plan | Export-IdlePlan | ConvertFrom-Json + + $json.schemaVersion | Should -Be '1.1' + $json.plan.steps[0].source.module | Should -Be 'IdLE.Steps.Common' + $json.plan.steps[0].source.version | Should -Be $script:CommonVersion + + @($json.plan.providers).Count | Should -Be 2 + $json.plan.providers[1].alias | Should -Be 'Identity' + $json.plan.providers[1].module | Should -Be 'IdLE.Provider.Mock' + } + } +} diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 index be8e4c70..8c246765 100644 --- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 +++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 @@ -181,8 +181,10 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' { $thrown = $_ } + # Assert on exception types instead of the 'Exception calling ...' text: PowerShell localizes it. $thrown | Should -Not -BeNullOrEmpty - $thrown.Exception.Message | Should -Match 'Exception calling "StartSyncCycle"' + $thrown.Exception | Should -BeOfType ([System.Management.Automation.MethodInvocationException]) + $thrown.Exception.InnerException | Should -BeOfType ([System.Management.Automation.ValidationMetadataException]) $thrown.Exception.Message | Should -Match 'Invalid' $thrown.Exception.Message | Should -Not -Match 'TriggerDirectorySync: With.PolicyType' } diff --git a/tests/fixtures/plan-export/expected/plan-export.json b/tests/fixtures/plan-export/expected/plan-export.json index 8c6a5095..1dde597d 100644 --- a/tests/fixtures/plan-export/expected/plan-export.json +++ b/tests/fixtures/plan-export/expected/plan-export.json @@ -1,5 +1,5 @@ { - "schemaVersion": "1.0", + "schemaVersion": "1.1", "engine": { "name": "IdLE" }, @@ -34,9 +34,14 @@ "mailboxType": "User" }, "expectedState": null, + "source": { + "module": null, + "version": null + }, "warnings": [] } ], + "providers": [], "warnings": [] }, "metadata": {