#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# US9: condition tracing is available only when explicitly enabled. The default matters more than the feature. Tracing widens what an audit record contains beyond the UPN and Object ID that are approved by default (Principle V), so a trace that appeared without being asked for would quietly change what the log holds - and nobody reviews a log for fields they did not know were there. #> BeforeAll { $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent . (Join-Path $repoRoot 'tests/TestHelpers.ps1') foreach ($file in (Get-PersonaSourceFile -RepoRoot $repoRoot)) { . $file } $script:target = 'extension__' $script:rules = @( [pscustomobject]@{ id = 'RULE-0010-GUEST'; name = 'Guests'; enabled = $true; priority = 10; persona = 'Guest' match = [pscustomobject]@{ operator = 'all'; conditions = @([pscustomobject]@{ type = 'property'; property = 'UserType'; operator = 'equals'; value = 'Guest' }) } } [pscustomobject]@{ id = 'RULE-0900-EMPLOYEE'; name = 'Employees'; enabled = $true; priority = 900; persona = 'Employee' match = [pscustomobject]@{ operator = 'all'; conditions = @([pscustomobject]@{ type = 'property'; property = 'Department'; operator = 'isNotNull' }) } } ) $script:record = New-PersonaUserRecord -AccountObjectId '00000000-0000-0000-0000-000000000101' ` -UserPrincipalName 'alex@example.invalid' -UserType 'Member' ` -Properties @{ Department = 'Finance' } -StoredPersona 'Employee' ` -Membership (New-PersonaMembershipRecord -AllRetrieved) } Describe 'Trace gating on the decision result' { It 'leaves ConditionTrace null by default' { $result = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules $result.ConditionTrace | Should -BeNullOrEmpty } It 'populates ConditionTrace when tracing is requested' { $result = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules -IncludeTrace $result.ConditionTrace | Should -Not -BeNullOrEmpty $result.ConditionTrace.Count | Should -Be $result.RulesEvaluated } It 'records the per-rule result and priority in the trace' { $result = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules -IncludeTrace $result.ConditionTrace[0].RuleId | Should -Be 'RULE-0010-GUEST' $result.ConditionTrace[0].Priority | Should -Be 10 $result.ConditionTrace[0].Result | Should -Be 'False' $result.ConditionTrace[1].RuleId | Should -Be 'RULE-0900-EMPLOYEE' $result.ConditionTrace[1].Result | Should -Be 'True' } It 'does not change the decision when tracing is on' { # Tracing is diagnostic. If it could alter an outcome, a debug run would stop # being evidence about the real one. $plain = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules $traced = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules -IncludeTrace $traced.Outcome | Should -Be $plain.Outcome $traced.MatchedRuleId | Should -Be $plain.MatchedRuleId $traced.CalculatedPersona | Should -Be $plain.CalculatedPersona $traced.RulesEvaluated | Should -Be $plain.RulesEvaluated } } Describe 'Trace gating on the audit record' { It 'omits conditionTrace when tracing is off' { $result = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules -IncludeTrace $result.Action = 'Unchanged' $record = New-PersonaAuditRecord -Context (New-TestAuditContext) -RecordType 'UserEvent' -Result $result # The result carries a trace, but the record was not asked to include it. # Both gates have to be open. $record.Contains('conditionTrace') | Should -BeFalse } It 'includes conditionTrace when both the result and the record are traced' { $result = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules -IncludeTrace $result.Action = 'Unchanged' $record = New-PersonaAuditRecord -Context (New-TestAuditContext) -RecordType 'UserEvent' -Result $result -IncludeTrace $record.Contains('conditionTrace') | Should -BeTrue $record['conditionTrace'].Count | Should -Be 2 $record['conditionTrace'][0]['ruleId'] | Should -Be 'RULE-0010-GUEST' } It 'omits conditionTrace when tracing was requested but the result carries none' { $result = Resolve-UserPersona -UserRecord $script:record -Rules $script:rules $result.Action = 'Unchanged' $record = New-PersonaAuditRecord -Context (New-TestAuditContext) -RecordType 'UserEvent' -Result $result -IncludeTrace $record.Contains('conditionTrace') | Should -BeFalse } } Describe 'Trace gating across a run' { BeforeEach { Mock Write-Host { } Mock Get-PersonaUsers { @(New-TestPopulation -Count 6 -TargetAttribute $script:target) } } It 'emits no condition values anywhere when tracing is off' { $info = $null $null = Invoke-PersonaEngineRun -Configuration (New-TestRuntimeConfiguration -TargetAttribute $script:target) ` -TargetAttribute $script:target -Context (New-TestAuditContext) ` -AuditParameters @{ Destination = 'stream' } -InformationVariable info foreach ($event in (Get-CapturedAuditRecord -Captured $info -RecordType 'UserEvent')) { $event.Contains('conditionTrace') | Should -BeFalse } } It 'emits a trace on every user event when tracing is on' { $info = $null $null = Invoke-PersonaEngineRun -Configuration (New-TestRuntimeConfiguration -TargetAttribute $script:target) ` -TargetAttribute $script:target -Context (New-TestAuditContext) ` -AuditParameters @{ Destination = 'stream' } -Tracing -InformationVariable info $events = Get-CapturedAuditRecord -Captured $info -RecordType 'UserEvent' $events.Count | Should -BeGreaterThan 0 foreach ($event in $events) { $event.Contains('conditionTrace') | Should -BeTrue $event['conditionTrace'].Count | Should -BeGreaterThan 0 } } }