Tests/EntraID-SecurityAudit.Tests.ps1

BeforeAll {
    $modulePath = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
    $moduleName = 'EntraID-SecurityAudit'
    $manifestPath = Join-Path $modulePath "$moduleName\$moduleName.psd1"

    if (Get-Module $moduleName) { Remove-Module $moduleName -Force }
    Import-Module $manifestPath -Force
}

Describe 'Module: EntraID-SecurityAudit' {

    Context 'Module Loading' {
        It 'Imports without errors' {
            { Import-Module $manifestPath -Force } | Should -Not -Throw
        }

        It 'Exports exactly 5 public functions' {
            $exported = (Get-Module $moduleName).ExportedFunctions.Keys
            $exported.Count | Should -Be 5
        }

        It 'Exports Invoke-EntraSecurityAudit' {
            Get-Command -Module $moduleName -Name 'Invoke-EntraSecurityAudit' | Should -Not -BeNullOrEmpty
        }

        It 'Exports Get-EntraUserRiskReport' {
            Get-Command -Module $moduleName -Name 'Get-EntraUserRiskReport' | Should -Not -BeNullOrEmpty
        }

        It 'Exports Get-EntraAppPermissionAudit' {
            Get-Command -Module $moduleName -Name 'Get-EntraAppPermissionAudit' | Should -Not -BeNullOrEmpty
        }

        It 'Exports Get-EntraSignInAnalysis' {
            Get-Command -Module $moduleName -Name 'Get-EntraSignInAnalysis' | Should -Not -BeNullOrEmpty
        }

        It 'Exports Get-EntraPrivilegedRoleReview' {
            Get-Command -Module $moduleName -Name 'Get-EntraPrivilegedRoleReview' | Should -Not -BeNullOrEmpty
        }

        It 'Does not export private functions' {
            $exported = (Get-Module $moduleName).ExportedFunctions.Keys
            $exported | Should -Not -Contain 'Test-GraphConnection'
            $exported | Should -Not -Contain 'New-HtmlDashboard'
        }
    }

    Context 'Parameter Validation' {
        It 'Invoke-EntraSecurityAudit has OutputPath parameter' {
            (Get-Command Invoke-EntraSecurityAudit).Parameters.Keys | Should -Contain 'OutputPath'
        }

        It 'Invoke-EntraSecurityAudit has DaysBack with ValidateRange 1-90' {
            $param = (Get-Command Invoke-EntraSecurityAudit).Parameters['DaysBack']
            $param | Should -Not -BeNullOrEmpty
            $rangeAttr = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $rangeAttr.MinRange | Should -Be 1
            $rangeAttr.MaxRange | Should -Be 90
        }

        It 'Invoke-EntraSecurityAudit has SkipSignIns switch' {
            $param = (Get-Command Invoke-EntraSecurityAudit).Parameters['SkipSignIns']
            $param.SwitchParameter | Should -Be $true
        }

        It 'Get-EntraUserRiskReport has RiskLevelFilter with ValidateSet' {
            $param = (Get-Command Get-EntraUserRiskReport).Parameters['RiskLevelFilter']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet.ValidValues | Should -Contain 'High'
            $validateSet.ValidValues | Should -Contain 'All'
        }

        It 'Get-EntraUserRiskReport has IncludeGuests switch' {
            $param = (Get-Command Get-EntraUserRiskReport).Parameters['IncludeGuests']
            $param.SwitchParameter | Should -Be $true
        }

        It 'Get-EntraAppPermissionAudit has ExpirationWarningDays parameter' {
            $param = (Get-Command Get-EntraAppPermissionAudit).Parameters['ExpirationWarningDays']
            $param | Should -Not -BeNullOrEmpty
        }

        It 'Get-EntraSignInAnalysis has FailureThreshold parameter' {
            $param = (Get-Command Get-EntraSignInAnalysis).Parameters['FailureThreshold']
            $param | Should -Not -BeNullOrEmpty
        }

        It 'Get-EntraPrivilegedRoleReview has MaxGlobalAdmins with ValidateRange' {
            $param = (Get-Command Get-EntraPrivilegedRoleReview).Parameters['MaxGlobalAdmins']
            $rangeAttr = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $rangeAttr.MinRange | Should -Be 1
            $rangeAttr.MaxRange | Should -Be 20
        }
    }

    Context 'Mock-Based Execution' {
        It 'Get-EntraUserRiskReport processes users and flags missing MFA' {
            Mock -ModuleName $moduleName Get-MgContext { @{ Scopes = @('User.Read.All','Directory.Read.All','Application.Read.All'); Account = 'test@contoso.com'; TenantId = 'test-tenant' } }
            Mock -ModuleName $moduleName Get-MgUser {
                @(
                    [PSCustomObject]@{
                        Id = '001'; DisplayName = 'Alice Admin'; UserPrincipalName = 'alice@contoso.com'
                        UserType = 'Member'; AccountEnabled = $true
                        RiskLevel = 'high'; RiskState = 'atRisk'
                        SignInActivity = @{ LastSignInDateTime = (Get-Date).AddDays(-5).ToString('o') }
                    },
                    [PSCustomObject]@{
                        Id = '002'; DisplayName = 'Bob User'; UserPrincipalName = 'bob@contoso.com'
                        UserType = 'Member'; AccountEnabled = $true
                        RiskLevel = $null; RiskState = $null
                        SignInActivity = @{ LastSignInDateTime = (Get-Date).AddDays(-120).ToString('o') }
                    }
                )
            }
            Mock -ModuleName $moduleName Get-MgUserAuthenticationMethod {
                @([PSCustomObject]@{ AdditionalProperties = @{ '@odata.type' = '#microsoft.graph.passwordAuthenticationMethod' } })
            }

            $result = Get-EntraUserRiskReport
            $result.Count | Should -Be 2
            ($result | Where-Object { $_.Finding -match 'HIGH RISK' }).Count | Should -BeGreaterThan 0
            ($result | Where-Object { $_.Finding -match 'NO MFA' }).Count | Should -Be 2
        }

        It 'Get-EntraAppPermissionAudit processes apps and flags multi-tenant' {
            Mock -ModuleName $moduleName Get-MgContext { @{ Scopes = @('Application.Read.All'); Account = 'test@contoso.com'; TenantId = 'test-tenant' } }
            Mock -ModuleName $moduleName Get-MgApplication {
                @(
                    [PSCustomObject]@{
                        Id = 'app-001'; DisplayName = 'Internal Tool'; AppId = 'guid-1'
                        SignInAudience = 'AzureADMyOrg'; CreatedDateTime = (Get-Date).AddMonths(-6)
                        RequiredResourceAccess = @(); PasswordCredentials = @(); KeyCredentials = @()
                    },
                    [PSCustomObject]@{
                        Id = 'app-002'; DisplayName = 'External App'; AppId = 'guid-2'
                        SignInAudience = 'AzureADMultipleOrgs'; CreatedDateTime = (Get-Date).AddMonths(-12)
                        RequiredResourceAccess = @(); PasswordCredentials = @(); KeyCredentials = @()
                    }
                )
            }
            Mock -ModuleName $moduleName Get-MgApplicationOwner { @() }

            $result = Get-EntraAppPermissionAudit
            $result.Count | Should -Be 2
            ($result | Where-Object { $_.Finding -match 'MULTI-TENANT' }).Count | Should -Be 1
            ($result | Where-Object { $_.Finding -match 'NO OWNER' }).Count | Should -Be 2
        }

        It 'Get-EntraPrivilegedRoleReview processes role assignments' {
            Mock -ModuleName $moduleName Get-MgContext { @{ Scopes = @('Directory.Read.All','RoleManagement.Read.Directory'); Account = 'test@contoso.com'; TenantId = 'test-tenant' } }
            Mock -ModuleName $moduleName Get-MgDirectoryRole {
                @(
                    [PSCustomObject]@{ Id = 'role-001'; DisplayName = 'Global Administrator' },
                    [PSCustomObject]@{ Id = 'role-002'; DisplayName = 'User Administrator' }
                )
            }
            Mock -ModuleName $moduleName Get-MgDirectoryRoleMember {
                param($DirectoryRoleId)
                @([PSCustomObject]@{
                    Id = 'user-001'
                    AdditionalProperties = @{
                        displayName = 'Alice Admin'
                        userPrincipalName = 'alice@contoso.com'
                        '@odata.type' = '#microsoft.graph.user'
                    }
                })
            }
            Mock -ModuleName $moduleName Get-MgRoleManagementDirectoryRoleEligibilitySchedule { throw 'Not available' }

            $result = Get-EntraPrivilegedRoleReview
            $result.Count | Should -BeGreaterThan 0
            ($result | Where-Object { $_.RoleName -eq 'Global Administrator' }).Count | Should -Be 1
        }
    }

    Context 'HTML Report Generation' {
        It 'New-HtmlDashboard creates valid HTML file' {
            $testSections = @(
                @{
                    Title = 'Test Section'
                    Data = @([PSCustomObject]@{ Name = 'Test'; Finding = 'OK' })
                    Summary = '1 item tested'
                }
            )
            $testFile = Join-Path $TestDrive 'test-report.html'

            & (Get-Module $moduleName) { New-HtmlDashboard -Sections $args[0] -OutputFile $args[1] -ReportTitle 'Test Report' } $testSections $testFile

            Test-Path $testFile | Should -Be $true
            $content = Get-Content $testFile -Raw
            $content | Should -Match '<title>Test Report</title>'
            $content | Should -Match 'EntraID-SecurityAudit'
            $content | Should -Match 'Test Section'
        }
    }

    Context 'Module Manifest' {
        It 'Has a valid manifest' {
            { Test-ModuleManifest -Path $manifestPath } | Should -Not -Throw
        }

        It 'Has the correct version' {
            (Test-ModuleManifest -Path $manifestPath).Version | Should -Be '1.0.0'
        }

        It 'Has the correct author' {
            (Test-ModuleManifest -Path $manifestPath).Author | Should -Be 'Larry Roberts'
        }

        It 'Has ProjectUri set' {
            (Test-ModuleManifest -Path $manifestPath).ProjectUri | Should -Not -BeNullOrEmpty
        }

        It 'Has LicenseUri set' {
            (Test-ModuleManifest -Path $manifestPath).LicenseUri | Should -Not -BeNullOrEmpty
        }

        It 'Has relevant tags' {
            $tags = (Test-ModuleManifest -Path $manifestPath).Tags
            $tags | Should -Contain 'EntraID'
            $tags | Should -Contain 'Security'
        }
    }
}