tests/Test-IWProject.ps1

#Requires -Version 7
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_TestHelpers.ps1')

$wizardRoot = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $wizardRoot 'modules/IW.Core.psm1') -Force
Import-Module (Join-Path $wizardRoot 'modules/IW.Project.psm1') -Force
Import-Module (Join-Path $wizardRoot 'modules/Definitions/Google.Entro.psm1') -Force

Register-IntegrationWizardPlatform -Name 'GoogleCloud' -Title 'Google Cloud' -CoreOptions @('GcpEntro') -CliName 'gcloud'
Register-GoogleEntroDefinitions
$def = Get-IntegrationWizardDefinition -Id 'google-gcp-entro'
Assert-True ($null -ne $def) 'google-gcp-entro registered'

$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("iw-project-test-{0}" -f [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
$env:SAILPOINT_CONNECT_ROOT = Join-Path $tmp '.sailpoint-connect'
Push-Location $tmp
try {
    $thrown = $false
    try { Test-IntegrationProjectName -Name '../bad' } catch { $thrown = $true }
    Assert-True $thrown 'rejects path separators in project name'

    $ctx = @{
        Project          = 'demo-customer'
        NonInteractive   = $true
        Action           = 'BuildRequirements'
        OrganizationId   = '416188549418'
        ProjectId        = 'emea-tes-team'
        Method           = 'PrivateKey'
        OutputDirectory  = $null
    }
    Initialize-IntegrationProjectContext -Platform 'GoogleCloud' -PlatformTitle 'Google Cloud' -Plan $def -Context $ctx -ScriptName 'Google Cloud.ps1'

    $root = Get-IntegrationProjectRoot
    Assert-True (Test-Path -LiteralPath $root) '.sailpoint-connect created on project init'

    $expectedFolder = Join-Path (Join-Path (Join-Path $root 'demo-customer') 'Google-Cloud') 'Entro-GCP'
    Assert-Equal $expectedFolder $ctx['IntegrationFolderPath'] 'integration folder path'
    Assert-Equal $expectedFolder $ctx['OutputDirectory'] 'OutputDirectory defaults to integration folder'

    Write-IntegrationProjectLogLine -Level 'STEP' -Message 'Test step line'
    $logPath = Join-Path $expectedFolder 'activity.log'
    Assert-True (Test-Path -LiteralPath $logPath) 'activity.log exists'
    $logText = Get-Content -LiteralPath $logPath -Raw
    Assert-True ($logText -match 'Test step line') 'activity.log contains step line'

    $ctxWithSecret = @{
        IntegrationProjectName = 'demo-customer'
        Action                 = 'Run'
        OrganizationId         = '1'
        ClientSecret           = 'super-secret'
    }
    $defSecret = @{
        Title  = 'Secret test'
        Inputs = @(
            @{ Name = 'OrganizationId'; Required = $false }
            @{ Name = 'ClientSecret'; Secret = $true; Required = $false }
        )
    }
    $snap = Get-IntegrationProjectSelectionSnapshot -Context $ctxWithSecret -Definition $defSecret -Platform 'GoogleCloud' -ScriptName 'Google Cloud.ps1'
    Assert-Equal '***' $snap.inputs.ClientSecret 'secrets masked in selection snapshot'

    $defInferredSecret = @{
        Title  = 'Inferred secret test'
        Inputs = @(
            @{ Name = 'ApiToken'; Required = $false }
        )
    }
    $ctxInferred = @{ IntegrationProjectName = 'demo-customer'; ApiToken = 'pat-token-value' }
    $snapInferred = Get-IntegrationProjectSelectionSnapshot -Context $ctxInferred -Definition $defInferredSecret -Platform 'GitHub' -ScriptName 'GitHub.ps1'
    Assert-Equal '***' $snapInferred.inputs.ApiToken 'inferred secret by name is masked in selection snapshot'

    $record = Get-IntegrationProjectSelectionSnapshot -Context $ctx -Definition $def -Platform 'GoogleCloud' -ScriptName 'Google Cloud.ps1'
    Write-IntegrationProjectSelection -Record $record
    $selPath = Join-Path (Join-Path $root 'demo-customer') 'selection.json'
    Assert-True (Test-Path -LiteralPath $selPath) 'selection.json written'
    $sel = Get-Content -LiteralPath $selPath -Raw | ConvertFrom-Json
    Assert-Equal 'Entro-GCP' $sel.integrationFolder 'selection records integration folder name'

    $jsonl = Join-Path (Join-Path $root 'demo-customer') 'selections.jsonl'
    Assert-True (Test-Path -LiteralPath $jsonl) 'selections.jsonl appended'

    $overridePath = Join-Path $tmp 'custom-out'
    $ctxOverride = @{
        Project         = 'demo-customer'
        NonInteractive  = $true
        Action          = 'BuildRequirements'
        OrganizationId  = '1'
        ProjectId       = 'p'
        Method          = 'PrivateKey'
        OutputDirectory = $overridePath
    }
    Initialize-IntegrationProjectActivityLog -LogPath (Join-Path $expectedFolder 'activity-override.log')
    Initialize-IntegrationProjectContext -Platform 'GoogleCloud' -PlatformTitle 'Google Cloud' -Plan $def -Context $ctxOverride -ScriptName 'Google Cloud.ps1'
    Assert-Equal $overridePath $ctxOverride['OutputDirectory'] 'explicit OutputDirectory unchanged'
    Assert-True $ctxOverride['IntegrationOutputOverride'] 'override flag set'

    $folderName = Get-IntegrationWizardIntegrationFolderName -Definition $def
    Assert-Equal 'Entro-GCP' $folderName 'FolderName from definition'

    $saasMember = @{
        Id          = 'google-saas-connector'
        Title       = 'Google Workspace connector'
        FolderName  = 'Google-Workspace'
        Platform    = 'GoogleCloud'
        CliName     = 'gcloud'
        Inputs      = @()
        AdminRoles  = @()
        OutputsHelp = ''
        Steps       = { param($ctx) @() }
    }
    $combined = Merge-IntegrationWizardDefinitions -Members @($saasMember, $def)
    $combinedFolder = Get-IntegrationWizardIntegrationFolderName -Definition $combined
    Assert-Equal 'Google-Workspace-and-Entro-GCP' $combinedFolder 'composite folder name is filesystem-safe'
    Assert-True ($combinedFolder -notmatch '[+]') 'composite folder name contains no plus sign'

    $existingOnly = @(Get-IntegrationProjectNames)
    Assert-True ($existingOnly -contains 'demo-customer') 'lists project subfolders only'

    Import-Module (Join-Path $wizardRoot 'modules/IW.Render.psm1') -Force
    Import-Module (Join-Path $wizardRoot 'modules/ISC.OperatorConsole.psm1') -Force -Global -WarningAction SilentlyContinue
    Initialize-OperatorConsole -NonInteractive
    $runCtx = @{
        IntegrationProjectName    = 'demo-customer'
        IntegrationPlatformTitle  = 'Google Cloud'
        NonInteractive            = $true
        ConnectionMode            = 'NoSession'
        ManualDetail              = 'Enable audit logs.'
    }
    $manualDef = @{
        Title      = 'Entro GCP'
        FolderName = 'Entro-GCP'
        Platform   = 'GoogleCloud'
        CliName    = 'gcloud'
        Steps      = {
            param($c)
            @(
                @{ Id = 'audit'; Title = 'Org audit logs'; Kind = 'Manual'; Manual = [string]$c['ManualDetail'] }
            )
        }
    }
    Invoke-IntegrationWizardRun -Definition $manualDef -Context $runCtx
    $actionsPath = Get-IntegrationProjectOperatorActionsPath -ProjectName 'demo-customer'
    Assert-True (Test-Path -LiteralPath $actionsPath) 'operator-actions.md written for the project'
    $open = @(Read-IntegrationProjectOperatorActions -ProjectName 'demo-customer')
    Assert-Equal 1 $open.Count 'one operator action recorded'
    Assert-Equal 'audit' $open[0].Id 'operator action keeps the step id'
    Assert-True (-not $open[0].Done) 'a non-interactive run leaves the action open'
    Assert-Equal 'Google Cloud / Entro-GCP' $open[0].Section 'section names the platform and integration'

    $otherDef = @{
        Title      = 'Entro Workspace'
        FolderName = 'Entro-Workspace'
        Platform   = 'GoogleCloud'
        CliName    = 'gcloud'
        Steps      = {
            param($c)
            @(
                @{ Id = 'dwd'; Title = 'Domain-wide delegation'; Kind = 'Manual'; Manual = 'Authorize the client.' }
            )
        }
    }
    Invoke-IntegrationWizardRun -Definition $otherDef -Context $runCtx
    $both = @(Read-IntegrationProjectOperatorActions -ProjectName 'demo-customer')
    Assert-Equal 2 $both.Count 'a second integration adds its own section'
    Assert-Equal 'audit,dwd' (($both | ForEach-Object { $_.Id }) -join ',') 'both integrations stay in the project file'

    $done = @{
        Section = 'Google Cloud / Entro-GCP'
        Id      = 'audit'
        Title   = 'Org audit logs'
        Detail  = 'Enable audit logs.'
        Done    = $true
    }
    Save-IntegrationProjectOperatorAction -ProjectName 'demo-customer' -Action $done
    Invoke-IntegrationWizardRun -Definition $manualDef -Context $runCtx
    $kept = @(@(Read-IntegrationProjectOperatorActions -ProjectName 'demo-customer') | Where-Object { $_.Id -eq 'audit' })[0]
    Assert-True $kept.Done 'rerun keeps an action marked done when the instruction is unchanged'
    Assert-Equal 2 @(Read-IntegrationProjectOperatorActions -ProjectName 'demo-customer').Count 'rerun does not drop the other integration'

    $runCtx['ManualDetail'] = 'Enable a different set of audit logs.'
    Invoke-IntegrationWizardRun -Definition $manualDef -Context $runCtx
    $reset = @(@(Read-IntegrationProjectOperatorActions -ProjectName 'demo-customer') | Where-Object { $_.Id -eq 'audit' })[0]
    Assert-True (-not $reset.Done) 'a changed instruction becomes open again'
    Assert-Equal 'Enable a different set of audit logs.' $reset.Detail 'the file stores the new instruction'

    # Headless Project Requirement:
    # Non-interactive run with -OutputDirectory and without -Project should succeed using ephemeral workspace
    $headlessOut = Join-Path $tmp 'headless-out'
    $ctxHeadless = @{
        NonInteractive  = $true
        Action          = 'BuildRequirements'
        OrganizationId  = '1'
        ProjectId       = 'p'
        Method          = 'PrivateKey'
        OutputDirectory = $headlessOut
    }
    Initialize-IntegrationProjectContext -Platform 'GoogleCloud' -PlatformTitle 'Google Cloud' -Plan $def -Context $ctxHeadless -ScriptName 'Google Cloud.ps1'
    Assert-Equal 'ephemeral' $ctxHeadless['IntegrationProjectName'] 'headless without Project defaults to ephemeral'
    Assert-Equal $headlessOut $ctxHeadless['OutputDirectory'] 'headless OutputDirectory is preserved'
    Assert-Equal $headlessOut $ctxHeadless['IntegrationFolderPath'] 'headless IntegrationFolderPath maps to OutputDirectory'
    Assert-True (Test-Path -LiteralPath (Join-Path $headlessOut 'activity.log')) 'headless activity.log written to OutputDirectory'

    # Non-interactive run without Project and without OutputDirectory must still throw
    $ctxMissing = @{
        NonInteractive = $true
        Action         = 'BuildRequirements'
    }
    $headlessThrow = $false
    try {
        Initialize-IntegrationProjectContext -Platform 'GoogleCloud' -PlatformTitle 'Google Cloud' -Plan $def -Context $ctxMissing -ScriptName 'Google Cloud.ps1'
    }
    catch {
        $headlessThrow = $true
        Assert-True ($_.Exception.Message -match 'Non-interactive run requires -Project') 'missing both Project and OutputDirectory throws'
    }
    Assert-True $headlessThrow 'throws when neither Project nor OutputDirectory provided'
}
finally {
    Remove-Item env:SAILPOINT_CONNECT_ROOT -ErrorAction SilentlyContinue
    Pop-Location
    Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
}

Write-Host "PASS ($script:AssertionCount assertions)"