tests/Start-AZSCEntraExtraction.Tests.ps1

#Requires -Modules Pester

<#
.SYNOPSIS
    Pester tests for Start-AZSCEntraExtraction.
 
.DESCRIPTION
    Validates the Entra ID extraction engine:
      - Returns @{ EntraResources = [array] } structure
      - Processes all 15 entra queries
      - Normalizes resources to { id, name, TYPE, tenantId, properties }
      - Handles SingleObject queries (e.g., organization)
      - Graceful degradation — failed queries do not stop execution
      - Empty results produce an empty array, not $null
 
.NOTES
    Author: thisismydemo
    Version: 1.0.0
    Created: 2026-02-23
#>


$ModuleRoot = Split-Path -Parent $PSScriptRoot
Import-Module (Join-Path $ModuleRoot 'AzureScout.psd1') -Force -ErrorAction Stop

InModuleScope 'AzureScout' {
Describe 'Start-AZSCEntraExtraction' {

    # ── Return Structure ──────────────────────────────────────────────
    Context 'Return Structure' {

        BeforeAll {
            Mock Invoke-AZSCGraphRequest {
                return @(
                    [PSCustomObject]@{ id = '1'; displayName = 'Test User'; userPrincipalName = 'user@test.com' }
                )
            }
        }

        It 'Returns an object with EntraResources property' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $result | Should -Not -BeNullOrEmpty
            $result.PSObject.Properties.Name | Should -Contain 'EntraResources'
        }

        It 'EntraResources is an array' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $result.EntraResources | Should -BeOfType [System.Object]
        }

        It 'EntraResources contains items when Graph returns data' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $result.EntraResources.Count | Should -BeGreaterThan 0
        }
    }

    # ── Normalized Resource Shape ─────────────────────────────────────
    Context 'Normalized Resource Shape' {

        BeforeAll {
            Mock Invoke-AZSCGraphRequest {
                param($Uri)
                if ($Uri -like '*users*') {
                    return @(
                        [PSCustomObject]@{ id = 'user-001'; displayName = 'Alice'; userPrincipalName = 'alice@test.com' }
                    )
                }
                return @(
                    [PSCustomObject]@{ id = 'obj-001'; displayName = 'Generic Object' }
                )
            }
        }

        It 'Each resource has an id property' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $resource = $result.EntraResources | Select-Object -First 1
            $resource.PSObject.Properties.Name | Should -Contain 'id'
        }

        It 'Each resource has a name property' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $resource = $result.EntraResources | Select-Object -First 1
            $resource.PSObject.Properties.Name | Should -Contain 'name'
        }

        It 'Each resource has a TYPE property' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $resource = $result.EntraResources | Select-Object -First 1
            $resource.PSObject.Properties.Name | Should -Contain 'TYPE'
        }

        It 'Each resource has a tenantId property' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $resource = $result.EntraResources | Select-Object -First 1
            $resource.PSObject.Properties.Name | Should -Contain 'tenantId'
        }

        It 'Each resource has a properties property' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $resource = $result.EntraResources | Select-Object -First 1
            $resource.PSObject.Properties.Name | Should -Contain 'properties'
        }

        It 'tenantId is set to the provided TenantID' {
            $result = Start-AZSCEntraExtraction -TenantID 'my-tenant-id'
            $resource = $result.EntraResources | Select-Object -First 1
            $resource.tenantId | Should -Be 'my-tenant-id'
        }
    }

    # ── All 15 Entra Queries ──────────────────────────────────────────
    Context 'All Entra Queries Executed' {

        BeforeAll {
            $script:graphCallUris = [System.Collections.Generic.List[string]]::new()
            Mock Invoke-AZSCGraphRequest {
                param($Uri)
                $script:graphCallUris.Add($Uri)
                return @(
                    [PSCustomObject]@{ id = 'obj-1'; displayName = 'Test Object' }
                )
            }
        }

        AfterAll {
            Remove-Variable -Name graphCallUris -Scope Script -ErrorAction SilentlyContinue
        }

        It 'Calls Invoke-AZSCGraphRequest for at least 15 entra queries' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            Should -Invoke Invoke-AZSCGraphRequest -Times 15 -Scope It
        }
    }

    # ── Graceful Degradation ──────────────────────────────────────────
    Context 'Graceful Degradation — Individual Query Failure' {

        BeforeAll {
            $script:failCallCount = 0
            Mock Invoke-AZSCGraphRequest {
                param($Uri)
                $script:failCallCount++
                # Fail the first query, succeed all others
                if ($script:failCallCount -eq 1) {
                    throw 'Forbidden — insufficient privileges'
                }
                return @(
                    [PSCustomObject]@{ id = "obj-$($script:failCallCount)"; displayName = 'Item' }
                )
            }
        }

        AfterAll {
            Remove-Variable -Name failCallCount -Scope Script -ErrorAction SilentlyContinue
        }

        It 'Does not throw when a single query fails' {
            { Start-AZSCEntraExtraction -TenantID 'test-tenant' } | Should -Not -Throw
        }

        It 'Still returns data from succeeded queries' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $result.EntraResources.Count | Should -BeGreaterThan 0
        }
    }

    # ── All Queries Fail ──────────────────────────────────────────────
    Context 'All Queries Fail' {

        BeforeAll {
            Mock Invoke-AZSCGraphRequest { throw 'Service unavailable' }
        }

        It 'Does not throw even when all queries fail' {
            { Start-AZSCEntraExtraction -TenantID 'test-tenant' } | Should -Not -Throw
        }

        It 'Returns an empty EntraResources array' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $result.EntraResources.Count | Should -Be 0
        }
    }

    # ── SingleObject Queries ──────────────────────────────────────────
    Context 'SingleObject Queries' {

        BeforeAll {
            Mock Invoke-AZSCGraphRequest {
                param($Uri)
                # Organization endpoint returns a single object
                if ($Uri -like '*organization*') {
                    return [PSCustomObject]@{ id = 'org-1'; displayName = 'Contoso Corp' }
                }
                # Collection endpoints return arrays
                return @(
                    [PSCustomObject]@{ id = 'item-1'; displayName = 'Item' }
                )
            }
        }

        It 'Handles single-object responses without error' {
            { Start-AZSCEntraExtraction -TenantID 'test-tenant' } | Should -Not -Throw
        }

        It 'Includes the organization object in results' {
            $result = Start-AZSCEntraExtraction -TenantID 'test-tenant'
            $orgResource = $result.EntraResources | Where-Object { $_.name -eq 'Contoso Corp' -or $_.id -eq 'org-1' }
            $orgResource | Should -Not -BeNullOrEmpty
        }
    }

    # ── TenantID Parameter ────────────────────────────────────────────
    Context 'TenantID Parameter' {

        It 'TenantID is mandatory' {
            $cmd = Get-Command Start-AZSCEntraExtraction
            $attr = $cmd.Parameters['TenantID'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] }
            $attr.Mandatory | Should -BeTrue
        }
    }
}
} # end InModuleScope