Files
Backup-Dashboard/collectors/tests/Mars.Tests.ps1
kyleandClaude Opus 5.5 c5fe69a813 Add a Pester test suite for the collectors
84 tests in collectors/tests, run with Invoke-CollectorTests.ps1 (~15 s,
PowerShell 7.2+ and Pester 5.5+):

- Module.Tests: the shared module (property lookup, time format,
  config/lookback, sent-run state and pruning, dry runs, exit codes,
  401 detection)
- Veeam/Mars/Wsb/Vb365.Tests: each collector run in its own process
  against its mock and a stand-in ingest API, asserting the behaviours
  confirmed on real servers: VBR 13 live schedule, copy jobs, MARS UTC
  times and ErrorCode 0, WSB culture-formatted times and nested VMs,
  VB365 organization mapping, log prefixes and retries, state and
  resending, rejected keys
- Installers.Tests: config, files and scheduled task from each
  installer, with stand-ins for icacls / Task Scheduler / the key prompt

Collectors run under the test runner's PowerShell, or another shell via
-CollectorShell (e.g. powershell.exe, to test under Windows PowerShell
5.1 like the MARS, WSB and VB365 servers). The mocks take their "now"
from MOCK_NOW so expectations can't drift with the clock; the WSB mock
gains -RunningJobFinished for the running-to-finished case.

Each of 12 past bugs, put back one at a time, fails the test for its
behaviour (and the runner exits 1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:36:27 -06:00

96 lines
4.6 KiB
PowerShell

# Azure Backup (MARS agent) collector, against collectors/mars/tests/MockMars.ps1 (shapes from a
# real agent, 2.0.9274: job times are UTC values with no UTC marking, ErrorInfo is always present).
BeforeAll {
Import-Module (Join-Path $PSScriptRoot 'TestHelpers.psm1') -Force
$now = Set-MockNow
$config = @{ ApiUrl = 'http://localhost:1'; ApiKey = 'bdk_x'; Tenant = 'Acme' }
# MARS reports job times in UTC; the API should get them with +00:00
function Utc([datetime]$Local) { ConvertTo-ApiTime $Local.ToUniversalTime() }
}
Describe 'MARS collector' {
Context 'what it reports (dry run)' {
BeforeAll {
$r = Invoke-Collector mars -Config $config -DryRun
$runs = @($r.Reports | Where-Object run | ForEach-Object run)
$byStart = @{}
foreach ($run in $runs) { $byStart[$run.started_at] = $run }
}
It 'runs cleanly' {
$r.ExitCode | Should -Be 0
$r.Log[-1] | Should -Be '[INFO] Reported 4 runs, 0 failures'
}
It 'reports the server as one job, named after it' {
$r.Reports.job.external_id | Select-Object -Unique | Should -Be 'mars:srv01'
$r.Reports.job.name | Select-Object -Unique | Should -Be 'SRV01 - Azure Backup'
$r.Reports.source_system | Select-Object -Unique | Should -Be 'Azure Backup (MARS)'
}
It 'computes the next run from the schedule (daily at 21:00)' {
$next = (Get-Date).Date.AddHours(21)
if ((Get-Date) -ge $next) { $next = $next.AddDays(1) }
$r.Reports[0].job.next_run_at | Should -Be (ConvertTo-ApiTime $next)
}
It 'treats job times as UTC (MARS gives UTC values without marking them)' {
$byStart.Keys | Should -Contain (Utc $now.AddHours(-12))
$byStart[(Utc $now.AddHours(-12))].finished_at | Should -Be (Utc $now.AddHours(-11.5))
}
It 'reports a completed backup as success, despite its (always present) ErrorInfo with code 0' {
$run = $byStart[(Utc $now.AddHours(-12))]
$run.status | Should -Be 'success'
$run.message | Should -Be 'Backup completed successfully.'
$run.size_bytes | Should -Be 523407872
$run.details.datasources | Should -Be @('E:\')
}
It 'reports a completed backup with failed files as a warning, with the count and log' {
$run = $byStart[(Utc $now.AddHours(-36))]
$run.status | Should -Be 'warning'
$run.message | Should -BeLike '*E:\: 12 file(s) failed to back up*'
$run.message | Should -BeLike '*Failed file log: C:\Program Files\Microsoft Azure Recovery Services Agent\Temp\Errors.log*'
$run.details.failed_files | Should -Be 12
$run.details.failed_bytes | Should -Be 40MB
}
It 'reports a failed backup with its error code and parameters' {
$run = $byStart[(Utc $now.AddHours(-30))]
$run.status | Should -Be 'failed'
$run.message | Should -BeLike '*Error code 100050 (Volume=E:)*'
}
It 'reports a backup in progress as running, without a finish time' {
$run = $byStart[(Utc $now.AddMinutes(-10))]
$run.status | Should -Be 'running'
$run.PSObject.Properties.Name | Should -Not -Contain 'finished_at'
}
It 'ignores restores and backups outside the lookback window' {
$runs.Count | Should -Be 4
}
It 'uses JobName from the config when set' {
$named = Invoke-Collector mars -Config ($config + @{ JobName = 'File Server' }) -DryRun
$named.Reports.job.name | Select-Object -Unique | Should -Be 'File Server'
$named.Reports.job.external_id | Select-Object -Unique | Should -Be 'mars:srv01'
}
}
Context 'sending to the API' {
BeforeAll {
$stub = Start-ApiStub
$live = $config.Clone(); $live.ApiUrl = $stub.Url
$first = Invoke-Collector mars -Config $live
$firstRequests = Get-StubRequests $stub
$second = Invoke-Collector mars -Config $live -WorkDir $first.WorkDir
$secondRequests = Get-StubRequests $stub
}
AfterAll { Stop-ApiStub $stub }
It 'sends every run on the first run' {
$first.ExitCode | Should -Be 0
$firstRequests.Count | Should -Be 4
}
It 'on the next run, resends only the backup in progress' {
$first.State.Count | Should -Be 3
$secondRequests.Count | Should -Be 1
$secondRequests[0].Payload.run.status | Should -Be 'running'
}
}
}