Files
Backup-Dashboard/collectors/tests/Veeam.Tests.ps1
T
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

128 lines
6.4 KiB
PowerShell

# Veeam B&R collector, against collectors/veeam/tests/MockVeeam.ps1 (shapes from a VBR 13 server).
BeforeAll {
Import-Module (Join-Path $PSScriptRoot 'TestHelpers.psm1') -Force
$now = Set-MockNow
$dash = [char]0x2013 # job names use an en dash, as real ones often do
$fileServer = "File Server $dash Nightly"
$config = @{ ApiUrl = 'http://localhost:1'; ApiKey = 'bdk_x'; Tenant = 'Acme' }
function Get-Runs($Reports, [string]$JobName) {
@($Reports | Where-Object { $_.job.name -eq $JobName -and $_.run } | ForEach-Object run)
}
}
Describe 'Veeam collector' {
Context 'what it reports (dry run)' {
BeforeAll {
$r = Invoke-Collector veeam -Config $config -DryRun
$jobs = @{}
foreach ($report in $r.Reports) { $jobs[$report.job.name] = $report.job }
}
It 'runs cleanly' {
$r.ExitCode | Should -Be 0
$r.Log[-1] | Should -Be '[INFO] Reported 5 jobs, 6 runs, 0 failures'
}
It 'reports every job, under the configured tenant' {
$jobs.Keys | Sort-Object | Should -Be @('Cloud Copy', $fileServer, 'Offsite Copy', 'Old Exchange', "SQL01 $dash Logs")
$r.Reports.tenant | Select-Object -Unique | Should -Be 'Acme'
$r.Reports.source_system | Select-Object -Unique | Should -Be 'Veeam B&R'
}
It "reads the next run from the live schedule (VBR 13's ScheduleOptions property is empty)" {
$jobs[$fileServer].next_run_at | Should -Be (ConvertTo-ApiTime $now.Date.AddDays(1).AddMinutes(15))
$jobs["SQL01 $dash Logs"].next_run_at | Should -Be (ConvertTo-ApiTime $now.AddMinutes(40))
}
It 'gives no next run to immediate-mode copy jobs (their NextRun is stale), disabled or unscheduled jobs' {
$jobs['Cloud Copy'].next_run_at | Should -BeNullOrEmpty
$jobs['Old Exchange'].next_run_at | Should -BeNullOrEmpty
$jobs['Offsite Copy'].next_run_at | Should -BeNullOrEmpty
}
It 'reports a successful session with its times and size' {
$run = Get-Runs $r.Reports $fileServer | Where-Object status -EQ 'success'
$run.started_at | Should -Be (ConvertTo-ApiTime $now.AddDays(-1).AddHours(-2))
$run.finished_at | Should -Be (ConvertTo-ApiTime $now.AddDays(-1).AddHours(-1))
$run.size_bytes | Should -Be 128GB
$run.message | Should -Be 'Job completed successfully.'
$run.details.objects | Should -Be 2
}
It 'reports a session in progress as running, without a finish time' {
$run = Get-Runs $r.Reports $fileServer | Where-Object status -EQ 'running'
$run.started_at | Should -Be (ConvertTo-ApiTime $now.AddMinutes(-20))
$run.PSObject.Properties.Name | Should -Not -Contain 'finished_at'
$run.message | Should -Be ''
}
It 'names the object behind a warning' {
$run = Get-Runs $r.Reports "SQL01 $dash Logs"
$run.status | Should -Be 'warning'
$run.message | Should -BeLike 'SQL01: Warning - Failed to truncate transaction logs*'
$run.details.objects_warning | Should -Be 1
}
It "includes a failure's reason and failed log lines, but not successful ones" {
$run = Get-Runs $r.Reports 'Offsite Copy'
$run.status | Should -Be 'failed'
$run.message | Should -BeLike '*Unable to connect to repository NAS02*'
$run.message | Should -BeLike '*Network path not found (0x80070035)*'
$run.message | Should -Not -BeLike '*Preparing backup chain*'
}
It "files copy jobs' worker sessions under the copy job, naming the source" {
$runs = Get-Runs $r.Reports 'Cloud Copy'
$runs.status | Sort-Object | Should -Be @('success', 'warning')
$runs.details.copied_from | Select-Object -Unique | Should -Be $fileServer
}
It "skips sessions outside the lookback window and from jobs it doesn't know" {
@($r.Reports | Where-Object run).Count | Should -Be 6
Get-Runs $r.Reports 'Old Exchange' | Should -BeNullOrEmpty
}
}
Context 'sending to the API' {
BeforeAll {
$stub = Start-ApiStub
$live = $config.Clone(); $live.ApiUrl = $stub.Url; $live.ApiKey = 'bdk_good'
$first = Invoke-Collector veeam -Config $live
$firstRequests = Get-StubRequests $stub
$second = Invoke-Collector veeam -Config $live -WorkDir $first.WorkDir
$secondRequests = Get-StubRequests $stub
}
AfterAll { Stop-ApiStub $stub }
It 'posts each report to /api/v1/ingest with the API key, as UTF-8 JSON' {
$first.ExitCode | Should -Be 0
$firstRequests.Count | Should -Be 7
$firstRequests.Path | Select-Object -Unique | Should -Be '/api/v1/ingest'
$firstRequests.Auth | Select-Object -Unique | Should -Be 'Bearer bdk_good'
$firstRequests.ContentType | Select-Object -Unique | Should -Be 'application/json; charset=utf-8'
}
It 'keeps non-ASCII job names intact' {
$firstRequests.Payload.job.name | Should -Contain $fileServer
}
It 'remembers finished sessions, but not running ones' {
$first.State.Count | Should -Be 5
}
It 'on the next run, resends only the running session (and refreshes every job)' {
$resent = @($secondRequests | Where-Object { $_.Payload.run })
$resent.Count | Should -Be 1
$resent[0].Payload.run.status | Should -Be 'running'
$secondRequests.Count | Should -Be 5
}
}
Context 'problems' {
It 'stops after the first job when the API key is rejected, exiting 1' {
$stub = Start-ApiStub
try {
$r = Invoke-Collector veeam -Config @{ ApiUrl = $stub.Url; ApiKey = 'bdk_bad'; Tenant = 'Acme' }
(Get-StubRequests $stub).Count | Should -Be 1
$r.ExitCode | Should -Be 1
$r.Log | Should -Contain '[ERROR] API key rejected; check ApiKey in the config. Stopping.'
} finally { Stop-ApiStub $stub }
}
It 'exits 2 when the config lacks a tenant' {
$r = Invoke-Collector veeam -Config @{ ApiUrl = 'http://localhost:1'; ApiKey = 'bdk_x' } -DryRun
$r.ExitCode | Should -Be 2
$r.Log[0] | Should -BeLike "*is missing 'Tenant'*"
}
}
}