modules/Azure/Discovery/Tests/E2E/discovery.Tests.ps1

BeforeAll {
    $projectRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..' '..' '..' '..' '..')
    . (Join-Path $projectRoot 'psu-app' 'Tests' 'E2E' 'PesterE2EHelper.ps1')
    Initialize-PesterE2E -ProjectRoot $projectRoot

    # Verify PSU is reachable and discovery command exists
    $script:cmdCheck = Run-OnPSU 'Get-Command Start-CIEMAzureDiscovery -ErrorAction Stop | Select-Object -ExpandProperty Name'
    if ($script:cmdCheck -ne 'Start-CIEMAzureDiscovery') {
        throw "Local PSU not reachable or CIEM module not loaded. Start with: ./scripts/setup-local-psu.sh start"
    }

    # Verify real Azure auth profile exists and is active, then connect
    $script:authReady = Run-OnPSU @'
        $profile = Get-CIEMAzureAuthenticationProfile -IsActive $true | Select-Object -First 1
        if (-not $profile) { throw 'No active Azure auth profile found. Configure one in PSU before running E2E tests.' }
        Connect-CIEMAzure | Out-Null
        'connected'
'@
 -TimeoutSeconds 120

    if ($script:authReady -ne 'connected') {
        throw "Failed to connect to Azure. Ensure an active auth profile with valid credentials exists."
    }

    $script:testRunIds = @()
}

AfterAll {
    # Clean up only the discovery run records created by tests
    foreach ($runId in $script:testRunIds) {
        try { Run-OnPSU "Remove-CIEMAzureDiscoveryRun -Id $runId -Confirm:`$false; 'ok'" } catch {}
    }
}

Describe 'Azure Discovery E2E' -Skip {

    Context 'Full discovery run (Scope=All)' {
        BeforeAll {
            # Run full discovery — connect + discover in the same runspace (each Run-OnPSU is isolated)
            # Uses Run-OnPSU-LongRunning: starts job via REST, waits via Wait-PSUJob (gRPC)
            $script:discoveryRun = Run-OnPSU-LongRunning @'
                Connect-CIEMAzure | Out-Null
                $run = Start-CIEMAzureDiscovery -Scope All
                $run | Select-Object Id, Scope, Status, StartedAt, CompletedAt, ArmTypeCount, ArmRowCount, EntraTypeCount, EntraRowCount, WarningCount, ErrorMessage
'@
 -TimeoutSeconds 600

            if ($script:discoveryRun.Id) {
                $script:testRunIds += $script:discoveryRun.Id
            }

            # Query actual DB counts on PSU (accurate — avoids JSON serialization discrepancies)
            $script:dbCounts = Run-OnPSU @'
                $arm = @(Get-CIEMAzureArmResource)
                $entra = @(Get-CIEMAzureEntraResource)
                $rels = @(Get-CIEMAzureResourceRelationship)
                [PSCustomObject]@{
                    ArmRowCount = $arm.Count
                    ArmTypeCount = ($arm | Where-Object { $_.Type } | Group-Object Type).Count
                    EntraRowCount = $entra.Count
                    EntraTypeCount = ($entra | Where-Object { $_.Type } | Group-Object Type).Count
                    RelCount = $rels.Count
                }
'@


            # Query sample resources for property/shape assertions
            $script:armSample = Run-OnPSU '@(Get-CIEMAzureArmResource) | Where-Object { $_.Id } | Select-Object -First 1 Id, Type, Name, SubscriptionId'
            $script:armByType = Run-OnPSU @'
                $first = @(Get-CIEMAzureArmResource) | Where-Object { $_.Type } | Select-Object -First 1
                if ($first) {
                    $subset = @(Get-CIEMAzureArmResource -Type $first.Type)
                    [PSCustomObject]@{ FilterType = $first.Type; Count = $subset.Count; TotalArm = @(Get-CIEMAzureArmResource).Count }
                } else { $null }
'@

            $script:entraSample = Run-OnPSU '@(Get-CIEMAzureEntraResource) | Select-Object -First 1 Id, Type, DisplayName'
            $script:entraUsers = Run-OnPSU '@(Get-CIEMAzureEntraResource -Type "user") | Select-Object -First 3 Id, Type, DisplayName'
            $script:relSample = Run-OnPSU '@(Get-CIEMAzureResourceRelationship) | Select-Object -First 1 Id, SourceId, TargetId, Relationship'
            $runId = $script:discoveryRun.Id
            $script:runById = if ($runId) {
                Run-OnPSU "Get-CIEMAzureDiscoveryRun -Id $runId | Select-Object Id, Scope, Status"
            } else { $null }
            $script:lastRun = Run-OnPSU 'Get-CIEMAzureDiscoveryRun -Last 1 | Select-Object Id, Scope, Status'
        }

        # --- Run record assertions ---
        It 'Run has a positive Id' {
            $script:discoveryRun.Id | Should -BeGreaterThan 0
        }

        It 'Run Status is Completed or Partial' {
            $script:discoveryRun.Status | Should -BeIn @('Completed', 'Partial')
        }

        It 'Run Scope is All' {
            $script:discoveryRun.Scope | Should -Be 'All'
        }

        It 'Run CompletedAt is set' {
            $script:discoveryRun.CompletedAt | Should -Not -BeNullOrEmpty
        }

        # --- Run metrics match DB state ---
        # ArmRowCount is counted in-memory before DB write; INSERT OR REPLACE deduplicates
        # by primary key, so DB count can be slightly lower. We verify they're close.
        It 'ARM rows persisted to database match run ArmRowCount' {
            $script:discoveryRun.ArmRowCount | Should -BeGreaterThan 0
            $script:dbCounts.ArmRowCount | Should -BeGreaterOrEqual ([math]::Floor($script:discoveryRun.ArmRowCount * 0.95))
            $script:dbCounts.ArmRowCount | Should -BeLessOrEqual $script:discoveryRun.ArmRowCount
        }

        It 'Entra rows persisted to database match run EntraRowCount' {
            $script:discoveryRun.EntraRowCount | Should -BeGreaterThan 0
            $script:dbCounts.EntraRowCount | Should -Be $script:discoveryRun.EntraRowCount
        }

        It 'ARM resources span multiple distinct types in database' {
            $script:discoveryRun.ArmTypeCount | Should -BeGreaterThan 0
            $script:dbCounts.ArmTypeCount | Should -BeGreaterThan 0
        }

        It 'Entra type count in database matches run EntraTypeCount' {
            $script:discoveryRun.EntraTypeCount | Should -BeGreaterThan 0
            $script:dbCounts.EntraTypeCount | Should -Be $script:discoveryRun.EntraTypeCount
        }

        It 'Relationships were persisted to the database' {
            $script:dbCounts.RelCount | Should -BeGreaterThan 0
        }

        # --- ARM resource shape assertions ---
        It 'ARM resources have required properties (Id, Type, Name)' {
            $script:armSample | Should -Not -BeNullOrEmpty
            $script:armSample.Id | Should -Not -BeNullOrEmpty
            $script:armSample.Type | Should -Not -BeNullOrEmpty
            $script:armSample.Name | Should -Not -BeNullOrEmpty
        }

        It 'ARM Type filter returns a subset' {
            $script:armByType | Should -Not -BeNullOrEmpty
            $script:armByType.Count | Should -BeGreaterThan 0
            $script:armByType.Count | Should -BeLessOrEqual $script:armByType.TotalArm
        }

        # --- Entra resource shape assertions ---
        It 'Entra resources have required properties (Id, Type)' {
            $script:entraSample | Should -Not -BeNullOrEmpty
            $script:entraSample.Id | Should -Not -BeNullOrEmpty
            $script:entraSample.Type | Should -Not -BeNullOrEmpty
        }

        It 'Entra Type filter for user returns results' {
            @($script:entraUsers).Count | Should -BeGreaterThan 0
            ($script:entraUsers | Select-Object -First 1).Type | Should -Be 'user'
        }

        # --- Relationship shape assertions ---
        It 'Relationships have expected properties (SourceId, TargetId, Relationship)' {
            $script:relSample | Should -Not -BeNullOrEmpty
            $script:relSample.SourceId | Should -Not -BeNullOrEmpty
            $script:relSample.TargetId | Should -Not -BeNullOrEmpty
            $script:relSample.Relationship | Should -Not -BeNullOrEmpty
        }

        # --- Run query assertions ---
        It 'Get-CIEMAzureDiscoveryRun -Id returns the correct run' {
            $script:runById.Id | Should -Be $script:discoveryRun.Id
            $script:runById.Scope | Should -Be 'All'
        }

        It 'Get-CIEMAzureDiscoveryRun -Last 1 returns the most recent run' {
            $script:lastRun | Should -Not -BeNullOrEmpty
            $script:lastRun.Id | Should -BeGreaterOrEqual $script:discoveryRun.Id
        }
    }

    Context 'Concurrency guard' {
        BeforeAll {
            # Seed a fake Running discovery run to trigger the concurrency guard
            $script:fakeRunId = $null
            $script:fakeRun = Run-OnPSU @'
                New-CIEMAzureDiscoveryRun -Scope 'All' -Status 'Running' -StartedAt (Get-Date).ToString('o') |
                    Select-Object Id, Status
'@

            $script:fakeRunId = $script:fakeRun.Id
        }

        AfterAll {
            if ($script:fakeRunId) {
                try { Run-OnPSU "Remove-CIEMAzureDiscoveryRun -Id $($script:fakeRunId) -Confirm:`$false; 'ok'" } catch {}
            }
        }

        It 'Throws when a discovery run is already Running' {
            $errorThrown = $false
            try {
                Run-OnPSU 'Start-CIEMAzureDiscovery' -TimeoutSeconds 30
            }
            catch {
                if ($_ -match 'already in progress') { $errorThrown = $true }
            }
            $errorThrown | Should -BeTrue
        }
    }
}