diff --git a/docs/reference/providers/provider-directorysync-entraconnect.md b/docs/reference/providers/provider-directorysync-entraconnect.md
index f4a4ecb5..568d48f7 100644
--- a/docs/reference/providers/provider-directorysync-entraconnect.md
+++ b/docs/reference/providers/provider-directorysync-entraconnect.md
@@ -11,7 +11,7 @@ import EntraConnectTriggerSync from '@site/../examples/workflows/templates/direc
- **Module:** `IdLE.Provider.DirectorySync.EntraConnect`
- **What it’s for:** Triggering and monitoring **Entra Connect (ADSync)** sync cycles on an on-prem server
-- **Execution model:** Remote execution via a host-provided AuthSession (elevated context)
+- **Execution model:** Remote execution via provider-managed PSRemoting using a host-provided credential
## When to use
@@ -35,7 +35,7 @@ Non-goals:
### Requirements
- An Entra Connect (Azure AD Connect) server with ADSync installed (ADSync cmdlets available)
-- A host/runtime that can provide an **elevated remote execution handle** to IdLE via AuthSessionBroker
+- A host/runtime that can provide an **elevated credential** to IdLE via AuthSessionBroker
- Rights to run `Start-ADSyncSyncCycle` and `Get-ADSyncScheduler` in that remote context
### Install (PowerShell Gallery)
@@ -66,52 +66,58 @@ $providers = @{
}
```
-## Authentication (important)
+## Authentication
-This provider requires an AuthSession that supports remote execution and **must be elevated**.
+This provider requires an AuthSession credential ([PSCredential]) and **must be elevated**.
+The provider creates and cleans up PSRemoting sessions internally.
-The AuthSession object must provide a method:
+There is no integrated/run-as authentication fallback; a credential-backed AuthSession must be supplied at runtime via the AuthSessionBroker.
+To select the runtime credential for this provider, pass the AuthSession via step configuration:
-- `InvokeCommand(CommandName, Parameters)`
+- With.AuthSessionName
+- With.AuthSessionOptions (optional)
-Your host/runtime should provide this session via the AuthSessionBroker and you reference it in the step via:
+> Keep credentials/secrets out of workflow files. Use the broker/host to resolve them at runtime.
-- `AuthSessionName = 'EntraConnect'`
-- `AuthSessionOptions = @{ Role = 'EntraConnectAdmin' }` (optional routing key)
+## Supported Step Types
-> No interactive prompts are made. If the remote context is not elevated, triggering a sync cycle will fail with a privilege/elevation error.
+The Directory Sync (Entra Connect) provider supports the directory sync step types listed below:
-## Supported operations
-
-This provider advertises these capabilities:
-
-- `IdLE.DirectorySync.Trigger`
-- `IdLE.DirectorySync.Status`
-
-Those are typically used by step types like:
-
-- `IdLE.Step.TriggerDirectorySync` (trigger + optional wait/poll)
+| Step type | Typical use | Notes |
+| --- | --- | --- |
+| `IdLE.Step.TriggerDirectorySync` | Trigger Directory Sync | Executed via a provider-managed PSRemoting session, with optional wait/poll |
## Context Resolvers
This provider does **not** support any of the allowlisted Context Resolver capabilities.
-Context Resolvers can only use read-only capabilities like `IdLE.Identity.Read` and `IdLE.Entitlement.List`.
-This provider does not advertise these capabilities, so it cannot be used in the workflow `ContextResolvers` section.
-
## Configuration
-This provider has no admin-facing option bag. Configuration is done through:
-- step inputs (`PolicyType`, `Wait`, `TimeoutSeconds`, `PollIntervalSeconds`)
-- host configuration (remote connection and elevation)
+This provider does **not** expose an admin-facing provider option bag.
+Configuration for triggering and monitoring sync is supplied through the
+`IdLE.Step.TriggerDirectorySync` step inputs via `With.*` keys.
+
+The generic step schema does not require any `With.*` keys at schema level for this
+step type. However, this provider requires specific inputs during provider validation
+and execution, as noted below.
+
+### Step input reference
+
+| Step input | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `With.ComputerName` | `string` | Required by provider | ComputerName for PSSession connection |
+| `With.PolicyType` | `string` | Required by provider | `Delta` or `Initial` sync policy |
+| `With.Wait` | `bool` | `false` | Poll sync status and wait for result (or timeout) |
+| `With.PollIntervalSeconds` | `int` | `10` | Interval in seconds to poll for sync status |
+| `With.TimeoutSeconds` | `int` | `600` | Timeout for poll wait in seconds. Will result in `StepFailed` |
-## Examples (canonical template)
+## Examples
{EntraConnectTriggerSync}
## Troubleshooting
-- **“Missing privileges or elevation”**: your AuthSession must run commands in an elevated context on the Entra Connect server.
-- **“AuthSession must implement InvokeCommand”**: your host must provide an AuthSession object with an `InvokeCommand()` method.
-- **Get-ADSyncScheduler not found**: ensure ADSync cmdlets are available in the remote session (module installed/accessible).
+- **“Missing privileges or elevation”**: ensure the provided credential is elevated on the Entra Connect server.
+- **“AuthSession must be a [PSCredential]”**: configure the AuthSessionBroker/host runtime to provide a credential-backed AuthSession ([PSCredential]) for this provider.
+- **Get-ADSyncScheduler not found**: ensure ADSync cmdlets are available on the target server.
- **Timeout waiting for completion**: increase `TimeoutSeconds` or check the scheduler state on the server.
diff --git a/docs/reference/steps/step-emit-event.md b/docs/reference/steps/step-emit-event.md
index 6bb78348..bd882516 100644
--- a/docs/reference/steps/step-emit-event.md
+++ b/docs/reference/steps/step-emit-event.md
@@ -22,8 +22,8 @@ to write structured events.
## Inputs (With.*)
-The required input keys could not be detected automatically.
-Please refer to the step description and examples for usage details.
+This step has no required ``With.*`` keys at step schema level.
+Inputs may still be provider-specific; refer to the step description and examples for usage details.
## Example
diff --git a/docs/reference/steps/step-prune-entitlements-ensure-keep.md b/docs/reference/steps/step-prune-entitlements-ensure-keep.md
index 580e1421..047346c6 100644
--- a/docs/reference/steps/step-prune-entitlements-ensure-keep.md
+++ b/docs/reference/steps/step-prune-entitlements-ensure-keep.md
@@ -77,8 +77,8 @@ Authentication:
## Inputs (With.*)
-The required input keys could not be detected automatically.
-Please refer to the step description and examples for usage details.
+This step has no required ``With.*`` keys at step schema level.
+Inputs may still be provider-specific; refer to the step description and examples for usage details.
## Example
diff --git a/docs/reference/steps/step-trigger-directory-sync.md b/docs/reference/steps/step-trigger-directory-sync.md
index f4797ae9..da98cdca 100644
--- a/docs/reference/steps/step-trigger-directory-sync.md
+++ b/docs/reference/steps/step-trigger-directory-sync.md
@@ -19,29 +19,29 @@ Triggers a directory sync cycle and optionally waits for completion.
The host must supply a provider instance via
Context.Providers[<ProviderAlias>] that implements:
-- StartSyncCycle(PolicyType, AuthSession)
+- StartSyncCycle(PolicyType, ComputerName, AuthSession)
-- GetSyncCycleState(AuthSession)
+- GetSyncCycleState(ComputerName, AuthSession)
The step is designed for remote execution and requires an elevated auth session
provided by the host's AuthSessionBroker.
Authentication:
-- With.AuthSessionName (required): routing key for AuthSessionBroker
+- With.AuthSessionName (optional): routing key for AuthSessionBroker
- With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection
+- If AuthSessionName is omitted, the broker is asked for a default session
+
+- ComputerName and PolicyType are provider-specific inputs and are validated by the selected provider
+
- ScriptBlocks in AuthSessionOptions are rejected (security boundary)
## Inputs (With.*)
-The following keys are required in the step's ``With`` configuration:
-
-| Key | Required | Description |
-| --- | --- | --- |
-| `AuthSessionName` | Yes | Name of auth session to use (optional) |
-| `PolicyType` | Yes | Type of policy (e.g., Delta, Initial) |
+This step has no required ``With.*`` keys at step schema level.
+Inputs may still be provider-specific; refer to the step description and examples for usage details.
## Example
@@ -51,6 +51,7 @@ $step = @{
Type = 'IdLE.Step.TriggerDirectorySync'
With = @{
AuthSessionName = 'DirectorySync'
+ ComputerName = 'ad-sync1.corp.local'
PolicyType = 'Delta'
Wait = $true
}
diff --git a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1 b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1
index 9e0d2711..5a9c9c4f 100644
--- a/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1
+++ b/examples/workflows/templates/ad-joiner-entraconnect-entraid.psd1
@@ -30,6 +30,7 @@
AuthSessionOptions = @{
Role = 'EntraConnectAdmin'
}
+ ComputerName = '{{Request.Intent.EntraConnectServer}}'
PolicyType = 'Delta'
Wait = $true
diff --git a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1 b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1
index a8470938..6c06cf9c 100644
--- a/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1
+++ b/examples/workflows/templates/directorysync-entraconnect-trigger-sync.psd1
@@ -10,11 +10,12 @@
With = @{
Provider = 'DirectorySync'
- # Auth session is provided by the host (remote execution handle).
+ # Auth session is provided by the host (credential), with an optional routing key.
AuthSessionName = 'EntraConnect'
AuthSessionOptions = @{
Role = 'EntraConnectAdmin'
}
+ ComputerName = '{{Request.Intent.ComputerName}}'
# Delta or Initial
PolicyType = '{{Request.Intent.PolicyType}}'
@@ -34,4 +35,4 @@
}
}
)
-}
\ No newline at end of file
+}
diff --git a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1 b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1
index 297bb91c..68e90a6f 100644
--- a/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1
+++ b/src/IdLE.Provider.DirectorySync.EntraConnect/Public/New-IdleEntraConnectDirectorySyncProvider.ps1
@@ -7,16 +7,15 @@ function New-IdleEntraConnectDirectorySyncProvider {
This provider triggers and monitors Entra ID Connect (ADSync) sync cycles on an
on-premises server via remote execution.
- The provider uses an AuthSession object (remote execution handle) provided by the host.
- The AuthSession must implement InvokeCommand(CommandName, Parameters) to execute
- commands in an elevated/privileged context on the Entra Connect server.
+ The provider uses a credential AuthSession provided by the host and establishes
+ a PSRemoting session to the target Entra Connect server internally.
No interactive prompts are made; elevation and authentication are the host's responsibility
via the AuthSessionBroker.
.OUTPUTS
PSCustomObject
- Provider instance with methods: GetCapabilities(), StartSyncCycle(PolicyType, AuthSession), GetSyncCycleState(AuthSession)
+ Provider instance with methods: GetCapabilities(), StartSyncCycle(PolicyType, ComputerName, AuthSession), GetSyncCycleState(ComputerName, AuthSession)
.EXAMPLE
$provider = New-IdleEntraConnectDirectorySyncProvider
@@ -24,15 +23,10 @@ function New-IdleEntraConnectDirectorySyncProvider {
# Returns: @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status')
.EXAMPLE
- # With a mock remote execution handle
- $mockAuthSession = [pscustomobject]@{
- InvokeCommand = { param($CommandName, $Parameters)
- # Mock implementation
- return @{ Started = $true }
- }
- }
+ # With a credential from AuthSessionBroker (AuthSessionType='Credential')
+ $credential = Get-Credential
$provider = New-IdleEntraConnectDirectorySyncProvider
- $result = $provider.StartSyncCycle('Delta', $mockAuthSession)
+ $result = $provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $credential)
#>
[CmdletBinding()]
param()
@@ -58,6 +52,60 @@ function New-IdleEntraConnectDirectorySyncProvider {
)
} -Force
+ $provider | Add-Member -MemberType ScriptMethod -Name NewRemoteSession -Value {
+ param(
+ [Parameter(Mandatory)]
+ [string] $ComputerName,
+
+ [Parameter(Mandatory)]
+ [pscredential] $Credential
+ )
+
+ try {
+ return New-PSSession -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop
+ }
+ catch {
+ throw "Failed to establish PSRemoting session to '$ComputerName': $($_.Exception.Message)"
+ }
+ } -Force
+
+ $provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value {
+ param(
+ [Parameter(Mandatory)]
+ [object] $Session,
+
+ [Parameter(Mandatory)]
+ [scriptblock] $ScriptBlock,
+
+ [Parameter()]
+ [AllowNull()]
+ [object[]] $ArgumentList
+ )
+
+ if ($null -eq $ArgumentList -or $ArgumentList.Count -eq 0) {
+ return Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ErrorAction Stop
+ }
+
+ return Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
+ } -Force
+
+ $provider | Add-Member -MemberType ScriptMethod -Name RemoveRemoteSession -Value {
+ param(
+ [Parameter(Mandatory)]
+ [AllowNull()]
+ [object] $Session
+ )
+
+ if ($null -ne $Session) {
+ try {
+ Remove-PSSession -Session $Session -ErrorAction Stop
+ }
+ catch {
+ Write-Warning "Failed to remove PSRemoting session: $($_.Exception.Message)"
+ }
+ }
+ } -Force
+
$provider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value {
<#
.SYNOPSIS
@@ -69,9 +117,11 @@ function New-IdleEntraConnectDirectorySyncProvider {
.PARAMETER PolicyType
The sync policy type: 'Delta' or 'Initial'.
+ .PARAMETER ComputerName
+ Target Entra Connect server hostname for PSRemoting.
+
.PARAMETER AuthSession
- Remote execution handle provided by the host's AuthSessionBroker.
- Must implement InvokeCommand(CommandName, Parameters).
+ Credential ([PSCredential]) provided by the host's AuthSessionBroker.
.OUTPUTS
PSCustomObject with properties:
@@ -83,29 +133,34 @@ function New-IdleEntraConnectDirectorySyncProvider {
[ValidateSet('Delta', 'Initial', IgnoreCase = $true)]
[string] $PolicyType,
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })]
+ [string] $ComputerName,
+
[Parameter(Mandatory)]
[ValidateNotNull()]
[object] $AuthSession
)
- # Validate AuthSession contract
- if ($null -eq $AuthSession.PSObject.Methods['InvokeCommand']) {
- throw "AuthSession must implement InvokeCommand(CommandName, Parameters) method. " + `
- "The host must provide an elevated remote session via AuthSessionBroker."
+ if ($AuthSession -isnot [pscredential]) {
+ $actualType = $AuthSession.GetType().FullName
+ throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]"
}
+ $remoteSession = $null
try {
- # Execute Start-ADSyncSyncCycle remotely
- # The remote session should already have ADSync module available or will import it
- $AuthSession.InvokeCommand('Start-ADSyncSyncCycle', @{
- PolicyType = $PolicyType
- }) | Out-Null
-
- # Start-ADSyncSyncCycle returns a result object or throws on error
- # Success case: return Started = true
+ $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession)
+
+ $this.InvokeRemoteCommand($remoteSession, {
+ param([string] $RemotePolicyType)
+ Import-Module -Name ADSync -ErrorAction Stop
+ Start-ADSyncSyncCycle -PolicyType $RemotePolicyType -ErrorAction Stop
+ }, @($PolicyType)) | Out-Null
+
return [pscustomobject]@{
Started = $true
- Message = "Sync cycle triggered with PolicyType: $PolicyType"
+ Message = "Sync cycle triggered with PolicyType: $PolicyType on $ComputerName"
}
}
catch {
@@ -117,9 +172,11 @@ function New-IdleEntraConnectDirectorySyncProvider {
"The AuthSession must provide an elevated execution context. Original error: $errorMessage"
}
- # Re-throw other errors
throw "Failed to start sync cycle: $errorMessage"
}
+ finally {
+ $this.RemoveRemoteSession($remoteSession)
+ }
} -Force
$provider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value {
@@ -131,9 +188,11 @@ function New-IdleEntraConnectDirectorySyncProvider {
Queries the sync scheduler state via Get-ADSyncScheduler to determine if a
sync cycle is currently in progress.
+ .PARAMETER ComputerName
+ Target Entra Connect server hostname for PSRemoting.
+
.PARAMETER AuthSession
- Remote execution handle provided by the host's AuthSessionBroker.
- Must implement InvokeCommand(CommandName, Parameters).
+ Credential ([PSCredential]) provided by the host's AuthSessionBroker.
.OUTPUTS
PSCustomObject with properties:
@@ -142,20 +201,29 @@ function New-IdleEntraConnectDirectorySyncProvider {
- Details (hashtable, optional): additional state information
#>
param(
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [ValidateScript({ -not [string]::IsNullOrWhiteSpace($_) })]
+ [string] $ComputerName,
+
[Parameter(Mandatory)]
[ValidateNotNull()]
[object] $AuthSession
)
- # Validate AuthSession contract
- if ($null -eq $AuthSession.PSObject.Methods['InvokeCommand']) {
- throw "AuthSession must implement InvokeCommand(CommandName, Parameters) method. " + `
- "The host must provide an elevated remote session via AuthSessionBroker."
+ if ($AuthSession -isnot [pscredential]) {
+ $actualType = $AuthSession.GetType().FullName
+ throw "AuthSession must be a [PSCredential] for PSRemoting session creation. Received: [$actualType]"
}
+ $remoteSession = $null
try {
- # Execute Get-ADSyncScheduler remotely
- $scheduler = $AuthSession.InvokeCommand('Get-ADSyncScheduler', @{})
+ $remoteSession = $this.NewRemoteSession($ComputerName, $AuthSession)
+
+ $scheduler = $this.InvokeRemoteCommand($remoteSession, {
+ Import-Module -Name ADSync -ErrorAction Stop
+ Get-ADSyncScheduler -ErrorAction Stop
+ }, @())
# Determine if sync is in progress
# Get-ADSyncScheduler returns an object with SyncCycleInProgress property
@@ -195,6 +263,9 @@ function New-IdleEntraConnectDirectorySyncProvider {
throw "Failed to get sync cycle state: $errorMessage"
}
+ finally {
+ $this.RemoveRemoteSession($remoteSession)
+ }
} -Force
return $provider
diff --git a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1 b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1
index b1733cb1..cac31134 100644
--- a/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1
+++ b/src/IdLE.Steps.DirectorySync/Public/Invoke-IdleStepTriggerDirectorySync.ps1
@@ -6,15 +6,17 @@ function Invoke-IdleStepTriggerDirectorySync {
.DESCRIPTION
This is a provider-agnostic step. The host must supply a provider instance via
Context.Providers[] that implements:
- - StartSyncCycle(PolicyType, AuthSession)
- - GetSyncCycleState(AuthSession)
+ - StartSyncCycle(PolicyType, ComputerName, AuthSession)
+ - GetSyncCycleState(ComputerName, AuthSession)
The step is designed for remote execution and requires an elevated auth session
provided by the host's AuthSessionBroker.
Authentication:
- - With.AuthSessionName (required): routing key for AuthSessionBroker
+ - With.AuthSessionName (optional): routing key for AuthSessionBroker
- With.AuthSessionOptions (optional, hashtable): forwarded to broker for session selection
+ - If AuthSessionName is omitted, the broker is asked for a default session
+ - ComputerName and PolicyType are provider-specific inputs and are validated by the selected provider
- ScriptBlocks in AuthSessionOptions are rejected (security boundary)
.PARAMETER Context
@@ -22,8 +24,9 @@ function Invoke-IdleStepTriggerDirectorySync {
.PARAMETER Step
Normalized step object from the plan. Must contain a 'With' hashtable with keys:
- - AuthSessionName (required, string): auth session name for broker
- - PolicyType (required, string): 'Delta' or 'Initial' (case-insensitive)
+ - AuthSessionName (optional, string): auth session name for broker (default session is used when omitted)
+ - ComputerName (optional, string): provider-specific target server input
+ - PolicyType (optional, string): provider-specific policy input
- Provider (optional, string): provider alias, defaults to 'DirectorySync'
- Wait (optional, bool): wait for cycle completion, defaults to $false
- TimeoutSeconds (optional, int): wait timeout, defaults to 600
@@ -39,6 +42,7 @@ function Invoke-IdleStepTriggerDirectorySync {
Type = 'IdLE.Step.TriggerDirectorySync'
With = @{
AuthSessionName = 'DirectorySync'
+ ComputerName = 'ad-sync1.corp.local'
PolicyType = 'Delta'
Wait = $true
}
@@ -60,19 +64,8 @@ function Invoke-IdleStepTriggerDirectorySync {
throw "TriggerDirectorySync requires 'With' to be a hashtable."
}
- # Validate required inputs
- if (-not $with.ContainsKey('AuthSessionName')) {
- throw "TriggerDirectorySync requires With.AuthSessionName."
- }
-
- if (-not $with.ContainsKey('PolicyType')) {
- throw "TriggerDirectorySync requires With.PolicyType."
- }
-
- $policyType = [string]$with.PolicyType
- if ($policyType -notin @('Delta', 'Initial')) {
- throw "TriggerDirectorySync: With.PolicyType must be 'Delta' or 'Initial' (case-insensitive). Got: $policyType"
- }
+ $policyType = if ($with.ContainsKey('PolicyType')) { [string]$with.PolicyType } else { $null }
+ $computerName = if ($with.ContainsKey('ComputerName')) { [string]$with.ComputerName } else { $null }
# Optional inputs with defaults
$providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'DirectorySync' }
@@ -103,8 +96,17 @@ function Invoke-IdleStepTriggerDirectorySync {
try {
# Trigger sync cycle
- $Context.EventSink.WriteEvent('DirectorySyncTriggered', "Triggering $policyType sync cycle", $stepName, @{
+ $policyTypeText = [string]$policyType
+ $triggerMessage = if ([string]::IsNullOrWhiteSpace($policyTypeText)) {
+ 'Triggering directory sync cycle'
+ }
+ else {
+ "Triggering $policyTypeText sync cycle"
+ }
+
+ $Context.EventSink.WriteEvent('DirectorySyncTriggered', $triggerMessage, $stepName, @{
PolicyType = $policyType
+ ComputerName = $computerName
})
$startResult = Invoke-IdleProviderMethod `
@@ -112,7 +114,7 @@ function Invoke-IdleStepTriggerDirectorySync {
-With $with `
-ProviderAlias $providerAlias `
-MethodName 'StartSyncCycle' `
- -MethodArguments @($policyType)
+ -MethodArguments @($policyType, $computerName)
$changed = $false
if ($null -ne $startResult -and ($startResult.PSObject.Properties.Name -contains 'Started')) {
@@ -140,7 +142,7 @@ function Invoke-IdleStepTriggerDirectorySync {
-With $with `
-ProviderAlias $providerAlias `
-MethodName 'GetSyncCycleState' `
- -MethodArguments @()
+ -MethodArguments @($computerName)
$lastState = if ($null -ne $stateResult) { $stateResult.State } else { 'Unknown' }
@@ -159,7 +161,7 @@ function Invoke-IdleStepTriggerDirectorySync {
-With $with `
-ProviderAlias $providerAlias `
-MethodName 'GetSyncCycleState' `
- -MethodArguments @()
+ -MethodArguments @($computerName)
$inProgress = $true
if ($null -ne $stateResult -and ($stateResult.PSObject.Properties.Name -contains 'InProgress')) {
diff --git a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1 b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1
index 5a0720e0..54109194 100644
--- a/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1
+++ b/src/IdLE.Steps.DirectorySync/StepMetadataCatalog.psd1
@@ -15,8 +15,8 @@
'IdLE.Step.TriggerDirectorySync' = @{
RequiredCapabilities = @('IdLE.DirectorySync.Trigger', 'IdLE.DirectorySync.Status')
WithSchema = @{
- RequiredKeys = @('AuthSessionName', 'PolicyType')
- OptionalKeys = @('Provider', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds', 'AuthSessionOptions')
+ RequiredKeys = @()
+ OptionalKeys = @('Provider', 'AuthSessionName', 'AuthSessionOptions', 'ComputerName', 'PolicyType', 'Wait', 'TimeoutSeconds', 'PollIntervalSeconds')
}
}
}
diff --git a/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1 b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1
new file mode 100644
index 00000000..77f5076d
--- /dev/null
+++ b/tests/ProviderContracts/DirectorySyncProviderComputerNameCredential.Contract.ps1
@@ -0,0 +1,83 @@
+Set-StrictMode -Version Latest
+
+function Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [ValidateNotNull()]
+ [scriptblock] $ProviderFactory
+ )
+
+ $cases = @(
+ @{
+ ProviderFactory = $ProviderFactory
+ }
+ )
+
+ Context 'ComputerName + Credential contract' -ForEach $cases {
+ BeforeEach {
+ $providerFactory = $_.ProviderFactory
+ if ($providerFactory -is [scriptblock]) {
+ $script:Provider = & $providerFactory
+ }
+ elseif ($providerFactory -is [string]) {
+ $script:Provider = & (Get-Command -Name $providerFactory -ErrorAction Stop)
+ }
+ else {
+ throw "ProviderFactory must be a scriptblock or command name string. Got: $($providerFactory.GetType().FullName)"
+ }
+ if ($null -eq $script:Provider) {
+ throw 'ProviderFactory returned $null. A provider instance is required for contract tests.'
+ }
+
+ $script:Credential = [PSCredential]::new(
+ 'contoso\syncadmin',
+ (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force)
+ )
+
+ $script:MockSession = [pscustomobject]@{
+ Id = 1
+ }
+ $script:Provider | Add-Member -NotePropertyName LastComputerName -NotePropertyValue $null -Force
+ $script:Provider | Add-Member -NotePropertyName LastCredential -NotePropertyValue $null -Force
+ $script:Provider | Add-Member -NotePropertyName RemovedSession -NotePropertyValue $null -Force
+
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name NewRemoteSession -Value {
+ param([string] $ComputerName, [pscredential] $Credential)
+ $this.LastComputerName = $ComputerName
+ $this.LastCredential = $Credential
+ return $script:MockSession
+ } -Force
+
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value {
+ param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList)
+ return [pscustomobject]@{
+ SyncCycleInProgress = $false
+ }
+ } -Force
+
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name RemoveRemoteSession -Value {
+ param([object] $Session)
+ $this.RemovedSession = $Session
+ } -Force
+ }
+
+ It 'StartSyncCycle accepts ComputerName and Credential auth session' {
+ $result = $script:Provider.StartSyncCycle('Delta', 'ad-sync1.corp.local', $script:Credential)
+
+ $result.Started | Should -BeTrue
+ $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local'
+ $script:Provider.LastCredential | Should -Be $script:Credential
+ $script:Provider.RemovedSession | Should -Be $script:MockSession
+ }
+
+ It 'GetSyncCycleState accepts ComputerName and Credential auth session' {
+ $result = $script:Provider.GetSyncCycleState('ad-sync1.corp.local', $script:Credential)
+
+ $result.InProgress | Should -BeFalse
+ $script:Provider.LastComputerName | Should -Be 'ad-sync1.corp.local'
+ $script:Provider.LastCredential | Should -Be $script:Credential
+ $script:Provider.RemovedSession | Should -Be $script:MockSession
+ }
+ }
+}
diff --git a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1 b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1
index 42a24090..7e83912e 100644
--- a/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1
+++ b/tests/Providers/EntraConnectDirectorySyncProvider.Tests.ps1
@@ -14,46 +14,50 @@ BeforeDiscovery {
throw "Provider capabilities contract not found at: $capabilitiesContractPath"
}
. $capabilitiesContractPath
+
+ $computerNameCredentialContractPath = Join-Path -Path $repoRoot -ChildPath 'tests\ProviderContracts\DirectorySyncProviderComputerNameCredential.Contract.ps1'
+ if (-not (Test-Path -LiteralPath $computerNameCredentialContractPath -PathType Leaf)) {
+ throw "Directory sync provider ComputerName+Credential contract not found at: $computerNameCredentialContractPath"
+ }
+ . $computerNameCredentialContractPath
}
Describe 'Entra Connect directory sync provider contracts' {
Invoke-IdleProviderCapabilitiesContractTests -ProviderFactory { New-IdleEntraConnectDirectorySyncProvider }
+ Invoke-IdleDirectorySyncProviderComputerNameCredentialContractTests -ProviderFactory { New-IdleEntraConnectDirectorySyncProvider }
Context 'Directory sync provider methods' {
- BeforeAll {
+ BeforeEach {
$script:Provider = New-IdleEntraConnectDirectorySyncProvider
+ $script:ComputerName = 'ad-sync1.corp.local'
+ $script:MockCredential = [PSCredential]::new(
+ 'contoso\syncadmin',
+ (ConvertTo-SecureString -String 'P@ssw0rd!' -AsPlainText -Force)
+ )
+ $script:MockSession = [pscustomobject]@{ Id = 1 }
+ $script:Provider | Add-Member -NotePropertyName LastComputerName -NotePropertyValue $null -Force
+ $script:Provider | Add-Member -NotePropertyName LastCredential -NotePropertyValue $null -Force
+ $script:Provider | Add-Member -NotePropertyName RemovedSession -NotePropertyValue $null -Force
+
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name NewRemoteSession -Value {
+ param([string] $ComputerName, [pscredential] $Credential)
+ $this.LastComputerName = $ComputerName
+ $this.LastCredential = $Credential
+ return $script:MockSession
+ } -Force
- # Mock AuthSession with InvokeCommand method
- $script:MockAuthSession = [pscustomobject]@{
- PSTypeName = 'Mock.AuthSession'
- }
-
- $script:MockAuthSession | Add-Member -MemberType ScriptMethod -Name InvokeCommand -Value {
- param(
- [Parameter(Mandatory)]
- [string] $CommandName,
-
- [Parameter(Mandatory)]
- [hashtable] $Parameters
- )
-
- # Mock behavior for Start-ADSyncSyncCycle
- if ($CommandName -eq 'Start-ADSyncSyncCycle') {
- return [pscustomobject]@{
- Result = 'Success'
- }
- }
-
- # Mock behavior for Get-ADSyncScheduler
- if ($CommandName -eq 'Get-ADSyncScheduler') {
- return [pscustomobject]@{
- SyncCycleInProgress = $false
- AllowedSyncCycleInterval = '00:30:00'
- NextSyncCyclePolicyType = 'Delta'
- }
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value {
+ param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList)
+ return [pscustomobject]@{
+ SyncCycleInProgress = $false
+ AllowedSyncCycleInterval = '00:30:00'
+ NextSyncCyclePolicyType = 'Delta'
}
+ } -Force
- throw "Unexpected command: $CommandName"
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name RemoveRemoteSession -Value {
+ param([object] $Session)
+ $this.RemovedSession = $Session
} -Force
}
@@ -61,45 +65,83 @@ Describe 'Entra Connect directory sync provider contracts' {
$script:Provider.PSObject.Methods.Name | Should -Contain 'StartSyncCycle'
}
- It 'StartSyncCycle accepts PolicyType and AuthSession parameters' {
- $result = $script:Provider.StartSyncCycle('Delta', $script:MockAuthSession)
+ It 'StartSyncCycle accepts PolicyType, ComputerName, and AuthSession parameters' {
+ $result = $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential)
$result | Should -Not -BeNullOrEmpty
$result.PSObject.Properties.Name | Should -Contain 'Started'
$result.PSObject.Properties.Name | Should -Contain 'Message'
+ $script:Provider.LastComputerName | Should -Be $script:ComputerName
+ $script:Provider.LastCredential | Should -Be $script:MockCredential
+ $script:Provider.RemovedSession | Should -Be $script:MockSession
}
It 'StartSyncCycle validates PolicyType' {
- { $script:Provider.StartSyncCycle('Invalid', $script:MockAuthSession) } | Should -Throw
+ { $script:Provider.StartSyncCycle('Invalid', $script:ComputerName, $script:MockCredential) } | Should -Throw
+ }
+
+ It 'StartSyncCycle validates ComputerName' {
+ { $script:Provider.StartSyncCycle('Delta', '', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*'
}
- It 'StartSyncCycle validates AuthSession implements InvokeCommand' {
+ It 'StartSyncCycle validates AuthSession is PSCredential' {
$badSession = [pscustomobject]@{ Name = 'BadSession' }
- { $script:Provider.StartSyncCycle('Delta', $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*InvokeCommand*'
+ { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*'
+ }
+
+ It 'StartSyncCycle always closes remoting session' {
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value {
+ param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList)
+ throw 'remote failure'
+ } -Force
+
+ { $script:Provider.StartSyncCycle('Delta', $script:ComputerName, $script:MockCredential) } | Should -Throw
+ $script:Provider.RemovedSession | Should -Be $script:MockSession
}
It 'Exposes GetSyncCycleState method' {
$script:Provider.PSObject.Methods.Name | Should -Contain 'GetSyncCycleState'
}
- It 'GetSyncCycleState accepts AuthSession parameter' {
- $result = $script:Provider.GetSyncCycleState($script:MockAuthSession)
+ It 'GetSyncCycleState accepts ComputerName and AuthSession parameters' {
+ $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential)
$result | Should -Not -BeNullOrEmpty
$result.PSObject.Properties.Name | Should -Contain 'InProgress'
$result.PSObject.Properties.Name | Should -Contain 'State'
$result.PSObject.Properties.Name | Should -Contain 'Details'
+ $script:Provider.LastComputerName | Should -Be $script:ComputerName
+ $script:Provider.LastCredential | Should -Be $script:MockCredential
+ $script:Provider.RemovedSession | Should -Be $script:MockSession
}
It 'GetSyncCycleState returns correct InProgress value' {
- $result = $script:Provider.GetSyncCycleState($script:MockAuthSession)
+ $result = $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential)
$result.InProgress | Should -BeOfType [bool]
}
- It 'GetSyncCycleState validates AuthSession implements InvokeCommand' {
+ It 'GetSyncCycleState validates ComputerName' {
+ { $script:Provider.GetSyncCycleState('', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*'
+ }
+
+ It 'GetSyncCycleState validates ComputerName whitespace' {
+ { $script:Provider.GetSyncCycleState(' ', $script:MockCredential) } | Should -Throw -ErrorId * -ExpectedMessage '*ComputerName*'
+ }
+
+ It 'GetSyncCycleState validates AuthSession is PSCredential' {
$badSession = [pscustomobject]@{ Name = 'BadSession' }
- { $script:Provider.GetSyncCycleState($badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*InvokeCommand*'
+ { $script:Provider.GetSyncCycleState($script:ComputerName, $badSession) } | Should -Throw -ErrorId * -ExpectedMessage '*PSCredential*'
+ }
+
+ It 'GetSyncCycleState always closes remoting session' {
+ $script:Provider | Add-Member -MemberType ScriptMethod -Name InvokeRemoteCommand -Value {
+ param([object] $Session, [scriptblock] $ScriptBlock, [object[]] $ArgumentList)
+ throw 'remote failure'
+ } -Force
+
+ { $script:Provider.GetSyncCycleState($script:ComputerName, $script:MockCredential) } | Should -Throw
+ $script:Provider.RemovedSession | Should -Be $script:MockSession
}
}
diff --git a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1 b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1
index d2629c89..be8e4c70 100644
--- a/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1
+++ b/tests/Steps/Invoke-IdleStepTriggerDirectorySync.Tests.ps1
@@ -12,6 +12,8 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
PSTypeName = 'Mock.DirectorySyncProvider'
Name = 'MockDirectorySyncProvider'
PollCount = 0
+ LastPolicyType = $null
+ LastComputerName = $null
}
$script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value {
@@ -23,10 +25,16 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
[Parameter(Mandatory)]
[string] $PolicyType,
+ [Parameter(Mandatory)]
+ [string] $ComputerName,
+
[Parameter(Mandatory)]
[object] $AuthSession
)
+ $this.LastPolicyType = $PolicyType
+ $this.LastComputerName = $ComputerName
+
return [pscustomobject]@{
Started = $true
Message = "Sync cycle triggered with PolicyType: $PolicyType"
@@ -35,10 +43,15 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
$script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value {
param(
+ [Parameter(Mandatory)]
+ [string] $ComputerName,
+
[Parameter(Mandatory)]
[object] $AuthSession
)
+ $this.LastComputerName = $ComputerName
+
# Increment poll count and determine state
$this.PollCount++
@@ -87,10 +100,18 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
Type = 'IdLE.Step.TriggerDirectorySync'
With = @{
AuthSessionName = 'EntraConnect'
+ ComputerName = 'ad-sync1.corp.local'
PolicyType = 'Delta'
Provider = 'DirectorySync'
}
}
+
+ $script:SetPermissiveStartSyncCycle = {
+ $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value {
+ param([AllowNull()][object] $PolicyType, [AllowNull()][object] $ComputerName, [object] $AuthSession)
+ return [pscustomobject]@{ Started = $true }
+ } -Force
+ }
}
Context 'Input validation' {
@@ -104,33 +125,72 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
{ & $handler -Context $script:Context -Step $step } | Should -Throw
}
- It 'throws when With.AuthSessionName is missing' {
+ It 'does not enforce With.AuthSessionName at step level' {
$step = $script:StepTemplate
$step.With.Remove('AuthSessionName')
$handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
- { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*AuthSessionName*'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
}
- It 'throws when With.PolicyType is missing' {
+ It 'does not enforce With.PolicyType at step level' {
$step = $script:StepTemplate
$step.With.Remove('PolicyType')
+ & $script:SetPermissiveStartSyncCycle
$handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
- { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
}
- It 'throws when With.PolicyType is invalid' {
+ It 'passes With.PolicyType = Initial through to the provider path' {
+ $step = $script:StepTemplate
+ $step.With.PolicyType = 'Initial'
+
+ $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
+ }
+
+ It 'throws error when provider rejects invalid With.PolicyType value' {
$step = $script:StepTemplate
$step.With.PolicyType = 'Invalid'
+ $script:MockProvider | Add-Member -MemberType ScriptMethod -Name StartSyncCycle -Value {
+ param(
+ [Parameter(Mandatory)]
+ [ValidateSet('Delta', 'Initial')]
+ [string] $PolicyType,
+ [Parameter(Mandatory)]
+ [string] $ComputerName,
+ [Parameter(Mandatory)]
+ [object] $AuthSession
+ )
+ return [pscustomobject]@{ Started = $true }
+ } -Force
$handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
- { & $handler -Context $script:Context -Step $step } | Should -Throw -ErrorId * -ExpectedMessage '*PolicyType*'
+ $thrown = $null
+ try {
+ $null = & $handler -Context $script:Context -Step $step
+ }
+ catch {
+ $thrown = $_
+ }
+
+ $thrown | Should -Not -BeNullOrEmpty
+ $thrown.Exception.Message | Should -Match 'Exception calling "StartSyncCycle"'
+ $thrown.Exception.Message | Should -Match 'Invalid'
+ $thrown.Exception.Message | Should -Not -Match 'TriggerDirectorySync: With.PolicyType'
}
- It 'accepts Delta as PolicyType' {
+ It 'does not enforce With.ComputerName at step level' {
$step = $script:StepTemplate
- $step.With.PolicyType = 'Delta'
+ $step.With.Remove('ComputerName')
+ & $script:SetPermissiveStartSyncCycle
$handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
$result = & $handler -Context $script:Context -Step $step
@@ -138,14 +198,28 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
$result.Status | Should -Be 'Completed'
}
- It 'accepts Initial as PolicyType' {
+ It 'does not enforce With.ComputerName whitespace validation at step level' {
$step = $script:StepTemplate
- $step.With.PolicyType = 'Initial'
+ $step.With.ComputerName = ' '
+ & $script:SetPermissiveStartSyncCycle
+
+ $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
+ }
+
+ It 'coerces non-string PolicyType and ComputerName to strings before provider invocation' {
+ $step = $script:StepTemplate
+ $step.With.PolicyType = 123
+ $step.With.ComputerName = 456
$handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
$result = & $handler -Context $script:Context -Step $step
$result.Status | Should -Be 'Completed'
+ $script:MockProvider.LastPolicyType | Should -Be '123'
+ $script:MockProvider.LastComputerName | Should -Be '456'
}
It 'uses default provider alias when not specified' {
@@ -186,6 +260,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
$result.Status | Should -Be 'Completed'
$result.Changed | Should -BeTrue
$result.Error | Should -BeNullOrEmpty
+ $script:MockProvider.LastComputerName | Should -Be 'ad-sync1.corp.local'
}
It 'defaults to not waiting when Wait is not specified' {
@@ -214,7 +289,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
It 'throws timeout error when sync does not complete in time' {
# Mock provider that never completes
$script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value {
- param([object] $AuthSession)
+ param([string] $ComputerName, [object] $AuthSession)
return [pscustomobject]@{
InProgress = $true
State = 'InProgress'
@@ -235,7 +310,7 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
# Use the provider's PollCount property which is already initialized
$script:MockProvider.PollCount = 0
$script:MockProvider | Add-Member -MemberType ScriptMethod -Name GetSyncCycleState -Value {
- param([object] $AuthSession)
+ param([string] $ComputerName, [object] $AuthSession)
$this.PollCount++
$inProgress = $this.PollCount -le 2
return [pscustomobject]@{
@@ -288,6 +363,45 @@ Describe 'Invoke-IdleStepTriggerDirectorySync (DirectorySync step)' {
$capturedEvents.Type | Should -Contain 'DirectorySyncTriggered'
}
+ It 'emits readable trigger message when With.PolicyType is omitted' {
+ $capturedEvents = [System.Collections.ArrayList]::new()
+ $script:Context.EventSink = [pscustomobject]@{}
+ $script:Context.EventSink | Add-Member -MemberType ScriptMethod -Name WriteEvent -Value {
+ param($Type, $Message, $StepName, $Data)
+ $null = $capturedEvents.Add(@{ Type = $Type; Message = $Message; StepName = $StepName; Data = $Data })
+ } -Force
+
+ $step = $script:StepTemplate
+ $step.With.Remove('PolicyType')
+ & $script:SetPermissiveStartSyncCycle
+
+ $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
+ $null = & $handler -Context $script:Context -Step $step
+
+ $triggerEvent = $capturedEvents | Where-Object { $_.Type -eq 'DirectorySyncTriggered' } | Select-Object -First 1
+ $triggerEvent.Message | Should -Be 'Triggering directory sync cycle'
+ }
+
+ It 'emits string values in DirectorySyncTriggered event data for PolicyType and ComputerName' {
+ $capturedEvents = [System.Collections.ArrayList]::new()
+ $script:Context.EventSink = [pscustomobject]@{}
+ $script:Context.EventSink | Add-Member -MemberType ScriptMethod -Name WriteEvent -Value {
+ param($Type, $Message, $StepName, $Data)
+ $null = $capturedEvents.Add(@{ Type = $Type; Message = $Message; StepName = $StepName; Data = $Data })
+ } -Force
+
+ $step = $script:StepTemplate
+ $step.With.PolicyType = 123
+ $step.With.ComputerName = 456
+
+ $handler = 'IdLE.Steps.DirectorySync\Invoke-IdleStepTriggerDirectorySync'
+ $null = & $handler -Context $script:Context -Step $step
+
+ $triggerEvent = $capturedEvents | Where-Object { $_.Type -eq 'DirectorySyncTriggered' } | Select-Object -First 1
+ $triggerEvent.Data.PolicyType | Should -Be '123'
+ $triggerEvent.Data.ComputerName | Should -Be '456'
+ }
+
It 'emits DirectorySyncCompleted event' {
$capturedEvents = [System.Collections.ArrayList]::new()
$script:Context.EventSink = [pscustomobject]@{}
diff --git a/tests/fixtures/workflows/joiner-with-dirsync.psd1 b/tests/fixtures/workflows/joiner-with-dirsync.psd1
index cf20dbba..404092b5 100644
--- a/tests/fixtures/workflows/joiner-with-dirsync.psd1
+++ b/tests/fixtures/workflows/joiner-with-dirsync.psd1
@@ -9,6 +9,7 @@
Type = 'IdLE.Step.TriggerDirectorySync'
With = @{
AuthSessionName = 'DirSync'
+ ComputerName = 'ad-sync1.corp.local'
PolicyType = 'Delta'
Wait = $false
Provider = 'DirectorySync'
diff --git a/tools/Generate-IdleStepReference.ps1 b/tools/Generate-IdleStepReference.ps1
index 4f289260..3a1b1b56 100644
--- a/tools/Generate-IdleStepReference.ps1
+++ b/tools/Generate-IdleStepReference.ps1
@@ -608,8 +608,8 @@ function New-IdleStepDetailPageContent {
[void]$sb.AppendLine()
if ($Model.RequiredWithKeys.Count -eq 0) {
- [void]$sb.AppendLine('The required input keys could not be detected automatically.')
- [void]$sb.AppendLine('Please refer to the step description and examples for usage details.')
+ [void]$sb.AppendLine('This step has no required ``With.*`` keys at step schema level.')
+ [void]$sb.AppendLine('Inputs may still be provider-specific; refer to the step description and examples for usage details.')
[void]$sb.AppendLine()
}
else {