# 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'*" } } }