Tests/Exports.Tests.ps1

BeforeAll {
    $script:Root = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
    $script:ModulePath = Join-Path -Path $script:Root -ChildPath 'TechCare.psd1'
    Import-Module -Name $script:ModulePath -Force
    $script:Manifest = Import-PowerShellDataFile -Path $script:ModulePath
    $script:Publicos = @(
        'Get-TechCareDiagnostics', 'Get-TechCarePerformance', 'Get-TechCareProfile',
        'Get-TechCareSecurity', 'Invoke-TechCare', 'Invoke-TechCareCleanup',
        'Invoke-TechCareRepair', 'Invoke-TechCareUpdate', 'New-TechCareReport',
        'Test-TechCarePrerequisite'
    )
    $script:Privados = @(
        'Add-TechCareResult', 'ConvertTo-TechCareHtml', 'Format-TechCareSize',
        'Get-TechCareFreeSpace', 'Get-TechCareManagedState', 'Get-TechCarePathSize',
        'Initialize-TechCareState', 'Invoke-TechCareProcess', 'New-TechCareRestorePoint',
        'Test-TechCareAdmin',
        'Test-TechCareConsent', 'Test-TechCarePlatform', 'Write-TechCareLog'
    )
}

AfterAll {
    Remove-Module -Name TechCare -Force -ErrorAction SilentlyContinue
}

Describe 'Contrato de exportaciones' {

    It 'exporta exactamente los diez cmdlets publicos' {
        $exportadas = @((Get-Module -Name TechCare).ExportedFunctions.Keys | Sort-Object)
        $exportadas | Should -Be @($script:Publicos | Sort-Object)
    }

    It 'no exporta ninguna funcion privada' {
        $exportadas = @((Get-Module -Name TechCare).ExportedFunctions.Keys)
        foreach ($nombre in $script:Privados) {
            $exportadas | Should -Not -Contain $nombre -Because "$nombre es interno y no forma parte de la API"
        }
    }

    It 'las funciones privadas no quedan accesibles en la sesion' {
        foreach ($nombre in $script:Privados) {
            @(Get-Command -Name $nombre -ErrorAction SilentlyContinue).Count | Should -Be 0 -Because "$nombre no debe poder llamarse desde fuera del modulo"
        }
    }

    It 'no exporta variables ni alias' {
        (Get-Module -Name TechCare).ExportedVariables.Count | Should -Be 0
        (Get-Module -Name TechCare).ExportedAliases.Count   | Should -Be 0
        (Get-Module -Name TechCare).ExportedCmdlets.Count   | Should -Be 0
    }

    It 'FunctionsToExport del manifiesto coincide con lo que hay en Public' {
        # Si no coinciden, PowerShell Gallery publica un modulo que exporta algo
        # distinto de lo documentado, y eso solo se descubre tras publicar.
        $enDisco = @(Get-ChildItem -LiteralPath (Join-Path -Path $script:Root -ChildPath 'Public') -Filter '*.ps1' -File |
                ForEach-Object { $_.BaseName } | Sort-Object)
        @($script:Manifest.FunctionsToExport | Sort-Object) | Should -Be $enDisco
    }

    It 'no usa comodines en FunctionsToExport' {
        # Un comodin obliga a PowerShell a cargar el modulo para saber que exporta
        # y rompe el autocompletado y el autoloading.
        foreach ($f in $script:Manifest.FunctionsToExport) {
            $f | Should -Not -BeLike '*`**'
        }
    }

    It 'cada fichero de Public y Private define una sola funcion, con el nombre del fichero' {
        foreach ($carpeta in @('Public', 'Private')) {
            foreach ($file in (Get-ChildItem -LiteralPath (Join-Path -Path $script:Root -ChildPath $carpeta) -Filter '*.ps1' -File)) {
                $tokens = $null
                $errores = $null
                $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errores)
                @($errores).Count | Should -Be 0 -Because "$($file.Name) debe parsear sin errores"
                $funciones = @($ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false))
                $funciones.Count | Should -Be 1 -Because "$($file.Name) debe definir una sola funcion"
                $funciones[0].Name | Should -Be $file.BaseName
            }
        }
    }
}

Describe 'Manifiesto del modulo' {

    It 'es un manifiesto valido para PowerShell' {
        { Test-ModuleManifest -Path $script:ModulePath -ErrorAction Stop } | Should -Not -Throw
    }

    It 'declara version 0.1.0 y las dos ediciones' {
        $script:Manifest.ModuleVersion        | Should -Be '0.1.0'
        $script:Manifest.PowerShellVersion    | Should -Be '5.1'
        @($script:Manifest.CompatiblePSEditions) | Should -Contain 'Desktop'
        @($script:Manifest.CompatiblePSEditions) | Should -Contain 'Core'
    }

    It 'acredita a Eduard Arbona como autor' {
        $script:Manifest.Author | Should -Be 'Eduard Arbona'
        $script:Manifest.Copyright | Should -BeLike '*Eduard Arbona*'
    }

    It 'no declara dependencias obligatorias' {
        # La promesa del README es que funciona con Windows recien instalado.
        # Se comprueba con ContainsKey y no con .Count: una clave ausente
        # devuelve $null, y @($null).Count vale 1, no 0.
        foreach ($clave in @('RequiredModules', 'RequiredAssemblies', 'NestedModules', 'ModuleList')) {
            if ($script:Manifest.ContainsKey($clave)) {
                @($script:Manifest[$clave]).Count | Should -Be 0 -Because "$clave debe estar vacia"
            }
        }
    }

    It 'publica Description, Tags, ProjectUri, LicenseUri e IconUri' {
        $psdata = $script:Manifest.PrivateData.PSData
        $script:Manifest.Description | Should -Not -BeNullOrEmpty
        $script:Manifest.Description.Length | Should -BeGreaterThan 60
        @($psdata.Tags).Count | Should -BeGreaterThan 5
        $psdata.ProjectUri | Should -Be 'https://github.com/earbona23/TechCare'
        $psdata.LicenseUri | Should -BeLike 'https://github.com/earbona23/TechCare/*'
        $psdata.IconUri    | Should -Be 'https://raw.githubusercontent.com/earbona23/TechCare/main/assets/icon.png'
    }

    It 'el IconUri apunta a raw y no a la pagina blob de GitHub' {
        # raw.githubusercontent.com devuelve el PNG; github.com/.../blob/... sirve
        # una pagina HTML y la Gallery no pinta ningun icono.
        $script:Manifest.PrivateData.PSData.IconUri | Should -BeLike 'https://raw.githubusercontent.com/*'
        $script:Manifest.PrivateData.PSData.IconUri | Should -Not -BeLike '*/blob/*'
    }

    It 'las ReleaseNotes son texto, no una URL suelta' {
        # La Gallery renderiza este campo COMO las notas de version. Una URL ahi
        # le ensena al lector un enlace donde esperaba leer que cambio.
        $notas = $script:Manifest.PrivateData.PSData.ReleaseNotes
        $notas | Should -Not -BeNullOrEmpty
        $notas.Trim() | Should -Not -Match '^https?://\S+$'
        $notas | Should -BeLike '*0.1.0*'
        $notas.Length | Should -BeGreaterThan 120
    }

    It 'declara las etiquetas de edicion que usa la Gallery para filtrar' {
        @($script:Manifest.PrivateData.PSData.Tags) | Should -Contain 'PSEdition_Desktop'
        @($script:Manifest.PrivateData.PSData.Tags) | Should -Contain 'PSEdition_Core'
    }

    It 'la version del manifiesto coincide con la del modulo en memoria' {
        InModuleScope -ModuleName TechCare {
            $script:ModuleVersion | Should -Be '0.1.0'
        }
    }

    It 'el icono referenciado existe en el repositorio' {
        Test-Path -LiteralPath (Join-Path -Path $script:Root -ChildPath 'assets/icon.png') | Should -BeTrue
    }
}