test/zGet-AdfFromService.Tests.ps1

BeforeDiscovery {
    $ModuleRootPath = $PSScriptRoot | Split-Path -Parent
    $moduleManifestName = 'azure.datafactory.tools.psd1'
    $moduleManifestPath = Join-Path -Path $ModuleRootPath -ChildPath $moduleManifestName

    Import-Module -Name $moduleManifestPath -Force -Verbose:$false
}

InModuleScope azure.datafactory.tools {
    $testHelperPath = $PSScriptRoot | Join-Path -ChildPath 'TestHelper'
    Import-Module -Name $testHelperPath -Force

    # Shared factory stub
    $script:fakeAdfi = [PSCustomObject]@{
        DataFactoryId = '/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.DataFactory/factories/adf-test'
        Location      = 'northeurope'
    }

    # ---------------------------------------------------------------------------
    Describe 'Get-AdfFromService' -Tag 'Unit' {

        It 'Should exist' {
            { Get-Command -Name Get-AdfFromService -ErrorAction Stop } | Should -Not -Throw
        }

        Context 'When all Az cmdlets succeed (happy path)' {
            BeforeEach {
                Mock Get-AzDataFactoryV2           { $script:fakeAdfi }
                Mock Get-AzDataFactoryV2Dataset           { @() }
                Mock Get-AzDataFactoryV2IntegrationRuntime { @() }
                Mock Get-AzDataFactoryV2LinkedService     { @() }
                Mock Get-AzDataFactoryV2Pipeline          { @() }
                Mock Get-AzDataFactoryV2DataFlow          { @() }
                Mock Get-AzDataFactoryV2Trigger           { @() }
                Mock Get-AzDFV2Credential                 { @() }
            }

            It 'Should return an AdfInstance with the correct factory name' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.Name | Should -Be 'adf-test'
            }

            It 'Should not call Invoke-AzRestMethod when Az cmdlets succeed' {
                Mock Invoke-AzRestMethod { throw 'Should not be called' }
                { Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test' } | Should -Not -Throw
            }
        }

        Context 'When Get-AzDataFactoryV2Dataset throws a deserialization error (issue #480)' {
            BeforeEach {
                Mock Get-AzDataFactoryV2           { $script:fakeAdfi }
                Mock Get-AzDataFactoryV2Dataset           { throw 'Unable to deserialize the response.' }
                Mock Get-AzDataFactoryV2IntegrationRuntime { @() }
                Mock Get-AzDataFactoryV2LinkedService     { @() }
                Mock Get-AzDataFactoryV2Pipeline          { @() }
                Mock Get-AzDataFactoryV2DataFlow          { @() }
                Mock Get-AzDataFactoryV2Trigger           { @() }
                Mock Get-AzDFV2Credential                 { @() }
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{
                        StatusCode = 200
                        Content    = '{"value":[{"name":"ds_adls_csv","properties":{}},{"name":"ds_servicenow_v2","properties":{}}]}'
                    }
                }
            }

            It 'Should not throw' {
                { Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test' } | Should -Not -Throw
            }

            It 'Should fall back to REST API and return all datasets by name' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.DataSets.Count | Should -Be 2
                $result.DataSets.Name | Should -Contain 'ds_adls_csv'
                $result.DataSets.Name | Should -Contain 'ds_servicenow_v2'
            }

            It 'Should return datasets as AdfPSDataset wrapper objects' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.DataSets | ForEach-Object { $_.GetType().Name | Should -Be 'AdfPSDataset' }
            }

            It 'Should call Invoke-AzRestMethod with the correct datasets URL' {
                Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                Assert-MockCalled Invoke-AzRestMethod -Times 1 -Exactly -ParameterFilter {
                    $Uri -like '*/datasets?api-version=2018-06-01'
                }
            }
        }

        # This context specifically tests the real-world failure mode from issue #480:
        # The Az.DataFactory SDK writes a *non-terminating* Write-Error (not a throw) when
        # it cannot deserialize an unsupported object type such as ServiceNow V2.
        # Without -ErrorAction Stop on the cmdlet call the try-catch never fires; with it,
        # the non-terminating error is promoted to terminating and the catch fires correctly.
        Context 'When Get-AzDataFactoryV2Dataset emits a non-terminating deserialization error (real Az.DataFactory behaviour)' {
            BeforeEach {
                Mock Get-AzDataFactoryV2           { $script:fakeAdfi }
                # Simulate the actual Az.DataFactory SDK behaviour: Write-Error (non-terminating),
                # NOT throw. The cmdlet call in Get-AdfFromService uses -ErrorAction Stop which
                # must promote this to a terminating error so the catch block fires.
                Mock Get-AzDataFactoryV2Dataset           { Write-Error 'Unable to deserialize the response.' }
                Mock Get-AzDataFactoryV2IntegrationRuntime { @() }
                Mock Get-AzDataFactoryV2LinkedService     { @() }
                Mock Get-AzDataFactoryV2Pipeline          { @() }
                Mock Get-AzDataFactoryV2DataFlow          { @() }
                Mock Get-AzDataFactoryV2Trigger           { @() }
                Mock Get-AzDFV2Credential                 { @() }
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{
                        StatusCode = 200
                        Content    = '{"value":[{"name":"ds_adls_csv","properties":{}},{"name":"ds_servicenow_v2","properties":{}}]}'
                    }
                }
            }

            It 'Should not throw even when the cmdlet only writes a non-terminating error' {
                { Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test' } | Should -Not -Throw
            }

            It 'Should activate the REST API fallback and return all datasets' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.DataSets.Count | Should -Be 2
                $result.DataSets.Name | Should -Contain 'ds_adls_csv'
                $result.DataSets.Name | Should -Contain 'ds_servicenow_v2'
            }

            It 'Should return AdfPSDataset wrapper objects from the REST fallback' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.DataSets | ForEach-Object { $_.GetType().Name | Should -Be 'AdfPSDataset' }
            }
        }

        Context 'When Get-AzDataFactoryV2LinkedService throws a deserialization error' {
            BeforeEach {
                Mock Get-AzDataFactoryV2           { $script:fakeAdfi }
                Mock Get-AzDataFactoryV2Dataset           { @() }
                Mock Get-AzDataFactoryV2IntegrationRuntime { @() }
                Mock Get-AzDataFactoryV2LinkedService     { throw 'Unable to deserialize the response.' }
                Mock Get-AzDataFactoryV2Pipeline          { @() }
                Mock Get-AzDataFactoryV2DataFlow          { @() }
                Mock Get-AzDataFactoryV2Trigger           { @() }
                Mock Get-AzDFV2Credential                 { @() }
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{
                        StatusCode = 200
                        Content    = '{"value":[{"name":"ls_adls","properties":{}},{"name":"ls_servicenow","properties":{}}]}'
                    }
                }
            }

            It 'Should not throw' {
                { Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test' } | Should -Not -Throw
            }

            It 'Should return linked services via REST fallback as AdfPSLinkedService objects' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.LinkedServices.Count | Should -Be 2
                $result.LinkedServices | ForEach-Object { $_.GetType().Name | Should -Be 'AdfPSLinkedService' }
            }
        }

        Context 'When Get-AzDataFactoryV2Trigger throws a deserialization error' {
            BeforeEach {
                Mock Get-AzDataFactoryV2           { $script:fakeAdfi }
                Mock Get-AzDataFactoryV2Dataset           { @() }
                Mock Get-AzDataFactoryV2IntegrationRuntime { @() }
                Mock Get-AzDataFactoryV2LinkedService     { @() }
                Mock Get-AzDataFactoryV2Pipeline          { @() }
                Mock Get-AzDataFactoryV2DataFlow          { @() }
                Mock Get-AzDataFactoryV2Trigger           { throw 'Unable to deserialize the response.' }
                Mock Get-AzDFV2Credential                 { @() }
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{
                        StatusCode = 200
                        Content    = '{"value":[{"name":"tr_daily","properties":{"runtimeState":"Started"}},{"name":"tr_hourly","properties":{"runtimeState":"Stopped"}}]}'
                    }
                }
            }

            It 'Should not throw' {
                { Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test' } | Should -Not -Throw
            }

            It 'Should return triggers as AdfPSTrigger objects with correct RuntimeState' {
                $result = Get-AdfFromService -FactoryName 'adf-test' -ResourceGroupName 'rg-test'
                $result.Triggers.Count | Should -Be 2
                $result.Triggers | ForEach-Object { $_.GetType().Name | Should -Be 'AdfPSTrigger' }
                ($result.Triggers | Where-Object Name -eq 'tr_daily').RuntimeState  | Should -Be 'Started'
                ($result.Triggers | Where-Object Name -eq 'tr_hourly').RuntimeState | Should -Be 'Stopped'
            }
        }
    }

    # ---------------------------------------------------------------------------
    Describe 'Get-AdfObjectsFromServiceRestAPI' -Tag 'Unit' {

        It 'Should exist' {
            { Get-Command -Name Get-AdfObjectsFromServiceRestAPI -ErrorAction Stop } | Should -Not -Throw
        }

        Context 'When the API returns an empty list' {
            BeforeEach {
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{ StatusCode = 200; Content = '{"value":[]}' }
                }
            }

            It 'Should return an empty collection' {
                $result = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                @($result).Count | Should -Be 0
            }

            It 'Should call Invoke-AzRestMethod exactly once' {
                Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                Assert-MockCalled Invoke-AzRestMethod -Times 1 -Exactly
            }
        }

        Context 'When the API returns a single page of datasets' {
            BeforeEach {
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{
                        StatusCode = 200
                        Content    = '{"value":[{"name":"ds_one","properties":{}},{"name":"ds_two","properties":{}},{"name":"ds_three","properties":{}}]}'
                    }
                }
            }

            It 'Should return all items from the page' {
                $result = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                $result.Count | Should -Be 3
            }

            It 'Should return AdfPSDataset wrapper objects' {
                $result = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                $result | ForEach-Object { $_.GetType().Name | Should -Be 'AdfPSDataset' }
            }

            It 'Should preserve the dataset names' {
                $result = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                $result.Name | Should -Contain 'ds_one'
                $result.Name | Should -Contain 'ds_two'
                $result.Name | Should -Contain 'ds_three'
            }

            It 'Should call Invoke-AzRestMethod exactly once (no extra pages)' {
                Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                Assert-MockCalled Invoke-AzRestMethod -Times 1 -Exactly
            }
        }

        Context 'When the API returns two pages (pagination)' {
            BeforeEach {
                $script:page2Url = 'https://management.azure.com/subscriptions/sub-123/.../datasets?api-version=2018-06-01&skipToken=page2'
                $script:callCount = 0
                Mock Invoke-AzRestMethod {
                    $script:callCount++
                    if ($script:callCount -eq 1) {
                        return [PSCustomObject]@{
                            StatusCode = 200
                            Content    = "{`"value`":[{`"name`":`"ds_p1_a`",`"properties`":{}},{`"name`":`"ds_p1_b`",`"properties`":{}}],`"nextLink`":`"$($script:page2Url)`"}"
                        }
                    } else {
                        return [PSCustomObject]@{
                            StatusCode = 200
                            Content    = '{"value":[{"name":"ds_p2_a","properties":{}}]}'
                        }
                    }
                }
            }

            It 'Should return items from all pages combined' {
                $result = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                $result.Count | Should -Be 3
                $result.Name | Should -Contain 'ds_p1_a'
                $result.Name | Should -Contain 'ds_p1_b'
                $result.Name | Should -Contain 'ds_p2_a'
            }

            It 'Should call Invoke-AzRestMethod twice (once per page)' {
                Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                Assert-MockCalled Invoke-AzRestMethod -Times 2 -Exactly
            }

            It 'Should use the nextLink URL for the second request' {
                Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                Assert-MockCalled Invoke-AzRestMethod -Times 1 -Exactly -ParameterFilter {
                    $Uri -eq $script:page2Url
                }
            }
        }

        Context 'When called for each supported object type' {
            BeforeEach {
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{
                        StatusCode = 200
                        Content    = '{"value":[{"name":"obj1","properties":{"runtimeState":"Stopped"}}]}'
                    }
                }
            }

            It 'Should return AdfPSDataset for simpleType Dataset' {
                $r = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset'
                $r[0].GetType().Name | Should -Be 'AdfPSDataset'
            }

            It 'Should return AdfPSPipeline for simpleType Pipeline' {
                $r = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'pipelines' -simpleType 'Pipeline'
                $r[0].GetType().Name | Should -Be 'AdfPSPipeline'
            }

            It 'Should return AdfPSLinkedService for simpleType LinkedService' {
                $r = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'linkedservices' -simpleType 'LinkedService'
                $r[0].GetType().Name | Should -Be 'AdfPSLinkedService'
            }

            It 'Should return AdfPSIntegrationRuntime for simpleType IntegrationRuntime' {
                $r = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'integrationruntimes' -simpleType 'IntegrationRuntime'
                $r[0].GetType().Name | Should -Be 'AdfPSIntegrationRuntime'
            }

            It 'Should return AdfPSDataFlow for simpleType DataFlow' {
                $r = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'dataflows' -simpleType 'DataFlow'
                $r[0].GetType().Name | Should -Be 'AdfPSDataFlow'
            }

            It 'Should return AdfPSTrigger for simpleType Trigger with RuntimeState' {
                $r = Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'triggers' -simpleType 'Trigger'
                $r[0].GetType().Name | Should -Be 'AdfPSTrigger'
                $r[0].RuntimeState | Should -Be 'Stopped'
            }
        }

        Context 'When the API returns a non-200 status code' {
            BeforeEach {
                Mock Invoke-AzRestMethod {
                    return [PSCustomObject]@{ StatusCode = 403; Content = '{"error":{"code":"AuthorizationFailed"}}' }
                }
            }

            It 'Should throw when ErrorAction Stop is used' {
                { Get-AdfObjectsFromServiceRestAPI -adfi $script:fakeAdfi -typePlural 'datasets' -simpleType 'Dataset' -ErrorAction Stop } | Should -Throw
            }
        }
    }

    # ---------------------------------------------------------------------------
    Describe 'Get-AdfFromService' -Tag 'Integration' {
        # Variables for use in integration tests
        $t = Get-TargetEnv 'adf2'
        $script:adfName = $t.DataFactoryName
        $script:rg      = $t.ResourceGroupName

        It 'Should execute' {
            Get-AdfFromService -FactoryName $adfName -ResourceGroupName $rg
        }
    }

}