Providers/Okta/Public/Test-OktaEnvironment.ps1

function Test-OktaEnvironment {
    <#
    .SYNOPSIS
        Verifies that the seeded Okta org matches the seed data
    .DESCRIPTION
        Reads the users, groups and applications the module owns in the connected org, the same
        way teardown finds them, and compares them with the seed files: every seeded login, group
        name and application label should be present, nothing the module owns should be there that
        the data does not describe, every display name should match the data by codepoint, and
        every membership a group row lists should hold.

        Names are compared ordinally, not with -eq, because a decomposed and a precomposed name are
        equal to -eq and different on the wire; a name that came back mangled is the fault this
        exists to catch. Logins are compared case-insensitively because Okta folds them.

        Memberships are judged on what is missing only: a group rule adds members the data never
        lists, and that is not a fault.

        An org whose active-user limit stopped the seed short reports the users it could not create
        as missing. That is the truth of the org, and the report says so rather than adjusting the
        expectation.
    .PARAMETER SkipMembership
        Do not read group memberships, which costs one call per seeded group
    .PARAMETER Quiet
        Return the result without writing to the console
    .OUTPUTS
        PSCustomObject typed TestEnvironmentVerification. Passed is $true when every check passed.
    .EXAMPLE
        PS> Test-OktaEnvironment

        Prints one line per check and returns the result.
    .EXAMPLE
        PS> if (-not (Test-OktaEnvironment -Quiet).Passed) { throw 'The Okta seed is incomplete.' }

        Gates a script on the org matching the data.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [switch]$SkipMembership,

        [Parameter()]
        [switch]$Quiet
    )

    $connection = Get-OktaConnection
    $dataPath = Get-OktaDataPath
    $checks = New-Object System.Collections.Generic.List[object]

    $read = { param($file) @(Import-Csv -Path (Join-Path -Path $dataPath -ChildPath $file) -Encoding UTF8) }
    $loginOf = { param($loginPrefix) '{0}@{1}' -f $loginPrefix, $connection.EmailDomain }
    $named = { param($name) '{0}-{1}' -f $connection.Prefix, $name }

    # --- Users -----------------------------------------------------------------------------
    $userRows = & $read 'OktaUsers.csv'
    $users = @(Get-OktaSeededUser -Prefix $connection.Prefix -EmailDomain $connection.EmailDomain)
    $checks.Add((New-TestEnvironmentCheck -Name 'Users' `
                -Expected @($userRows | ForEach-Object { & $loginOf $_.LoginPrefix }) `
                -Found @($users | ForEach-Object { [string]$_.profile.login }) -IgnoreCase))

    $userByLogin = @{}
    foreach ($user in $users) {
        if ($user.profile.login) { $userByLogin[([string]$user.profile.login).ToLowerInvariant()] = $user }
    }
    $compared = 0
    $mismatch = foreach ($row in $userRows) {
        if (-not $row.DisplayName) { continue }
        $user = $userByLogin[(& $loginOf $row.LoginPrefix).ToLowerInvariant()]
        if (-not $user) { continue }
        $compared++
        if (-not [string]::Equals([string]$user.profile.displayName, [string]$row.DisplayName, [StringComparison]::Ordinal)) {
            "{0}: '{1}' should be '{2}'" -f $row.LoginPrefix, $user.profile.displayName, $row.DisplayName
        }
    }
    $checks.Add((New-TestEnvironmentCheck -Name 'User display names' -Compared $compared -Mismatch @($mismatch)))

    # --- Groups and applications -----------------------------------------------------------
    $groupRows = & $read 'OktaGroups.csv'
    $groups = @(Get-OktaSeededGroup -Prefix $connection.Prefix -SeedMarker $connection.SeedMarker)
    $checks.Add((New-TestEnvironmentCheck -Name 'Groups' `
                -Expected @($groupRows | ForEach-Object { & $named $_.DisplayName }) `
                -Found @($groups | ForEach-Object { [string]$_.profile.name })))

    $appRows = & $read 'OktaApps.csv'
    $apps = @(Get-OktaSeededApp -Prefix $connection.Prefix -EmailDomain $connection.EmailDomain)
    $checks.Add((New-TestEnvironmentCheck -Name 'Applications' `
                -Expected @($appRows | ForEach-Object { & $named $_.Label }) `
                -Found @($apps | ForEach-Object { [string]$_.label })))

    # --- Memberships -----------------------------------------------------------------------
    if (-not $SkipMembership) {
        $groupByName = @{}
        foreach ($group in $groups) { if ($group.profile.name) { $groupByName[[string]$group.profile.name] = $group } }

        $expectedPairs = New-Object System.Collections.Generic.List[string]
        $foundPairs = New-Object System.Collections.Generic.List[string]
        foreach ($row in $groupRows) {
            $members = @($row.Members -split ';' | Where-Object { $_ })
            if ($members.Count -eq 0) { continue }
            $groupName = & $named $row.DisplayName
            foreach ($member in $members) { $expectedPairs.Add(('{0} <- {1}' -f $groupName, (& $loginOf $member))) }

            $group = $groupByName[$groupName]
            if (-not $group) { continue }
            try {
                foreach ($user in @(Invoke-OktaRequest -Method GET -Path "/api/v1/groups/$($group.id)/users" -Query @{ limit = 200 } -Paginate)) {
                    if ($user.profile.login) { $foundPairs.Add(('{0} <- {1}' -f $groupName, [string]$user.profile.login)) }
                }
            }
            catch {
                Write-Warning "Could not read the members of '$groupName': $($_.Exception.Message). They are counted as missing."
            }
        }
        $checks.Add((New-TestEnvironmentCheck -Name 'Group memberships' -Expected $expectedPairs -Found $foundPairs -IgnoreCase -MissingOnly))
    }

    return New-TestEnvironmentVerification -Provider 'Okta' -Target $connection.OrgUrl -Check $checks.ToArray() -Quiet:$Quiet
}