#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } <# New-PersonaWriteBody throws for any attribute other than the configured target, and for any target absent from the approved list. Throwing rather than correcting is the design. A caller that asked to write the wrong attribute has a defect; silently substituting the right one hides it until the day the substitution is also wrong. #> 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:approved = @($script:target) } Describe 'Rejection of a non-target attribute' -Tag 'Safety' { It 'refuses <_>' -ForEach @('department', 'jobTitle', 'userPrincipalName', 'accountEnabled', 'onPremisesImmutableId') { { New-PersonaWriteBody -AttributeName $_ -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved } | Should -Throw -ExpectedMessage '*only the configured target attribute*' } It 'refuses a different extension property' { { New-PersonaWriteBody -AttributeName 'extension__' -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved } | Should -Throw } It 'refuses a casing variant of the target' { # Extension property names are case-sensitive in Graph, so this is a different # attribute, not the same one spelled differently. Accepting it would write to # a property nobody approved. { New-PersonaWriteBody -AttributeName $script:target.ToUpperInvariant() -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved } | Should -Throw } } Describe 'Rejection of an unapproved target' -Tag 'Safety' { It 'refuses a target absent from the approved list' { { New-PersonaWriteBody -AttributeName $script:target -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes @('extension__') } | Should -Throw -ExpectedMessage '*not present in approvedWritableAttributes*' } It 'refuses when the approved list is empty' { { New-PersonaWriteBody -AttributeName $script:target -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes @() } | Should -Throw } It 'refuses when the approved list differs only in casing' { { New-PersonaWriteBody -AttributeName $script:target -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes @($script:target.ToUpperInvariant()) } | Should -Throw } It 'refuses a blank attribute name' { { New-PersonaWriteBody -AttributeName '' -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved } | Should -Throw } } Describe 'Resolve-TargetAttribute enforces the same rule earlier' -Tag 'Safety' { It 'returns the target when it is approved' { $config = [pscustomobject]@{ TargetAttribute = $script:target; ApprovedWritableAttributes = $script:approved } Resolve-TargetAttribute -Configuration $config | Should -BeExactly $script:target } It 'throws rather than returning null for a blank target' { # A null return would be indistinguishable from a caller forgetting to check, # and that caller writes to whatever name it was holding. $config = [pscustomobject]@{ TargetAttribute = ' '; ApprovedWritableAttributes = $script:approved } { Resolve-TargetAttribute -Configuration $config } | Should -Throw -ExpectedMessage '*blank*' } It 'throws for an unapproved target' { $config = [pscustomobject]@{ TargetAttribute = 'department'; ApprovedWritableAttributes = $script:approved } { Resolve-TargetAttribute -Configuration $config } | Should -Throw } } Describe 'Set-UserPersonaAttribute refuses an unconfirmed call' -Tag 'Safety' { It 'throws when -Confirmed is absent' { # Reaching the write adapter without the gate is a control-flow defect. The # only safe response is to refuse, not to infer intent. Mock Invoke-PersonaGraphRequest { @{} } { Set-UserPersonaAttribute -UserObjectId '00000000-0000-0000-0000-000000000101' ` -AttributeName $script:target -Value 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved } | Should -Throw -ExpectedMessage '*without a confirmed ShouldProcess gate*' Should -Invoke Invoke-PersonaGraphRequest -Times 0 -Exactly } It 'returns a failed result rather than throwing when the PATCH fails' { # A failed write is a per-user outcome. Throwing would abandon the rest of the # population over one account. Mock Invoke-PersonaGraphRequest { throw 'Response status code does not indicate success: 403 (Forbidden).' } $result = Set-UserPersonaAttribute -UserObjectId '00000000-0000-0000-0000-000000000101' ` -AttributeName $script:target -Value 'Employee' -PreviousValue 'Guest' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved -Confirmed $result.Succeeded | Should -BeFalse $result.FailureReason | Should -Not -BeNullOrEmpty $result.PreviousValue | Should -Be 'Guest' } It 'captures previousValue from the value observed before the write' { Mock Invoke-PersonaGraphRequest { @{} } $result = Set-UserPersonaAttribute -UserObjectId '00000000-0000-0000-0000-000000000101' ` -AttributeName $script:target -Value 'Tier0-Admin' -PreviousValue 'Employee' ` -TargetAttribute $script:target -ApprovedWritableAttributes $script:approved -Confirmed $result.Succeeded | Should -BeTrue $result.PreviousValue | Should -Be 'Employee' $result.Value | Should -Be 'Tier0-Admin' } }