Tests/BlastRadiusSvg.Tests.ps1
|
BeforeAll { $moduleRoot = Split-Path -Parent $PSScriptRoot # Import the .psm1 directly (not the manifest) so the tests run fully # offline, without Microsoft.Graph.Authentication installed. Import-Module (Join-Path $moduleRoot 'EntraGroupInsights.psm1') -Force $script:blastRadius = [pscustomobject]@{ GroupId = '11111111-2222-3333-4444-555555555555' DisplayName = 'Sales-DE-Dynamic' IsRoleAssignable = $false IsTeamsGroup = $false MembershipRule = '(user.department -eq "Sales") -and (user.country -eq "DE")' ParentGroups = @() ChildGroups = @() RuleReferencedGroups = @() RuleReferencedByGroups = @() ConditionalAccessPolicies = @() AssignedLicenses = @() AppRoleAssignments = @() PimEligibleRoleCount = 0 TotalDependencyCount = 0 RiskLevel = 'Low' } } Describe 'Export-EGIGroupBlastRadiusSvg' { It 'draws the membership rule as its own box' { $path = Join-Path $TestDrive 'rule-only.svg' $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Membership rule' $svg | Should -Match 'user\.department' } It 'omits the rule box when the group has no membership rule' { $path = Join-Path $TestDrive 'no-rule.svg' $noRule = $script:blastRadius | Select-Object * -ExcludeProperty MembershipRule $noRule | Add-Member -NotePropertyName MembershipRule -NotePropertyValue $null $noRule | Export-EGIGroupBlastRadiusSvg -Path $path $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Not -Match 'Membership rule' } It 'draws a matching example user as a green "Matches rule" node' { $path = Join-Path $TestDrive 'match.svg' $matchingUser = [pscustomobject]@{ DisplayName = 'Alice Nguyen'; department = 'Sales'; country = 'DE' } $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUser $matchingUser $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Alice Nguyen' $svg | Should -Match 'Matches rule' $svg | Should -Match '#16a34a' } It 'draws a non-matching example user as a red "Does not match rule" node' { $path = Join-Path $TestDrive 'no-match.svg' $nonMatchingUser = [pscustomobject]@{ DisplayName = 'Bob Fischer'; department = 'Engineering'; country = 'DE' } $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUser $nonMatchingUser $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Bob Fischer' $svg | Should -Match 'Does not match rule' $svg | Should -Match '#dc2626' } It 'produces well-formed XML' { $path = Join-Path $TestDrive 'wellformed.svg' $matchingUser = [pscustomobject]@{ DisplayName = 'Alice Nguyen'; department = 'Sales'; country = 'DE' } $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUser $matchingUser { [xml](Get-Content -LiteralPath $path -Raw) } | Should -Not -Throw } It 'rejects -ExampleUser and -ExampleUserId together' { $path = Join-Path $TestDrive 'both.svg' { $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUser @{ DisplayName = 'X' } -ExampleUserId 'x@contoso.com' } | Should -Throw '*either*' } Context '-ExampleUserId (looks a real user up via Microsoft Graph)' { BeforeAll { Mock -ModuleName EntraGroupInsights Invoke-MgGraphRequest { param($Method, $Uri) $script:lastGraphUri = $Uri if ($Uri -match 'alice') { return [pscustomobject]@{ id = 'aaaa'; displayName = 'Alice Nguyen'; department = 'Sales'; country = 'DE' } } return [pscustomobject]@{ id = 'bbbb'; displayName = 'Bob Fischer'; department = 'Engineering'; country = 'DE' } } } It 'looks the user up by UPN and draws a matching-user node' { $path = Join-Path $TestDrive 'upn-match.svg' $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUserId 'alice.nguyen@contoso.com' $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Alice Nguyen' $svg | Should -Match 'Matches rule' Should -Invoke -ModuleName EntraGroupInsights Invoke-MgGraphRequest -Times 1 } It 'only requests properties referenced by the membership rule (plus id/displayName)' { $path = Join-Path $TestDrive 'select.svg' $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUserId 'alice.nguyen@contoso.com' $script:lastGraphUri | Should -Match '\$select=id,displayName,department,country' } It 'draws a non-matching user in red' { $path = Join-Path $TestDrive 'id-no-match.svg' $script:blastRadius | Export-EGIGroupBlastRadiusSvg -Path $path -ExampleUserId '99999999-9999-9999-9999-999999999999' $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Bob Fischer' $svg | Should -Match 'Does not match rule' } } Context 'nested group columns' { BeforeAll { $script:nestedBlastRadius = [pscustomobject]@{ GroupId = '11111111-2222-3333-4444-555555555555' DisplayName = 'Sales-DE-Dynamic' IsRoleAssignable = $false IsTeamsGroup = $false MembershipRule = '(user.department -eq "Sales") -and (user.country -eq "DE")' ParentGroups = @([pscustomobject]@{ id = 'p1'; displayName = 'EU-AllStaff' }) ChildGroups = @([pscustomobject]@{ id = 'c1'; displayName = 'Sales-DE-VIP' }) ConditionalAccessPolicies = @( [pscustomobject]@{ DisplayName = 'Require MFA EU'; State = 'enabled'; Reference = 'Include'; Source = "Nested via 'EU-AllStaff'" } ) AssignedLicenses = @( [pscustomobject]@{ SkuId = 'sku-eu-123'; Source = "Nested via 'EU-AllStaff'" } ) AppRoleAssignments = @( [pscustomobject]@{ resourceDisplayName = 'Salesforce'; appRoleId = 'role-1'; Source = "Nested via 'EU-AllStaff'" } ) PimEligibleRoleCount = 0 TotalDependencyCount = 3 RiskLevel = 'Critical' } } It 'draws parent and child group columns' { $path = Join-Path $TestDrive 'nested.svg' $script:nestedBlastRadius | Export-EGIGroupBlastRadiusSvg -Path $path $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Nested in \(parent groups\)' $svg | Should -Match 'EU-AllStaff' $svg | Should -Match 'Contains \(nested groups\)' $svg | Should -Match 'Sales-DE-VIP' } It 'labels inherited Conditional Access, license, and app role entries with their source group' { $path = Join-Path $TestDrive 'nested-sources.svg' $script:nestedBlastRadius | Export-EGIGroupBlastRadiusSvg -Path $path $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match "via 'EU-AllStaff'" } It 'produces well-formed XML with the extra columns' { $path = Join-Path $TestDrive 'nested-wellformed.svg' $script:nestedBlastRadius | Export-EGIGroupBlastRadiusSvg -Path $path { [xml](Get-Content -LiteralPath $path -Raw) } | Should -Not -Throw } } Context 'memberOf rule-reference column' { It 'draws groups referenced by a memberOf rule clause' { $ruleReferenced = $script:blastRadius | Select-Object * -ExcludeProperty RuleReferencedGroups $ruleReferenced | Add-Member -NotePropertyName RuleReferencedGroups -NotePropertyValue @( [pscustomobject]@{ id = 'd1'; displayName = 'VendorX-Contractors' } ) $path = Join-Path $TestDrive 'memberof.svg' $ruleReferenced | Export-EGIGroupBlastRadiusSvg -Path $path $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Rule references \(memberOf\)' $svg | Should -Match 'VendorX-Contractors' } } Context 'memberOf reverse rule-reference column' { It 'draws other dynamic groups whose rule references this group' { $referencedBy = $script:blastRadius | Select-Object * -ExcludeProperty RuleReferencedByGroups $referencedBy | Add-Member -NotePropertyName RuleReferencedByGroups -NotePropertyValue @( [pscustomobject]@{ id = 'a1'; displayName = 'Contractors-AllOf-VendorX' } ) $path = Join-Path $TestDrive 'memberof-reverse.svg' $referencedBy | Export-EGIGroupBlastRadiusSvg -Path $path $svg = Get-Content -LiteralPath $path -Raw $svg | Should -Match 'Referenced by \(memberOf\)' $svg | Should -Match 'Contractors-AllOf-VendorX' } } } |