#requires -Version 5.1 # Isolated behavioural tests: no real downloads, Scoop updates or scheduled tasks. $ErrorActionPreference = 'Stop' $projectRoot = Split-Path $PSScriptRoot -Parent $fixtureRoot = Join-Path $PSScriptRoot ('.auto-update-test-' + [guid]::NewGuid()) $oldScoop = $env:SCOOP $oldPath = $env:PATH $oldFixture = $env:SCOOP_AUTO_UPDATE_TEST_ROOT $oldFailure = $env:SCOOP_AUTO_UPDATE_TEST_FAILURE function Assert-True($Condition, [string] $Message) { if (-not $Condition) { throw $Message } } # These stand-ins capture scheduling settings without changing Windows. function New-ScheduledTaskAction { param($Execute, $Argument) [pscustomobject]$PSBoundParameters } function New-ScheduledTaskTrigger { param([switch]$Daily, $At) [pscustomobject]$PSBoundParameters } function New-ScheduledTaskPrincipal { param($UserId, $LogonType, $RunLevel) [pscustomobject]$PSBoundParameters } function New-ScheduledTaskSettingsSet { param([switch]$StartWhenAvailable, [switch]$RunOnlyIfNetworkAvailable, [switch]$AllowStartIfOnBatteries, [switch]$DontStopIfGoingOnBatteries, $MultipleInstances, $ExecutionTimeLimit) [pscustomobject]$PSBoundParameters } function Register-ScheduledTask { param($TaskName, $Action, $Trigger, $Principal, $Settings, [switch]$Force, $Description) $global:AutoUpdateTestTask = [pscustomobject]$PSBoundParameters } function Read-Host { param($Prompt) $global:AutoUpdateTestAnswers.Dequeue() } function Invoke-WebRequest { param($Uri, $OutFile, [switch]$UseBasicParsing) Assert-True ($Uri -in @('https://example.test/scoop/raw/branch/master/scripts/auto-update.ps1', 'https://gitea.medelexis.ch/public/scoop/raw/branch/master/scripts/auto-update.ps1')) 'Unexpected download URL.' if ($global:AutoUpdateTestInvalidDownload) { 'This is not the setup script.' | Set-Content -LiteralPath $OutFile } else { @' param([string] $ScoopRoot) throw 'The downloaded runner must not execute during setup.' '@ | Set-Content -LiteralPath $OutFile } } try { # Spaces in the installation path exercise the scheduled command's quoting. $scoopRoot = Join-Path $fixtureRoot 'scoop with spaces' $shims = Join-Path $scoopRoot 'shims' New-Item -ItemType Directory -Path $shims -Force | Out-Null $scriptPath = Join-Path $fixtureRoot 'auto-update.ps1' Copy-Item -LiteralPath (Join-Path $projectRoot 'scripts\auto-update.ps1') -Destination $scriptPath $env:SCOOP_AUTO_UPDATE_TEST_ROOT = $fixtureRoot $commandLog = Join-Path $fixtureRoot 'commands.log' @' param([Parameter(ValueFromRemainingArguments = $true)][string[]] $ScoopArguments) Add-Content -LiteralPath (Join-Path $env:SCOOP_AUTO_UPDATE_TEST_ROOT 'commands.log') -Value ($ScoopArguments -join ' ') if ($env:SCOOP_AUTO_UPDATE_TEST_FAILURE -eq ($ScoopArguments -join ' ')) { exit 1 } if ($ScoopArguments[0] -eq 'list') { [pscustomobject]@{ Name = 'elexis'; Info = '' } [pscustomobject]@{ Name = 'Elexis-test'; Info = 'Held package' } [pscustomobject]@{ Name = 'myelexis-cli'; Info = '' } [pscustomobject]@{ Name = 'git'; Info = '' } [pscustomobject]@{ Name = 'elexis-global'; Info = 'Global install' } [pscustomobject]@{ Name = 'elexis-broken'; Info = 'Install failed' } } exit 0 '@ | Set-Content -LiteralPath (Join-Path $shims 'scoop.ps1') -Encoding UTF8 & $scriptPath -ScoopRoot $scoopRoot Assert-True ($LASTEXITCODE -eq 0) 'Expected successful update.' Assert-True (((Get-Content $commandLog) -join '|') -eq 'list|hold elexis|hold Elexis-test|update|update *') 'Expected only local elexis* held before updates.' Assert-True (Test-Path (Join-Path $fixtureRoot 'auto-update.log')) 'Expected update log.' foreach ($failure in @('list', 'hold elexis', 'update', 'update *')) { Remove-Item -LiteralPath $commandLog $env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $failure # Expected errors are logged; do not let test ErrorActionPreference terminate the caller. $ErrorActionPreference = 'Continue' & $scriptPath -ScoopRoot $scoopRoot 2>$null $result = $LASTEXITCODE $ErrorActionPreference = 'Stop' Assert-True ($result -eq 1) "Expected failure status for $failure." $commands = @(Get-Content $commandLog) Assert-True ($commands[-1] -eq $failure) "Must abort immediately after $failure." } # Exercise the actual opt-in/download block without running the bucket migration. $tokens = $null $errors = $null $bucketAst = [Management.Automation.Language.Parser]::ParseFile((Join-Path $projectRoot 'scripts\update-bucket.ps1'), [ref]$tokens, [ref]$errors) Assert-True ($errors.Count -eq 0) 'Invalid bucket script syntax.' $optInBlock = [scriptblock]::Create((($bucketAst.EndBlock.Statements | Select-Object -Last 2 | ForEach-Object { $_.Extent.Text }) -join "`n")) $repository = 'https://example.test/scoop' Remove-Item -LiteralPath $commandLog $global:AutoUpdateTestAnswers = [Collections.Generic.Queue[string]]::new() $global:AutoUpdateTestAnswers.Enqueue('n') & $optInBlock $downloadDirectory = Join-Path $scoopRoot 'scripts\medelexis' Assert-True (-not (Test-Path $downloadDirectory)) 'Declining must not download or set up anything.' $global:AutoUpdateTestAnswers.Enqueue('invalid') $global:AutoUpdateTestAnswers.Enqueue('Y') & $optInBlock $downloadedPath = Join-Path $downloadDirectory 'auto-update.ps1' Assert-True (Test-Path $downloadedPath) 'Expected downloaded runner.' $task = $global:AutoUpdateTestTask Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00') 'Expected daily 03:00 schedule.' Assert-True ($task.Settings.StartWhenAvailable -and $task.Settings.RunOnlyIfNetworkAvailable) 'Expected catch-up and network settings.' Assert-True ($task.Settings.MultipleInstances -eq 'IgnoreNew') 'Expected overlap protection.' Assert-True ($task.Principal.LogonType -eq 'Interactive' -and $task.Principal.RunLevel -eq 'Limited') 'Expected normal user session.' Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-NonInteractive')) 'Expected background execution.' Assert-True ($task.Action.Argument.Contains('-File "' + $downloadedPath + '"') -and $task.Action.Argument.Contains('-ScoopRoot "' + $scoopRoot + '"')) 'Expected quoted paths pointing to the downloaded runner.' Assert-True (-not (Test-Path $commandLog)) 'Setup must not execute Scoop.' $taskName = $task.TaskName $global:AutoUpdateTestAnswers.Enqueue('y') & $optInBlock Assert-True ($global:AutoUpdateTestTask.TaskName -eq $taskName -and $global:AutoUpdateTestTask.Force) 'Repeated setup must replace the same task.' $originalDownload = [IO.File]::ReadAllText($downloadedPath) Remove-Variable AutoUpdateTestTask -Scope Global $global:AutoUpdateTestInvalidDownload = $true $global:AutoUpdateTestAnswers.Enqueue('y') $rejected = $false try { & $optInBlock } catch { $rejected = $true } Assert-True $rejected 'Invalid download must be rejected.' Assert-True ([IO.File]::ReadAllText($downloadedPath) -ceq $originalDownload) 'Invalid download must preserve the existing script.' Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Invalid download must not register a task.' Assert-True (@(Get-ChildItem $downloadDirectory -Filter '*.download').Count -eq 0) 'Temporary downloads must be removed.' # The additional standalone installer must offer the same setup without migration. $standaloneSetup = Join-Path $projectRoot 'scripts\setup-auto-update.ps1' $global:AutoUpdateTestAnswers.Enqueue('n') & $standaloneSetup -ScoopRoot $scoopRoot Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Standalone n must not register a task.' $global:AutoUpdateTestInvalidDownload = $false $global:AutoUpdateTestAnswers.Enqueue('y') & $standaloneSetup -ScoopRoot $scoopRoot $task = $global:AutoUpdateTestTask Assert-True ($task.TaskName -eq $taskName -and $task.Force) 'Standalone setup must use the same task.' Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00' -and $task.Settings.StartWhenAvailable) 'Standalone setup must schedule daily 03:00 with catch-up.' Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-File "' + $downloadedPath + '"')) 'Standalone task must invoke the downloaded runner hidden.' Assert-True (-not (Test-Path $commandLog)) 'Standalone setup must not run Scoop or migrations.' Write-Host 'PASS: schedule, repeat setup, hold filtering, command order, logging, failures, opt-in and download setup.' } finally { $env:SCOOP = $oldScoop $env:PATH = $oldPath $env:SCOOP_AUTO_UPDATE_TEST_ROOT = $oldFixture $env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $oldFailure Remove-Variable AutoUpdateTestTask, AutoUpdateTestAnswers, AutoUpdateTestInvalidDownload -Scope Global -ErrorAction SilentlyContinue # Delete only this test's unique directory, after checking its absolute parent. if ((Test-Path -LiteralPath $fixtureRoot) -and [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($fixtureRoot)) -eq [IO.Path]::GetFullPath($PSScriptRoot) -and [IO.Path]::GetFileName($fixtureRoot).StartsWith('.auto-update-test-')) { Remove-Item -LiteralPath $fixtureRoot -Recurse -Force } }