# Veeam Backup for Microsoft 365 collector, against collectors/vb365/tests/MockVb365.ps1 (shapes from # a VB365 8.4 server: log items carry their status as a Title prefix, retries are separate sessions). BeforeAll { Import-Module (Join-Path $PSScriptRoot 'TestHelpers.psm1') -Force $now = Set-MockNow $base = @{ ApiUrl = 'http://localhost:1'; ApiKey = 'bdk_x' } function Get-JobRuns($Reports, [string]$JobName) { @($Reports | Where-Object { $_.job.name -eq $JobName -and $_.run } | ForEach-Object run) } function Get-Tenants($Reports) { $map = @{} foreach ($report in $Reports) { $map[$report.job.name] = $report.tenant } return $map } } Describe 'VB365 collector' { Context 'what it reports (dry run)' { BeforeAll { $r = Invoke-Collector vb365 -Config ($base + @{ Tenant = 'Default Co' }) -DryRun $acme = Get-JobRuns $r.Reports 'Acme 365 Backup' $blue = Get-JobRuns $r.Reports 'Bluewater 365 Backup' } It 'runs cleanly, reporting every job' { $r.ExitCode | Should -Be 0 $r.Log[-1] | Should -Be '[INFO] Reported 3 jobs, 5 runs, 0 failures' $r.Reports.source_system | Select-Object -Unique | Should -Be 'Veeam Backup for M365' } It "uses each job's NextRun, and none for a disabled job" { $next = $now.Date.AddHours(17).AddDays($(if ($now.Hour -ge 17) { 1 } else { 0 })) ($r.Reports | Where-Object { $_.job.name -eq 'Acme 365 Backup' })[0].job.next_run_at | Should -Be (ConvertTo-ApiTime $next) ($r.Reports | Where-Object { $_.job.name -eq 'Bluewater Old Mailboxes' })[0].job.next_run_at | Should -BeNullOrEmpty } It 'reports a successful session with its size ("187.2 MB" in bytes) and objects' { $run = $acme | Where-Object status -EQ 'success' $run.started_at | Should -Be (ConvertTo-ApiTime $now.Date.AddDays(-1).AddHours(17)) $run.size_bytes | Should -Be ([long](187.2 * 1MB)) $run.details.processed_objects | Should -Be 568 $run.details.organization | Should -Be 'acme.onmicrosoft.com' $run.message | Should -Be 'Job completed successfully.' } It 'reports a session in progress as running, without a finish time' { $run = $acme | Where-Object status -EQ 'running' $run.PSObject.Properties.Name | Should -Not -Contain 'finished_at' $run.message | Should -Be '' } It 'builds the message from [Warning]/[Failed] log items, without the prefix' { $run = $blue | Where-Object status -EQ 'warning' $run.message | Should -BeLike 'Processing mailbox reception@bluewater.ca finished with warning: Item "Q3.pst" exceeds the maximum size*' # Literal check: in -like patterns, [Warning] is a character set $run.message.Contains('[Warning]') | Should -BeFalse } It 'ignores [Success] log items, even ones mentioning "error"' { ($blue | Where-Object status -EQ 'warning').message | Should -Not -BeLike '*error-reports*' } It 'flags a session VB365 will retry, and links the retry back to it' { $warning = $blue | Where-Object status -EQ 'warning' $retry = $blue | Where-Object { $_.details.retry_of } $warning.message | Should -BeLike '*Veeam will retry this job.' $warning.details.will_retry | Should -BeTrue $retry.status | Should -Be 'success' $retry.details.retry_of | Should -Be $warning.external_id } It 'reports a failed session with its error' { $run = $blue | Where-Object status -EQ 'failed' $run.message | Should -BeLike '*(401) Unauthorized*' } It 'skips sessions outside the lookback window' { Get-JobRuns $r.Reports 'Bluewater Old Mailboxes' | Should -BeNullOrEmpty } } Context 'which company a job appears under' { It 'uses OrganizationTenants (not case-sensitive), then Tenant for unmapped organizations' { $r = Invoke-Collector vb365 -DryRun -Config ($base + @{ Tenant = 'Default Co'; OrganizationTenants = @{ 'ACME.onmicrosoft.com' = 'Acme Corp' } }) $tenants = Get-Tenants $r.Reports $tenants['Acme 365 Backup'] | Should -Be 'Acme Corp' $tenants['Bluewater 365 Backup'] | Should -Be 'Default Co' } It "falls back to the organization's name with no mapping and no Tenant" { $r = Invoke-Collector vb365 -Config $base -DryRun $r.ExitCode | Should -Be 0 $tenants = Get-Tenants $r.Reports $tenants['Acme 365 Backup'] | Should -Be 'acme.onmicrosoft.com' $tenants['Bluewater 365 Backup'] | Should -Be 'bluewater.onmicrosoft.com' } } Context 'sending to the API' { BeforeAll { $stub = Start-ApiStub $live = $base + @{ Tenant = 'Default Co' }; $live.ApiUrl = $stub.Url $first = Invoke-Collector vb365 -Config $live $firstRequests = Get-StubRequests $stub $second = Invoke-Collector vb365 -Config $live -WorkDir $first.WorkDir $secondRequests = Get-StubRequests $stub } AfterAll { Stop-ApiStub $stub } It 'sends every session, remembering the finished ones' { $first.ExitCode | Should -Be 0 @($firstRequests | Where-Object { $_.Payload.run }).Count | Should -Be 5 $first.State.Count | Should -Be 4 } 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 3 } } }