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]>
80 lines
4.2 KiB
PowerShell
80 lines
4.2 KiB
PowerShell
# Stand-ins for the MARS agent cmdlets. Object shapes follow Get-MarsCollectorDiagnostics.ps1
|
|
# output from a real agent (2.0.9274): job times are UTC values with no UTC marking, "not set" is
|
|
# 1601-01-01, ErrorInfo is always present (ErrorCode 0 = no error), Datasource is an object, and
|
|
# schedule times are TimeSpans.
|
|
#
|
|
# . ./tests/MockMars.ps1
|
|
# ./Send-MarsBackupStatus.ps1 -ConfigPath ./my-test-config.json -DryRun
|
|
|
|
# Fixed to the top of the hour, and IDs derived from times, so repeated runs see the same jobs
|
|
# 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 }
|
|
$notSet = [datetime]::new(1601, 1, 1)
|
|
function New-StableGuid([string]$Seed) {
|
|
[guid]::new([Security.Cryptography.MD5]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($Seed)))
|
|
}
|
|
|
|
# Local time -> the UTC value MARS reports, with Kind Unspecified like the real objects
|
|
function ConvertTo-MarsTime([datetime]$Local) {
|
|
[datetime]::SpecifyKind($Local.ToUniversalTime(), [DateTimeKind]::Unspecified)
|
|
}
|
|
|
|
function New-MockErrorInfo([int]$Code = 0, $Params = @()) {
|
|
[pscustomobject]@{ DetailedErrorCode = 0; DetailedErrorSource = $null; ErrorCode = $Code; ErrorParamList = $Params }
|
|
}
|
|
|
|
function New-MockDatasource($Name, [uint64]$Done, [uint64]$FailedBytes = 0, [uint64]$FailedFiles = 0, $ErrorInfo = (New-MockErrorInfo)) {
|
|
[pscustomobject]@{
|
|
ByteProgress = [pscustomobject]@{ Changed = $Done; Failed = $FailedBytes; Progress = $Done; Total = 2TB }
|
|
Datasource = [pscustomobject]@{ DataSourceName = $Name; DataSourceId = 6310158806455554328 }
|
|
ErrorInfo = $ErrorInfo
|
|
FileProgress = [pscustomobject]@{ Changed = 100; CurrentFile = ''; Failed = $FailedFiles; Progress = 100; Total = 100 }
|
|
JobState = 'Completed'
|
|
}
|
|
}
|
|
|
|
function New-MockJob($Type, [datetime]$Start, $End, $State, $Datasources = @(), $ErrorInfo = (New-MockErrorInfo), $FailedFileLog = '') {
|
|
$endValue = if ($End) { ConvertTo-MarsTime $End } else { $notSet }
|
|
[pscustomobject]@{
|
|
IsRetried = $false
|
|
JobId = New-StableGuid "$Type|$($Start.Ticks)"
|
|
JobType = $Type
|
|
DisplayTime = $Start # the only local time on the object
|
|
JobStatus = [pscustomobject]@{
|
|
DatasourceStatus = $Datasources
|
|
EndTime = $endValue
|
|
ErrorInfo = $ErrorInfo
|
|
FailedFileLog = $FailedFileLog
|
|
JobState = $State
|
|
StartTime = ConvertTo-MarsTime $Start
|
|
}
|
|
}
|
|
}
|
|
|
|
$global:MockMarsRunning = @(
|
|
(New-MockJob 'Backup' $now.AddMinutes(-10) $null 'InProgress' @((New-MockDatasource 'E:\' 2GB)))
|
|
)
|
|
$global:MockMarsPrevious = @(
|
|
(New-MockJob 'Backup' $now.AddHours(-12) $now.AddHours(-11.5) 'Completed' @((New-MockDatasource 'E:\' 523407872))),
|
|
# Completed, but some files failed
|
|
(New-MockJob 'Backup' $now.AddHours(-36) $now.AddHours(-35) 'Completed' @(
|
|
(New-MockDatasource 'E:\' 3GB 40MB 12)) -FailedFileLog 'C:\Program Files\Microsoft Azure Recovery Services Agent\Temp\Errors.log'),
|
|
(New-MockJob 'Backup' $now.AddHours(-30) $now.AddHours(-29.5) 'Failed' @() `
|
|
(New-MockErrorInfo 100050 @([pscustomobject]@{ Name = 'Volume'; Value = 'E:' }))),
|
|
(New-MockJob 'Recovery' $now.AddHours(-5) $now.AddHours(-4) 'Completed'), # a restore: must be ignored
|
|
(New-MockJob 'Backup' $now.AddDays(-5) $now.AddDays(-5).AddHours(1) 'Completed') # outside 48h lookback
|
|
)
|
|
|
|
# Every day at 21:00 local, as on the real server
|
|
$global:MockMarsSchedule = [pscustomobject]@{
|
|
ScheduleRunDays = [DayOfWeek[]](0..6)
|
|
ScheduleRunTimes = [timespan[]]@([timespan]::FromHours(21))
|
|
ScheduleWeeklyFrequency = 1
|
|
TypeOfBackupSchedule = 'Daily'
|
|
}
|
|
|
|
function Get-OBJob { param([int]$Previous) if ($PSBoundParameters.ContainsKey('Previous')) { $global:MockMarsPrevious } else { $global:MockMarsRunning } }
|
|
function Get-OBPolicy { [pscustomobject]@{ PolicyName = 'policy'; BackupSchedule = $global:MockMarsSchedule } }
|
|
function Get-OBSchedule { param($Policy) $global:MockMarsSchedule }
|
|
function Get-OBSystemStatePolicy { $null } # none configured, as on the real server
|