Files
Backup-Dashboard/collectors/vb365/tests/MockVb365.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

80 lines
4.7 KiB
PowerShell

# Stand-ins for the Veeam Backup for Microsoft 365 cmdlets. Object shapes follow
# Get-Vb365CollectorDiagnostics.ps1 output from a real VB365 8.4 server: session and NextRun times
# are local, Statistics.TransferredData is a string ("187.2 MB"), log items carry their status as a
# Title prefix ("[Warning] ..."), and retries are separate sessions whose MainSessionId points at
# the session they retry (which has JobWillBeRetried = True).
#
# . ./tests/MockVb365.ps1
# ./Send-Vb365BackupStatus.ps1 -ConfigPath ./my-test-config.json -DryRun
# Fixed to the top of the hour, and IDs derived from names/times, so repeated runs see the same data
# MOCK_NOW (ISO 8601, set by the test suite) pins the time everything is relative to
$now = if ($env:MOCK_NOW) { [datetime]$env:MOCK_NOW } else { Get-Date -Minute 0 -Second 0 -Millisecond 0 }
function New-StableGuid([string]$Seed) {
[guid]::new([Security.Cryptography.MD5]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($Seed)))
}
function New-MockOrg($Name) { [pscustomobject]@{ Name = $Name; OfficeName = $Name; Type = 'Office365'; IsBackedUp = $true } }
$acme = New-MockOrg 'acme.onmicrosoft.com'
$blue = New-MockOrg 'bluewater.onmicrosoft.com'
function New-MockJob($Name, $Org, $NextRun, [bool]$Enabled = $true) {
[pscustomobject]@{
Id = New-StableGuid $Name; Name = $Name; Organization = $Org; JobType = 'Backup'
IsEnabled = $Enabled; NextRun = $NextRun; LastStatus = 'Success'
LastRun = $(if ($NextRun) { $NextRun.AddDays(-1).ToUniversalTime() }) # UTC, as on the real server
}
}
function New-LogItem($Status, $Text, $At) {
[pscustomobject]@{ Id = [guid]::NewGuid(); USN = 1; Title = "[$Status] $Text"; CreationTime = $At; EndTime = $At }
}
function New-MockSession($Job, $Start, $End, $Status, $Transferred = '0 B', [long]$Objects = 0, $Log = @(),
$MainSessionId = [guid]::Empty, [bool]$WillRetry = $false) {
$logItems = @((New-LogItem 'Success' "Job started at: $($Start.ToString('G'))" $Start)) + @($Log)
[pscustomobject]@{
JobId = $Job.Id; JobName = $Job.Name; Status = $Status
Statistics = [pscustomobject]@{ ProcessingRate = '1.6 MB/s (4 items/s)'; TransferredData = $Transferred; ProcessedObjects = $Objects; Bottleneck = 'Target' }
JobSessionConfigType = 'Incremental'
Id = New-StableGuid "$($Job.Name)|$($Start.Ticks)"
CreationTime = $Start
EndTime = $(if ($End) { $End } else { [datetime]::MinValue })
Progress = 49; MainSessionId = $MainSessionId; JobWillBeRetried = $WillRetry
Log = $logItems
}
}
$global:MockVboJobs = @(
(New-MockJob 'Acme 365 Backup' $acme $now.Date.AddHours(17).AddDays($(if ($now.Hour -ge 17) { 1 } else { 0 }))),
(New-MockJob 'Bluewater 365 Backup' $blue $now.Date.AddHours(17.75).AddDays($(if ($now.Hour -ge 17) { 1 } else { 0 }))),
(New-MockJob 'Bluewater Old Mailboxes' $blue $now.AddDays(1) $false) # disabled
)
$acmeJob, $blueJob, $oldJob = $global:MockVboJobs
$y = $now.Date.AddDays(-1) # yesterday
$blueWarn = New-MockSession $blueJob $y.AddHours(17.75) $y.AddHours(17.75).AddMinutes(3.5) 'Warning' '32.9 MB' 47 @(
(New-LogItem 'Success' 'Found 94 objects' $y.AddHours(17.75)),
# A success line mentioning "error" must not end up in the message
(New-LogItem 'Success' 'Processing site: https://bluewater.sharepoint.com/sites/error-reports' $y.AddHours(17.76)),
(New-LogItem 'Warning' 'Processing mailbox [email protected] finished with warning: Item "Q3.pst" exceeds the maximum size' $y.AddHours(17.78))
) -WillRetry $true
$global:MockVboSessions = @(
(New-MockSession $acmeJob $y.AddHours(17) $y.AddHours(17).AddMinutes(4.8) 'Success' '187.2 MB' 568 @(
(New-LogItem 'Success' 'Found 49 objects' $y.AddHours(17)), (New-LogItem 'Success' 'Connected to organization' $y.AddHours(17)))),
(New-MockSession $acmeJob $now.AddMinutes(-20) $null 'Running' '0 B' 12),
$blueWarn,
# Its retry 13 minutes later, which succeeded
(New-MockSession $blueJob $y.AddHours(17.75).AddMinutes(13.5) $y.AddHours(17.75).AddMinutes(16.8) 'Success' '0 B' 0 -MainSessionId $blueWarn.Id),
(New-MockSession $blueJob $now.AddHours(-30) $now.AddHours(-29.9) 'Failed' '0 B' 0 @(
(New-LogItem 'Failed' 'Failed to connect to organization: The remote server returned an error: (401) Unauthorized.' $now.AddHours(-30)))),
(New-MockSession $oldJob $now.AddDays(-5) $now.AddDays(-5).AddHours(1) 'Success') # outside 48h lookback
)
function Get-VBOJob { $global:MockVboJobs }
function Get-VBOJobSession { param($Job) $global:MockVboSessions | Where-Object { -not $Job -or $_.JobId -eq $Job.Id } }
function Get-VBOOrganization { $acme, $blue }
function Get-VBOCopyJob { } # none, as on the real server
function Get-VBOCopyJobSession { }