# Pester 3.4 (ships with Windows 10): powershell -NoProfile -Command "Invoke-Pester windows\tests" $here = Split-Path -Parent $MyInvocation.MyCommand.Path . (Join-Path $here '..\common.ps1') $repo = Resolve-Path (Join-Path $here '..\..') Describe 'settings.txt' { $f = Join-Path $TestDrive 'settings.txt' It 'falls back to the defaults when the file is missing' { (Read-Settings $f).CS_PORT | Should Be '27015' } It 'replaces a key, keeps comments, appends new keys, strips double quotes' { [IO.File]::WriteAllText($f, "# my server`r`nCS_HOSTNAME=old name`r`nCS_BOTS=6`r`n") Write-Setting $f 'CS_HOSTNAME' 'My "cool" server' Write-Setting $f 'CS_PUBLIC_ADDR' 'x.ply.gg:123' $s = Read-Settings $f $s.CS_HOSTNAME | Should Be 'My cool server' $s.CS_BOTS | Should Be '6' $s.CS_PUBLIC_ADDR | Should Be 'x.ply.gg:123' ([IO.File]::ReadAllLines($f))[0] | Should Be '# my server' } It 'keeps = signs inside values' { Write-Setting $f 'CS_PASSWORD' 'a=b' (Read-Settings $f).CS_PASSWORD | Should Be 'a=b' } } Describe 'install folder' { It 'uses Documents\cs16 normally, spaces included' { $r = Resolve-InstallDir 'C:\Users\John Smith\Documents' 'C:\Users\John Smith' @('C:\Users\John Smith\OneDrive') $r.Path | Should Be 'C:\Users\John Smith\Documents\cs16' $r.Reason | Should Be '' } It 'avoids a OneDrive-synced Documents folder' { $r = Resolve-InstallDir 'C:\Users\jo\OneDrive\Documents' 'C:\Users\jo' @('C:\Users\jo\OneDrive', $null) $r.Path | Should Be 'C:\Users\jo\cs16' $r.Reason | Should Match 'OneDrive' } It 'falls back to C:\cs16 for non-ASCII user folders' { $r = Resolve-InstallDir ('C:\Users\Zo' + [char]0xEB + '\Documents') ('C:\Users\Zo' + [char]0xEB) @() $r.Path | Should Be 'C:\cs16' } It 'refuses relative, non-ASCII and Program Files paths' { Test-InstallDir 'cs16' | Should Not BeNullOrEmpty Test-InstallDir ('C:\J' + [char]0xF6 + 'rg\cs16') | Should Not BeNullOrEmpty Test-InstallDir (Join-Path $env:ProgramFiles 'cs16') | Should Not BeNullOrEmpty Test-InstallDir 'D:\Games\cs 16' | Should BeNullOrEmpty } } Describe 'pinned downloads' { $all = Get-PinnedDownloads $repo It 'drops the Linux-only files' { @($all | Where-Object { $_.File -match '-linux' }).Count | Should Be 0 } It 'adds the Windows builds of AMX Mod X and YaPB' { @($all | Where-Object { $_.File -eq 'amxmodx-1.10.0-git5486-base-windows.zip' }).Count | Should Be 1 @($all | Where-Object { $_.File -eq 'amxmodx-1.10.0-git5486-cstrike-windows.zip' }).Count | Should Be 1 @($all | Where-Object { $_.File -eq 'yapb-4.4.957-windows.zip' }).Count | Should Be 1 } It 'keeps the shared files with lower-case hashes' { $r = $all | Where-Object { $_.File -like 'rehlds-bin-*' } $r.Sha | Should Match '^[0-9a-f]{64}$' @($all | Where-Object { $_.File -like 'spawns/*' }).Count | Should BeGreaterThan 5 } It 'refuses a file whose hash does not match' { $src = Join-Path $TestDrive 'src.bin'; [IO.File]::WriteAllText($src, 'hello') $item = [pscustomobject]@{ File = 'x/bad.bin'; Sha = ('0' * 64); Url = ([Uri]$src).AbsoluteUri } { Get-Pinned $item (Join-Path $TestDrive 'dl') } | Should Throw 'checksum mismatch' Test-Path (Join-Path $TestDrive 'dl\x\bad.bin') | Should Be $false } It 'gives every file a backup URL on the Gitea mirror' { ($all | Where-Object { $_.File -eq 'spawns/de_dust2.spawns.cfg' }).Mirror | Should Match '/releases/download/mods/de_dust2\.spawns\.cfg$' } It 'uses the backup copy when the main site fails or sends the wrong file' { $good = Join-Path $TestDrive 'mirror.bin'; [IO.File]::WriteAllText($good, 'real file') $wrong = Join-Path $TestDrive 'blocked.html'; [IO.File]::WriteAllText($wrong, 'blocked') foreach ($main in @((Join-Path $TestDrive 'missing.bin'), $wrong)) { $item = [pscustomobject]@{ File = 'm/f.bin'; Sha = (Get-Sha256 $good); Url = ([Uri]$main).AbsoluteUri; Mirror = ([Uri]$good).AbsoluteUri } $dl = Join-Path $TestDrive ('dl' + [guid]::NewGuid()) [IO.File]::ReadAllText((Get-Pinned $item $dl)) | Should Be 'real file' } } It 'keeps a file whose hash matches' { $src = Join-Path $TestDrive 'src.bin' $item = [pscustomobject]@{ File = 'x/good.bin'; Sha = (Get-Sha256 $src); Url = ([Uri]$src).AbsoluteUri } $p = Get-Pinned $item (Join-Path $TestDrive 'dl') [IO.File]::ReadAllText($p) | Should Be 'hello' } } Describe 'spawn files' { It 'pads old 6-field lines to 10 fields' { Convert-SpawnLine '-1024 512 64 0 90 0' | Should Be '-1024 512 64 0 90 0 0 0 0 0' } It 'leaves 10-field lines alone' { Convert-SpawnLine '1 2 3 4 5 6 7 8 9 10' | Should Be '1 2 3 4 5 6 7 8 9 10' } } Describe 'zip extraction' { It 'extracts only the entries under the prefix, without it' { $src = Join-Path $TestDrive 'z'; New-Item -ItemType Directory -Force "$src\bin\win32\sub", "$src\bin\linux32" | Out-Null 'a' | Set-Content "$src\bin\win32\sub\a.txt"; 'b' | Set-Content "$src\bin\linux32\b.txt" $zip = Join-Path $TestDrive 'z.zip' [IO.Compression.ZipFile]::CreateFromDirectory($src, $zip) $out = Join-Path $TestDrive 'out' Expand-ZipPart $zip 'bin/win32/' $out Test-Path "$out\sub\a.txt" | Should Be $true Test-Path "$out\b.txt" | Should Be $false { Expand-ZipPart $zip 'nope/' $out } | Should Throw } } Describe 'A2S_INFO reply' { It 'reads name, map and player counts' { $b = New-Object Collections.Generic.List[byte] $b.AddRange([byte[]](255, 255, 255, 255, 0x49, 48)) foreach ($s in 'My server', 'de_dust2', 'cstrike', 'Counter-Strike') { $b.AddRange([Text.Encoding]::UTF8.GetBytes($s)); $b.Add(0) } $b.AddRange([byte[]](10, 0, 3, 16, 2, 100, 119, 1, 1)) # appid, players, max, bots, type, env, password, vac $i = ConvertFrom-A2SInfo $b.ToArray() $i.Name | Should Be 'My server' $i.Map | Should Be 'de_dust2' $i.Players | Should Be 3 $i.Max | Should Be 16 $i.Bots | Should Be 2 $i.Password | Should Be $true } It 'returns nothing for another packet type' { ConvertFrom-A2SInfo ([byte[]](255, 255, 255, 255, 0x41, 1, 2, 3, 4)) | Should BeNullOrEmpty } } Describe 'kill counter' { $lines = @( 'L 10/15/2026 - 20:01:02: "Helios<3>" killed "articwolf<4>" with "ak47"', 'L 10/15/2026 - 20:01:05: "a "quoted" <5>" killed "Helios<3>" with "deagle"', ('L 10/16/2026 - 00:00:01: "Zo' + [char]0xEB + '<6><>" killed "x<7>" with "knife"'), 'L 10/15/2026 - 20:01:09: "Helios<3>" triggered "Planted_The_Bomb"' ) It 'finds every kill line, odd names included' { $k = @(Get-KillLines $lines) $k.Count | Should Be 3 $k[0].Killer | Should Be 'Helios' $k[1].Killer | Should Be 'a "quoted" ' $k[1].Weapon | Should Be 'deagle' $k[2].Day | Should Be '10/16/2026' } It 'tallies logs and keeps counts of deleted logs' { $logs = Join-Path $TestDrive 'logs'; New-Item -ItemType Directory -Force $logs | Out-Null [IO.File]::WriteAllLines("$logs\L1015000.log", [string[]]$lines, (New-Object Text.UTF8Encoding $false)) $state = Join-Path $TestDrive 'kills-tally.json' $t = Update-KillTally $logs $state $t.Total | Should Be 3 $t.Who['Helios'] | Should Be 1 $t.Days.Count | Should Be 2 Remove-Item "$logs\L1015000.log" [IO.File]::WriteAllLines("$logs\L1016000.log", [string[]]@($lines[0])) $t = Update-KillTally $logs $state $t.Total | Should Be 4 $t.Who['Helios'] | Should Be 2 $t.Maps | Should Be 2 } It 'reads the log the server is still writing' { $logs = Join-Path $TestDrive 'live'; New-Item -ItemType Directory -Force $logs | Out-Null $w = New-Object IO.FileStream "$logs\L1017000.log", ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::Read) try { $b = [Text.Encoding]::UTF8.GetBytes($lines[0] + "`n"); $w.Write($b, 0, $b.Length); $w.Flush() (Update-KillTally $logs (Join-Path $TestDrive 'live.json')).Total | Should Be 1 } finally { $w.Dispose() } } } Describe 'network' { It 'recognises the Tailscale address range' { Test-CgnatAddress '100.118.97.33' | Should Be $true Test-CgnatAddress '100.12.1.1' | Should Be $false Test-CgnatAddress '192.168.1.5' | Should Be $false } }