Tests/Sources.Tests.ps1
|
BeforeDiscovery { $script:Root = Split-Path -Parent (Split-Path -Parent $PSCommandPath) # Todos los ficheros de codigo del repositorio, pruebas incluidas. $script:Todos = @( Get-ChildItem -LiteralPath $script:Root -Recurse -Include '*.ps1', '*.psm1' -File | Where-Object { $_.FullName -notmatch '[\\/](out|\.git)[\\/]' } | Sort-Object -Property FullName | ForEach-Object { @{ Ruta = $_.FullName; Nombre = $_.Name } } ) # Solo el modulo y el script de publicacion. Las pruebas quedan fuera de los # controles de contenido porque contienen a proposito los patrones prohibidos # (en expresiones regulares) para poder vigilarlos. $script:Motor = @($script:Todos | Where-Object { $_.Ruta -notmatch '[\\/]Tests[\\/]' }) } BeforeAll { $script:Root = Split-Path -Parent (Split-Path -Parent $PSCommandPath) } # Estas pruebas no ejercitan comportamiento: vigilan el codigo fuente para que # los defectos ya corregidos no puedan volver a entrar en una contribucion. Describe 'Higiene de todo el codigo' { It '<Nombre> no pasa -and ni -or como argumento de un comando' -ForEach $script:Todos { # "if (Test-TechCareAdmin -and (Test-Path $x))" no es un AND: PowerShell # pasa -and y el resultado de Test-Path como argumentos posicionales a la # funcion, que no los declara, y descarta su VALOR. Medido con contadores: # la segunda condicion SI se ejecuta (1 llamada), solo se tira el # resultado, y se pierde el cortocircuito. En un modulo que borra ficheros # y para servicios, eso significa que cualquier segunda condicion con # efecto lateral se ejecutaria sin la guarda que el autor creia tener. $errores = $null $tokens = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Ruta, [ref]$tokens, [ref]$errores) @($errores).Count | Should -Be 0 -Because "$Nombre debe parsear sin errores" $comandos = @($ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true)) $sospechosos = @( foreach ($c in $comandos) { foreach ($e in $c.CommandElements) { if ($e -is [System.Management.Automation.Language.CommandParameterAst] -and @('and', 'or') -contains $e.ParameterName) { '{0}:{1}' -f $Nombre, $e.Extent.StartLineNumber } } } ) $sospechosos -join ' | ' | Should -BeNullOrEmpty -Because 'el operador logico necesita parentesis alrededor de la llamada' } It '<Nombre> empieza por BOM UTF-8 y es ASCII puro despues' -ForEach $script:Todos { # Sin BOM, Windows PowerShell 5.1 lee el fichero con la pagina de codigos # ANSI del sistema y cualquier byte no ASCII sale corrupto en la consola. $bytes = [System.IO.File]::ReadAllBytes($Ruta) $bytes.Length | Should -BeGreaterThan 3 "$($bytes[0]),$($bytes[1]),$($bytes[2])" | Should -Be '239,187,191' -Because "$Nombre debe llevar BOM UTF-8" $noAscii = @($bytes[3..($bytes.Length - 1)] | Where-Object { $_ -gt 127 }) $noAscii.Count | Should -Be 0 -Because "$Nombre debe ser ASCII puro tras el BOM" } } Describe 'Higiene del codigo del modulo' { It '<Nombre> no usa sintaxis que no exista en Windows PowerShell 5.1' -ForEach $script:Motor { $texto = Get-Content -LiteralPath $Ruta -Raw $texto | Should -Not -Match '\?\?' -Because 'el operador de fusion de nulos no existe en 5.1' $texto | Should -Not -Match '-Parallel\b' -Because 'ForEach-Object -Parallel no existe en 5.1' $texto | Should -Not -Match 'Join-String' -Because 'Join-String no existe en 5.1' $texto | Should -Not -Match '\$using:' -Because 'se usa con -Parallel, que esta prohibido' } It '<Nombre> no deja marcadores de trabajo pendiente' -ForEach $script:Motor { # Se usa [regex]::IsMatch y no -Match porque el operador -match de # PowerShell ignora las mayusculas: '\bTODO\b' casaba con la palabra # castellana "todo", que aparece de forma legitima en los comentarios. $texto = Get-Content -LiteralPath $Ruta -Raw foreach ($marcador in @('\bTODO\b', '\bFIXME\b', '\bXXX\b', 'TU[- ]?NOMBRE', 'TU[- ]?EMPRESA', 'TU[- ]?USUARIO')) { [regex]::IsMatch($texto, $marcador) | Should -BeFalse -Because "$Nombre no debe contener $marcador" } } } Describe 'Promesas del README verificadas contra el codigo' { BeforeAll { $script:Ficheros = @( Get-ChildItem -LiteralPath (Join-Path -Path $script:Root -ChildPath 'Public') -Filter '*.ps1' -File Get-ChildItem -LiteralPath (Join-Path -Path $script:Root -ChildPath 'Private') -Filter '*.ps1' -File ) $script:Codigo = ($script:Ficheros | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw }) -join "`n" # Version sin comentarios: un comentario que menciona un argumento # prohibido no es una llamada que lo use. $script:CodigoSinComentarios = ( ($script:Codigo -split "`n") | Where-Object { $_ -notmatch '^\s*#' } ) -join "`n" } It 'no reinicia ni apaga el equipo en ningun punto' { $script:CodigoSinComentarios | Should -Not -Match 'Restart-Computer' $script:CodigoSinComentarios | Should -Not -Match 'Stop-Computer' $script:CodigoSinComentarios | Should -Not -Match 'shutdown(\.exe)?['']?\s' } It 'no desinstala aplicaciones integradas' { $script:CodigoSinComentarios | Should -Not -Match 'Remove-Appx' } It 'no desactiva Defender' { $script:CodigoSinComentarios | Should -Not -Match 'DisableRealtimeMonitoring' $script:CodigoSinComentarios | Should -Not -Match 'Set-MpPreference' $script:CodigoSinComentarios | Should -Not -Match 'Remove-MpPreference' } It 'no deshabilita Windows Update, y rearranca lo que para' { $script:CodigoSinComentarios | Should -Not -Match 'Set-Service[^\n]*Disabled' $script:CodigoSinComentarios | Should -Not -Match 'NoAutoUpdate' $script:CodigoSinComentarios | Should -Match 'Start-Service' -Because 'si se para un servicio, hay codigo que lo rearranca' } It 'chkdsk solo se invoca en modo escaneo, nunca con /f, /r ni /spotfix' { foreach ($prohibido in @("'/f'", "'/r'", "'/spotfix'")) { $script:CodigoSinComentarios | Should -Not -BeLike ('*' + $prohibido + '*') } $script:CodigoSinComentarios | Should -BeLike "*'/scan'*" -Because 'chkdsk se usa en modo solo lectura' } It 'DISM no usa /ResetBase, que haria irreversibles las actualizaciones' { $script:CodigoSinComentarios | Should -Not -BeLike "*'/ResetBase'*" } It 'no contacta ningun servidor: no hay descargas ni llamadas web' { # Sin telemetria es una promesa del README, y esta es su comprobacion. $script:CodigoSinComentarios | Should -Not -Match 'Invoke-WebRequest' $script:CodigoSinComentarios | Should -Not -Match 'Invoke-RestMethod' $script:CodigoSinComentarios | Should -Not -Match 'System\.Net\.WebClient' $script:CodigoSinComentarios | Should -Not -Match 'Start-BitsTransfer' $script:CodigoSinComentarios | Should -Not -Match 'New-Object\s+Net\.Sockets' } It 'Write-Host solo aparece en el unico sumidero de consola' { foreach ($f in $script:Ficheros) { if ($f.BaseName -eq 'Write-TechCareLog') { continue } (Get-Content -LiteralPath $f.FullName -Raw) | Should -Not -Match 'Write-Host' -Because "$($f.Name) debe registrar con Write-TechCareLog" } } It 'todo cmdlet publico que escribe consulta ShouldProcess de verdad' { # Declarar SupportsShouldProcess y no llamar a ShouldProcess seria una # promesa falsa: -WhatIf se aceptaria y no haria nada. foreach ($f in (Get-ChildItem -LiteralPath (Join-Path -Path $script:Root -ChildPath 'Public') -Filter '*.ps1' -File)) { $texto = Get-Content -LiteralPath $f.FullName -Raw if ($texto -notmatch 'SupportsShouldProcess') { continue } $texto | Should -Match 'ShouldProcess\(' -Because "$($f.Name) declara ShouldProcess y debe consultarlo" } } } |