Providers/Okta/Public/New-OktaUserType.ps1

function New-OktaUserType {
    <#
    .EXTERNALHELP TestEnvironment-Help.xml
    .SYNOPSIS
        Creates the second Okta user type and extends its schema
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [string[]]$TypeName,

        [Parameter()]
        [switch]$PassThru
    )

    $connection = Get-OktaConnection

    $rows = @(Import-Csv -Path (Join-Path (Get-OktaDataPath) 'OktaUserTypes.csv') -Encoding UTF8)
    if ($TypeName) {
        $rows = @($rows | Where-Object { $TypeName -contains $_.Name })
        $unknown = @($TypeName | Where-Object { $rows.Name -notcontains $_ })
        if ($unknown) { throw "No user type definition for: $($unknown -join ', ')" }
    }

    $result = [PSCustomObject]@{
        TotalTypes    = $rows.Count
        CreatedTypes  = 0
        ExistingTypes = 0
        Types         = @()
        Errors        = @()
    }

    $existingTypes = @(Invoke-OktaRequest -Method GET -Path '/api/v1/meta/types/user')
    $types = [System.Collections.Generic.List[object]]::new()

    foreach ($row in $rows) {
        # The API name must be a bare identifier, so the prefix is carried in the display name
        # and description instead. Teardown matches on the name having the prefix in it.
        $typeName = Get-OktaUserTypeName -Prefix $connection.Prefix -UserTypeKey $row.Name
        $displayName = '{0}-{1}' -f $connection.Prefix, $row.DisplayName

        if (-not $PSCmdlet.ShouldProcess($displayName, 'Create Okta user type')) { continue }

        try {
            $existing = @($existingTypes | Where-Object { $_.name -eq $typeName })

            if ($existing.Count -gt 0) {
                $type = $existing[0]
                $result.ExistingTypes++
                Write-Verbose "Reusing user type $typeName"
            }
            else {
                # Okta refuses to create a user type for a long while after one has been
                # deleted, answering with a bare "request body was not well-formed" that names
                # nothing. Measured against a live org across three teardown-then-reseed runs:
                # still refused four minutes after the deletion, still at eight, still at
                # fifteen, and accepted instantly at about thirty-five. It is not the NAME being
                # reserved - a brand-new name was refused in the same window while a raw POST of
                # the byte-identical body succeeded once the window had passed.
                #
                # So the wait is deliberately short. Sitting for fifteen minutes and then failing
                # anyway is worse than failing in two and saying what to do: it hides a
                # recoverable condition behind a hang, and the caller still ends up with a
                # half-seeded org. Two minutes covers the genuinely transient case; beyond that
                # the orchestrator reports the step as failed and the message below says exactly
                # which command to re-run once Okta has settled.
                $type = Invoke-OktaPendingCleanupRequest -Method POST -MaxWaitSeconds 120 `
                    -Path '/api/v1/meta/types/user' -Body @{
                        name        = $typeName
                        displayName = $displayName
                        description = $row.Description
                    }
                $result.CreatedTypes++
                Write-Verbose "Created user type $typeName"
            }

            # Each type has its own schema, reachable only through the link on the type object.
            # There is no predictable path to build by hand, which is why this is captured here
            # and passed to New-OktaProfileAttribute rather than reconstructed later.
            $schemaPath = $null
            if ($type._links -and $type._links.schema -and $type._links.schema.href) {
                $schemaPath = ([uri]$type._links.schema.href).AbsolutePath
            }

            $types.Add([PSCustomObject]@{
                Id          = $type.id
                Key         = $row.Name
                Name        = $typeName
                DisplayName = $displayName
                SchemaPath  = $schemaPath
            })
        }
        catch {
            $message = "Failed to create user type '$typeName': $($_.Exception.Message)"

            # The one failure here that is recoverable by waiting, and the one a caller will
            # otherwise misread as a broken request. Say what it is and what to run, rather than
            # leaving them with a 400 that names nothing and an org missing a user type, its ten
            # schema attributes and the two users that belong to it.
            if ($_.Exception.Message -match 'HTTP 400' -and $_.Exception.Message -match 'was not well-formed') {
                $message += [Environment]::NewLine +
                'Okta refuses to create any user type for a period after one has been deleted, and reports ' +
                'it as this malformed-body error. Nothing is wrong with the request. Measured at roughly ' +
                'half an hour on a trial org. The rest of the environment is unaffected - re-run ' +
                '"New-TestEnvironment -Skip ServiceApp" later and it will create the type, its schema ' +
                'attributes and the two users that need it, reusing everything already there.'
            }

            $result.Errors += $message
            Write-Error $message
        }
    }

    $result.Types = $types.ToArray()

    if ($PassThru) { return $result }
}