Public/Tests/Invoke-TelemetryCollection.Tests.ps1
|
BeforeAll { $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config' $env:TCS_SKIP_UPDATE_CHECK = '1' $env:TCS_TELEMETRY_OPTOUT = '1' $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.core.psd1') -Force } AfterAll { Remove-Module -Name tcs.core -Force -ErrorAction SilentlyContinue } Describe 'Invoke-TelemetryCollection' { BeforeAll { $uri = 'https://telemetry.example.com/ingest/powershell' } BeforeEach { $env:TCS_TELEMETRY_OPTOUT = $null $env:TCS_TELEMETRY_URI = $null Mock -ModuleName tcs.core Send-TelemetryPayload { } } AfterAll { $env:TCS_TELEMETRY_OPTOUT = '1' } It 'Sends one event on End with the measured duration and the ingestion schema' { Invoke-TelemetryCollection -ModuleName 'tcs.x' -ModuleVersion '1.0.0' -CommandName 'Get-X' -ExecutionID 'e1' -Stage Start -URI $uri Start-Sleep -Milliseconds 50 Invoke-TelemetryCollection -ModuleName 'tcs.x' -ModuleVersion '1.0.0' -CommandName 'Get-X' -ExecutionID 'e1' -Stage End -URI $uri -ApiKey 'key' Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { $body = $Body | ConvertFrom-Json $Uri -eq 'https://telemetry.example.com/ingest/powershell' -and $ApiKey -eq 'key' -and $body.command_name -eq 'Get-X' -and $body.module_name -eq 'tcs.x' -and $body.version -eq '1.0.0' -and $body.duration_ms -ge 40 -and $body.success -eq $true -and $body.tags.stage -eq 'End' -and $body.os_platform -and $body.ps_version -and $body.host_name -match '^[0-9a-f-]{36}$' } } It 'Sends only the exception type, never the message, user or path' { $err = [System.IO.IOException]::new('C:\Users\secret\file.txt') Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e2' -Stage End -Failed $true -Exception $err -URI $uri -ModulePath 'C:\Users\secret' Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { $Body -notmatch 'secret' -and ($Body | ConvertFrom-Json).error_type -eq 'System.IO.IOException' -and ($Body | ConvertFrom-Json).success -eq $false -and $Body -notmatch [regex]::Escape([Environment]::UserName) -and $Body -notmatch [regex]::Escape([Environment]::MachineName) } } It 'Accepts an ErrorRecord as the exception' { try { throw [System.InvalidOperationException]::new('boom') } catch { $record = $_ } Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e3' -Stage End -Failed $true -Exception $record -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { ($Body | ConvertFrom-Json).error_type -eq 'System.InvalidOperationException' } } It 'Does not send on Start or In-Progress' { Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e4' -Stage Start -URI $uri Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e4' -Stage 'In-Progress' -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 0 -Exactly } It 'Sends nothing when TCS_TELEMETRY_OPTOUT is set' { $env:TCS_TELEMETRY_OPTOUT = '1' Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e5' -Stage Module-Load -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 0 -Exactly } It 'Sends nothing when the module has telemetry turned off' { InModuleScope tcs.core { $script:ModuleConfigCache['tcs.off'] = @{ Telemetry = $false } } Invoke-TelemetryCollection -ModuleName 'tcs.off' -ExecutionID 'e6' -Stage Module-Load -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 0 -Exactly } It 'Honours Telemetry = $false in the settings file when the module has not loaded its settings' { InModuleScope tcs.core { $script:ModuleConfigCache.Remove('tcs.fileoff'); $script:TelemetryConfigCache.Clear() } $folder = Join-Path -Path $env:TCS_CONFIG_ROOT -ChildPath 'tcs.fileoff' $null = New-Item -Path $folder -ItemType Directory -Force '{ "Telemetry": false }' | Set-Content -Path (Join-Path $folder 'Module.Config.json') Invoke-TelemetryCollection -ModuleName 'tcs.fileoff' -ExecutionID 'e6b' -Stage Module-Load -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 0 -Exactly } It 'Honours a telemetry opt-out made with Set-ModuleConfig in the same session' { Invoke-TelemetryCollection -ModuleName 'tcs.lateoff' -ExecutionID 'e6c' -Stage Module-Load -URI $uri Set-ModuleConfig -ModuleName 'tcs.lateoff' -Telemetry $false Invoke-TelemetryCollection -ModuleName 'tcs.lateoff' -ExecutionID 'e6d' -Stage Module-Load -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly } It 'Uses the endpoint from the settings file when the module has not loaded its settings' { InModuleScope tcs.core { $script:TelemetryConfigCache.Clear() } $folder = Join-Path -Path $env:TCS_CONFIG_ROOT -ChildPath 'tcs.fileuri' $null = New-Item -Path $folder -ItemType Directory -Force '{ "TelemetryUri": "https://file.example.com/ingest" }' | Set-Content -Path (Join-Path $folder 'Module.Config.json') Invoke-TelemetryCollection -ModuleName 'tcs.fileuri' -ExecutionID 'e6e' -Stage Module-Load Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { $Uri -eq 'https://file.example.com/ingest' } } It 'Decrypts a protected API key before sending' { Set-ModuleConfig -ModuleName 'tcs.keysend' -TelemetryApiKey 'SECRET-KEY' -TelemetryUri $uri Invoke-TelemetryCollection -ModuleName 'tcs.keysend' -ExecutionID 'e6f' -Stage Module-Load Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { $ApiKey -eq 'SECRET-KEY' } } It 'Still sends a plain-text API key saved by an earlier version' { InModuleScope tcs.core { $script:TelemetryConfigCache.Clear() } $folder = Join-Path -Path $env:TCS_CONFIG_ROOT -ChildPath 'tcs.plainkey' $null = New-Item -Path $folder -ItemType Directory -Force '{ "TelemetryApiKey": "PLAIN-KEY" }' | Set-Content -Path (Join-Path $folder 'Module.Config.json') Invoke-TelemetryCollection -ModuleName 'tcs.plainkey' -ExecutionID 'e6g' -Stage Module-Load -URI $uri Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { $ApiKey -eq 'PLAIN-KEY' } } It 'Sends nothing when no endpoint is configured' { Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e7' -Stage Module-Load Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 0 -Exactly } It 'Uses TCS_TELEMETRY_URI when no URI is passed' { $env:TCS_TELEMETRY_URI = $uri Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e8' -Stage Module-Load Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 1 -Exactly -ParameterFilter { $Uri -eq 'https://telemetry.example.com/ingest/powershell' } } It 'Refuses non-HTTPS endpoints other than localhost' { Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e9' -Stage Module-Load -URI 'http://telemetry.example.com/ingest' Should -Invoke -ModuleName tcs.core Send-TelemetryPayload -Times 0 -Exactly } It 'Never throws, even if sending fails' { Mock -ModuleName tcs.core Send-TelemetryPayload { throw 'network down' } { Invoke-TelemetryCollection -ModuleName 'tcs.x' -ExecutionID 'e10' -Stage Module-Load -URI $uri } | Should -Not -Throw } It 'Keeps the same installation ID across calls' { $first = InModuleScope tcs.core { Get-TelemetryInstallationId } InModuleScope tcs.core { $script:TelemetryInstallationId = $null } InModuleScope tcs.core { Get-TelemetryInstallationId } | Should -Be $first } } Describe 'Invoke-TelemetryCollection deprecations' { BeforeEach { InModuleScope tcs.core { $script:DeprecationWarningsShown.Clear() } } It 'Warns once per session for <Name>, and keeps working' -ForEach @( @{ Name = '-ModulePath'; Arguments = @{ ModulePath = 'x'; Stage = 'Module-Load' }; Pattern = '-ModulePath is deprecated' } @{ Name = '-Minimal'; Arguments = @{ Minimal = $true; Stage = 'Module-Load' }; Pattern = '-Minimal is deprecated' } @{ Name = 'In-Progress'; Arguments = @{ Stage = 'In-Progress' }; Pattern = "'In-Progress' is deprecated" } ) { $warnings = $null Invoke-TelemetryCollection -ExecutionID 'd1' @Arguments -WarningVariable warnings -WarningAction SilentlyContinue Invoke-TelemetryCollection -ExecutionID 'd2' @Arguments -WarningVariable +warnings -WarningAction SilentlyContinue @($warnings).Count | Should -Be 1 [string]$warnings[0] | Should -Match $Pattern [string]$warnings[0] | Should -Match '1\.0' } It 'Does not warn for current usage' { $warnings = $null Invoke-TelemetryCollection -ExecutionID 'd3' -Stage Start -WarningVariable warnings -WarningAction SilentlyContinue Invoke-TelemetryCollection -ExecutionID 'd3' -Stage End -WarningVariable +warnings -WarningAction SilentlyContinue @($warnings).Count | Should -Be 0 } } |