Tests/ResolveHelpers.Tests.ps1

Import-Module "$PSScriptRoot/../ChocoForge.psd1" -Force

Describe 'Compare-PublishedVersions' {
    InModuleScope 'ChocoForge' {
        It 'Returns versions not in published list' {
            $release = @([version]'1.0.0', [version]'2.0.0', [version]'3.0.0')
            $published = @([version]'1.0.0', [version]'2.0.0')

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -HaveCount 1
            $missing | Should -Be ([version]'3.0.0')
        }

        It 'Returns empty when all versions are published' {
            $release = @([version]'1.0.0', [version]'2.0.0')
            $published = @([version]'1.0.0', [version]'2.0.0')

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -BeNullOrEmpty
        }

        It 'Returns all when none are published' {
            $release = @([version]'1.0.0', [version]'2.0.0')
            $published = @()

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -HaveCount 2
        }

        It 'Treats Revision=-1 as wildcard (release without revision matches published)' {
            # Release version 5.0.1 (3-part, Revision=-1) matches published 5.0.1.0
            $release = @([version]'5.0.1')
            $published = @([version]'5.0.1.0')

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -BeNullOrEmpty
        }

        It 'Treats published Revision=-1 as wildcard' {
            # Published 5.0.1 (Revision=-1) matches release 5.0.1.2
            $release = @([version]'5.0.1.2')
            $published = @([version]'5.0.1')

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -BeNullOrEmpty
        }

        It 'Does not match different revisions when both are explicit' {
            $release = @([version]'5.0.1.2')
            $published = @([version]'5.0.1.1')

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -HaveCount 1
            $missing | Should -Be ([version]'5.0.1.2')
        }

        It 'Handles 4-part versions correctly' {
            $release = @([version]'2.0.1.5', [version]'2.0.1.6')
            $published = @([version]'2.0.1.5')

            $missing = Compare-PublishedVersions -ReleaseVersions $release -PublishedVersions $published
            $missing | Should -HaveCount 1
            $missing | Should -Be ([version]'2.0.1.6')
        }
    }
}

Describe 'Resolve-SourceCredentials' {
    InModuleScope 'ChocoForge' {
        BeforeEach {
            Mock Expand-EnvironmentVariables { 'fake-key' }
        }

        It 'Resolves GitHub credentials from URL' {
            $source = [PSCustomObject]@{
                url    = 'https://nuget.pkg.github.com/fdcastel/index.json'
                apiKey = '${APIKEY_GITHUB}'
            }
            $creds = Resolve-SourceCredentials -Source $source -SourceName 'github'
            $creds.User | Should -Be 'fdcastel'
            $creds.Password | Should -Be 'fake-key'
        }

        It 'Resolves GitLab credentials with username' {
            $source = [PSCustomObject]@{
                url      = 'https://gitlab.com/api/v4/projects/123/packages/nuget/index.json'
                apiKey   = '${APIKEY_GITLAB}'
                username = 'myuser'
            }
            $creds = Resolve-SourceCredentials -Source $source -SourceName 'gitlab'
            $creds.User | Should -Be 'myuser'
            $creds.Password | Should -Be 'fake-key'
        }

        It 'Returns empty credentials for Chocolatey community source' {
            $source = [PSCustomObject]@{
                url    = 'https://community.chocolatey.org/api/v2'
                apiKey = '${APIKEY_CHOCOLATEY}'
            }
            $creds = Resolve-SourceCredentials -Source $source -SourceName 'community'
            $creds.Keys | Should -HaveCount 0
        }

        It 'Throws when GitLab username is missing' {
            $source = [PSCustomObject]@{
                url    = 'https://gitlab.com/api/v4/projects/123/packages/nuget/index.json'
                apiKey = '${APIKEY_GITLAB}'
            }
            { Resolve-SourceCredentials -Source $source -SourceName 'gitlab' } | Should -Throw '*requires a username*'
        }
    }
}

Describe 'Resolve-SourceCredentials (missing token degrades, does not throw)' {
    InModuleScope 'ChocoForge' {
        BeforeEach {
            # Deliberately NOT mocked to a constant - a blanket mock hides this behaviour.
            Mock Expand-EnvironmentVariables { $null }
        }

        It 'Returns empty credentials for GitHub when the token is not set' {
            $source = [PSCustomObject]@{
                url    = 'https://nuget.pkg.github.com/fdcastel/index.json'
                apiKey = '${APIKEY_GITHUB}'
            }
            { Resolve-SourceCredentials -Source $source -SourceName 'github' } | Should -Not -Throw
            (Resolve-SourceCredentials -Source $source -SourceName 'github').Keys | Should -HaveCount 0
        }

        It 'Returns empty credentials for GitLab when the token is not set' {
            $source = [PSCustomObject]@{
                url      = 'https://gitlab.com/api/v4/projects/123/packages/nuget/index.json'
                apiKey   = '${APIKEY_GITLAB}'
                username = 'myuser'
            }
            { Resolve-SourceCredentials -Source $source -SourceName 'gitlab' } | Should -Not -Throw
            (Resolve-SourceCredentials -Source $source -SourceName 'gitlab').Keys | Should -HaveCount 0
        }

        It 'Still throws for a GitLab source with no username (a configuration error, not a missing secret)' {
            $source = [PSCustomObject]@{
                url    = 'https://gitlab.com/api/v4/projects/123/packages/nuget/index.json'
                apiKey = '${APIKEY_GITLAB}'
            }
            { Resolve-SourceCredentials -Source $source -SourceName 'gitlab' } | Should -Throw '*requires a username*'
        }
    }
}

Describe 'Resolve-ForgeConfiguration (degrades when tokens are absent)' {
    InModuleScope 'ChocoForge' {
        It 'Completes and marks unqueryable sources instead of throwing' {
            Mock Invoke-RestMethod {
                Get-Content "$PSScriptRoot/assets/firebird-mocks/github-releases.json" -Raw | ConvertFrom-Json
            }
            Mock Invoke-Chocolatey {
                [PSCustomObject]@{
                    ExitCode = 0
                    StdOut   = Get-Content "$PSScriptRoot/assets/firebird-mocks/chocolatey-packages.txt" -Raw
                    StdErr   = ''
                }
            }
            # Expand-EnvironmentVariables deliberately NOT mocked: with no tokens set it
            # returns $null, which is exactly the situation this test exists to cover.
            Mock Expand-EnvironmentVariables { $null }

            # Assigned outside any scriptblock: a value assigned inside one passed to
            # Should -Not -Throw would not escape that scriptblock's scope.
            { Read-ForgeConfiguration -Path "$PSScriptRoot/assets/firebird-package/firebird.forge.yaml" | Resolve-ForgeConfiguration } |
                Should -Not -Throw

            $config = Read-ForgeConfiguration -Path "$PSScriptRoot/assets/firebird-package/firebird.forge.yaml" | Resolve-ForgeConfiguration
            $config | Should -Not -BeNullOrEmpty
            $config.versions | Should -Not -BeNullOrEmpty

            # Every source lacks a usable key, so each must carry a skip reason...
            foreach ($name in $config.sources.Keys) {
                $config.sources[$name].skipReason | Should -Not -BeNullOrEmpty
            }
            # ...and the credential-requiring ones must report unknown, not "nothing published".
            $config.sources.github.publishedVersions | Should -BeNullOrEmpty
        }
    }
}

Describe 'Resolve-SourcePublishingStatus' {
    InModuleScope 'ChocoForge' {
        It 'Sets resolvedApiKey when environment variable is set' {
            Mock Expand-EnvironmentVariables { 'resolved-key' }
            $source = [PSCustomObject]@{ apiKey = '${SOME_KEY}' }
            Resolve-SourcePublishingStatus -Source $source -SourceName 'test'

            $source.resolvedApiKey | Should -Be 'resolved-key'
            $source.skipReason | Should -BeNullOrEmpty
        }

        It 'Sets skipReason when environment variable is not set' {
            Mock Expand-EnvironmentVariables { $null }
            $source = [PSCustomObject]@{ apiKey = '${MISSING_KEY}' }
            Resolve-SourcePublishingStatus -Source $source -SourceName 'test'

            $source.resolvedApiKey | Should -BeNullOrEmpty
            $source.skipReason | Should -Match 'not set'
        }

        It 'Warns when API key is plain text (not an env var reference)' {
            Mock Expand-EnvironmentVariables { param($InputString) $InputString }
            $source = [PSCustomObject]@{ apiKey = 'plaintext-key' }
            Resolve-SourcePublishingStatus -Source $source -SourceName 'test'

            $source.resolvedApiKey | Should -Be 'plaintext-key'
            $source.warningMessage | Should -Match 'plain text'
        }

        It 'Sets skipReason when no apiKey configured' {
            $source = [PSCustomObject]@{}
            Resolve-SourcePublishingStatus -Source $source -SourceName 'test'

            $source.skipReason | Should -Match 'No API key'
        }
    }
}