Tests/Varibill.SourceCollector.Core.Tests.ps1
|
# Requires Pester 5 Import-Module "$PSScriptRoot/../Varibill.SourceCollector.Core.psm1" -Force Describe "Varibill SourceData Module" { BeforeAll { $script:BaseTemp = Join-Path ([System.IO.Path]::GetTempPath()) "VaribillTests" function New-TestUnrated { param([string]$Path, [int]$MaxRecords = 1000) New-UnratedSourceData -SourceCollectorAPI "https://test.api" ` -TenantKey ([guid]::NewGuid()) ` -SourceCollectorKey ([guid]::NewGuid()) ` -SourceCollectorSecret "secret" ` -SourceCollectorPath $Path ` -MaxRecordsPerFile $MaxRecords } function New-TestRated { param([string]$Path, [int]$MaxRecords = 1000) New-RatedSourceData -SourceCollectorAPI "https://test.api" ` -TenantKey ([guid]::NewGuid()) ` -SourceCollectorKey ([guid]::NewGuid()) ` -SourceCollectorSecret "secret" ` -SourceCollectorPath $Path ` -MaxRecordsPerFile $MaxRecords } } AfterAll { if (Test-Path $script:BaseTemp) { Remove-Item $script:BaseTemp -Recurse -Force } } # Each test gets an isolated temp directory so leftover files never affect counts. BeforeEach { $script:TempPath = Join-Path $script:BaseTemp ([guid]::NewGuid().ToString()) New-Item -Path $script:TempPath -ItemType Directory | Out-Null } AfterEach { if (Test-Path $script:TempPath) { Remove-Item $script:TempPath -Recurse -Force } } Context "UnratedSourceData" { It "can create an instance via constructor" { $u = New-TestUnrated -Path $script:TempPath -MaxRecords 10 $u.GetType().Name | Should -Be 'UnratedSourceData' } It "can create an instance via New-UnratedSourceData function" { $u = New-TestUnrated -Path $script:TempPath $u.GetType().Name | Should -Be 'UnratedSourceData' } It "can add records" { $u = New-TestUnrated -Path $script:TempPath $u.AddUnratedSourceData("Client1","Prod1","Rec1","UID1",(Get-Date),5,"Tag1") $u.Records.Count | Should -Be 1 } It "creates one .rdy file when record count equals MaxRecordsPerFile" { $u = New-TestUnrated -Path $script:TempPath -MaxRecords 3 1..3 | ForEach-Object { $u.AddUnratedSourceData("C$_","P$_","R$_","U$_",(Get-Date),$_,"") } $u.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" $files.Count | Should -Be 1 } It "splits into multiple .rdy files when records exceed MaxRecordsPerFile" { $u = New-TestUnrated -Path $script:TempPath -MaxRecords 2 1..5 | ForEach-Object { $u.AddUnratedSourceData("C$_","P$_","R$_","U$_",(Get-Date),$_,"") } $u.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" $files.Count | Should -Be 3 } It "clears records after SaveData so a second call writes nothing" { $u = New-TestUnrated -Path $script:TempPath $u.AddUnratedSourceData("C1","P1","R1","U1",(Get-Date),1,"") $u.SaveData() $u.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" $files.Count | Should -Be 1 } It "uploads all .rdy files and renames them to .json on success" { $u = New-TestUnrated -Path $script:TempPath -MaxRecords 1 1..3 | ForEach-Object { $u.AddUnratedSourceData("C$_","P$_","R$_","U$_",(Get-Date),$_,"") } Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{"result":"ok"}' } } $u.PostSourceData() $rdyFiles = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" $jsonFiles = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.json" $rdyFiles.Count | Should -Be 0 $jsonFiles.Count | Should -Be 3 } It "keeps .rdy file when server returns non-2xx status" { $u = New-TestUnrated -Path $script:TempPath $u.AddUnratedSourceData("C1","P1","R1","U1",(Get-Date),5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 500; Content = '{"error":"internal"}' } } $u.PostSourceData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" $files.Count | Should -Be 1 } It "throws with the filename when the .rdy file has an empty payload" { $u = New-TestUnrated -Path $script:TempPath $dir = Join-Path $script:TempPath "unrated" New-Item -Path $dir -ItemType Directory -Force | Out-Null $emptyFile = Join-Path $dir "empty_payload.rdy" Set-Content -Path $emptyFile -Value "" -Encoding utf8 { $u.PostSourceData() } | Should -Throw -ExpectedMessage "*empty_payload.rdy*" } It "throws with the filename when the .rdy file contains only whitespace" { $u = New-TestUnrated -Path $script:TempPath $dir = Join-Path $script:TempPath "unrated" New-Item -Path $dir -ItemType Directory -Force | Out-Null $wsFile = Join-Path $dir "whitespace_only.rdy" Set-Content -Path $wsFile -Value " `n " -Encoding utf8 { $u.PostSourceData() } | Should -Throw -ExpectedMessage "*whitespace_only.rdy*" } It "throws with the filename when Invoke-WebRequest fails" { $u = New-TestUnrated -Path $script:TempPath $u.AddUnratedSourceData("C1","P1","R1","U1",(Get-Date),5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { throw [System.Net.Http.HttpRequestException]::new("Connection refused") } { $u.PostSourceData() } | Should -Throw -ExpectedMessage "*Upload failed for file*" } It "SaveData with no records creates no files" { $u = New-TestUnrated -Path $script:TempPath $u.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" $files.Count | Should -Be 0 } It "PostSourceData with no records does not call Invoke-WebRequest" { $u = New-TestUnrated -Path $script:TempPath Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { } $u.PostSourceData() Should -Invoke Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -Times 0 -Exactly } It "saved .rdy file contains correct field names and ISO 8601 date" { $u = New-TestUnrated -Path $script:TempPath $date = [datetime]"2025-06-15T09:30:00" $u.AddUnratedSourceData("ClientA","ProdB","RecC","UID-X",$date,42.5,"MyTag") $u.SaveData() $rdyFile = Get-ChildItem -Path (Join-Path $script:TempPath "unrated") -Filter "*.rdy" | Select-Object -First 1 $record = Get-Content $rdyFile.FullName -Raw | ConvertFrom-Json $rawJson = Get-Content $rdyFile.FullName -Raw $record.ClientIdentifier | Should -Be "ClientA" $record.ProductIdentifier | Should -Be "ProdB" $record.RecordIdentifier | Should -Be "RecC" $record.UniqueIdentifier | Should -Be "UID-X" $record.Quantity | Should -Be 42.5 $record.Tag | Should -Be "MyTag" # Verify ISO 8601 format is preserved in the raw JSON that will be sent to the API $rawJson | Should -Match '"LastSeenDate"\s*:\s*"\d{4}-\d{2}-\d{2}T' } It "writes a response log file to the log directory after a successful upload" { $u = New-TestUnrated -Path $script:TempPath $u.AddUnratedSourceData("C1","P1","R1","U1",(Get-Date),5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{"result":"ok"}' } } $u.PostSourceData() $logFiles = Get-ChildItem -Path (Join-Path $script:TempPath "log") -Filter "*.json" $logFiles.Count | Should -Be 1 } It "posts to the Unrated API endpoint" { $u = New-TestUnrated -Path $script:TempPath $u.AddUnratedSourceData("C1","P1","R1","U1",(Get-Date),5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{}' } } $u.PostSourceData() Should -Invoke Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -Times 1 -Exactly ` -ParameterFilter { $Uri -like "*api/v1/SourceCollectors/Unrated*" } } It "creates a temp directory structure when no SourceCollectorPath is provided" { $tenantKey = [guid]::NewGuid() $collectorKey = [guid]::NewGuid() $u = New-UnratedSourceData -SourceCollectorAPI "https://test.api" ` -TenantKey $tenantKey ` -SourceCollectorKey $collectorKey ` -SourceCollectorSecret "secret" $expectedPath = Join-Path ([System.IO.Path]::GetTempPath()) (Join-Path "Varibill" (Join-Path $tenantKey $collectorKey)) try { Test-Path $expectedPath | Should -BeTrue } finally { Remove-Item (Join-Path ([System.IO.Path]::GetTempPath()) (Join-Path "Varibill" $tenantKey)) -Recurse -Force -ErrorAction SilentlyContinue } } It "sets Host header to localhost when posting to host.docker.internal" { $u = New-UnratedSourceData -SourceCollectorAPI "http://host.docker.internal/" ` -TenantKey ([guid]::NewGuid()) ` -SourceCollectorKey ([guid]::NewGuid()) ` -SourceCollectorSecret "secret" ` -SourceCollectorPath $script:TempPath $u.AddUnratedSourceData("C1","P1","R1","U1",(Get-Date),5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{}' } } $u.PostSourceData() Should -Invoke Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -Times 1 -Exactly ` -ParameterFilter { $Headers['Host'] -eq 'localhost' } } It "removes an existing .json file before renaming .rdy on successful upload" { $u = New-TestUnrated -Path $script:TempPath $dir = Join-Path $script:TempPath "unrated" $rdyFile = Join-Path $dir "collision_test.rdy" $jsonFile = Join-Path $dir "collision_test.json" Set-Content -Path $rdyFile -Value '{"test":"data"}' -Encoding utf8 Set-Content -Path $jsonFile -Value '{"old":"data"}' -Encoding utf8 Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{}' } } $u.PostSourceData() Test-Path $rdyFile | Should -BeFalse Test-Path $jsonFile | Should -BeTrue } } Context "RatedSourceData" { It "can create an instance via constructor" { $r = New-TestRated -Path $script:TempPath -MaxRecords 10 $r.GetType().Name | Should -Be 'RatedSourceData' } It "can create an instance via New-RatedSourceData function" { $r = New-TestRated -Path $script:TempPath $r.GetType().Name | Should -Be 'RatedSourceData' } It "can add records" { $r = New-TestRated -Path $script:TempPath $r.AddRatedSourceData("C1","P1","R1","U1",(Get-Date),10,20,5,"Tag1") $r.Records.Count | Should -Be 1 } It "creates one .rdy file when record count equals MaxRecordsPerFile" { $r = New-TestRated -Path $script:TempPath -MaxRecords 3 1..3 | ForEach-Object { $r.AddRatedSourceData("C$_","P$_","R$_","U$_",(Get-Date),$_,$_ * 2,$_ * 1.5,"") } $r.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" $files.Count | Should -Be 1 } It "splits into multiple .rdy files when records exceed MaxRecordsPerFile" { $r = New-TestRated -Path $script:TempPath -MaxRecords 2 1..5 | ForEach-Object { $r.AddRatedSourceData("C$_","P$_","R$_","U$_",(Get-Date),$_,$_ * 2,$_ * 1.5,"Tag$_") } $r.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" $files.Count | Should -Be 3 } It "clears records after SaveData so a second call writes nothing" { $r = New-TestRated -Path $script:TempPath $r.AddRatedSourceData("C1","P1","R1","U1",(Get-Date),10,20,5,"") $r.SaveData() $r.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" $files.Count | Should -Be 1 } It "uploads all .rdy files and renames them to .json on success" { $r = New-TestRated -Path $script:TempPath -MaxRecords 1 1..3 | ForEach-Object { $r.AddRatedSourceData("C$_","P$_","R$_","U$_",(Get-Date),$_,$_ * 2,$_ * 1.5,"") } Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{"result":"ok"}' } } $r.PostSourceData() $rdyFiles = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" $jsonFiles = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.json" $rdyFiles.Count | Should -Be 0 $jsonFiles.Count | Should -Be 3 } It "keeps .rdy file when server returns non-2xx status" { $r = New-TestRated -Path $script:TempPath $r.AddRatedSourceData("C1","P1","R1","U1",(Get-Date),10,20,5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 400; Content = '{"error":"bad request"}' } } $r.PostSourceData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" $files.Count | Should -Be 1 } It "throws with the filename when the .rdy file has an empty payload" { $r = New-TestRated -Path $script:TempPath $dir = Join-Path $script:TempPath "rated" New-Item -Path $dir -ItemType Directory -Force | Out-Null $emptyFile = Join-Path $dir "empty_payload.rdy" Set-Content -Path $emptyFile -Value "" -Encoding utf8 { $r.PostSourceData() } | Should -Throw -ExpectedMessage "*empty_payload.rdy*" } It "throws with the filename when the .rdy file contains only whitespace" { $r = New-TestRated -Path $script:TempPath $dir = Join-Path $script:TempPath "rated" New-Item -Path $dir -ItemType Directory -Force | Out-Null $wsFile = Join-Path $dir "whitespace_only.rdy" Set-Content -Path $wsFile -Value " `n " -Encoding utf8 { $r.PostSourceData() } | Should -Throw -ExpectedMessage "*whitespace_only.rdy*" } It "throws with the filename when Invoke-WebRequest fails" { $r = New-TestRated -Path $script:TempPath $r.AddRatedSourceData("C1","P1","R1","U1",(Get-Date),10,20,5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { throw [System.Net.Http.HttpRequestException]::new("Connection refused") } { $r.PostSourceData() } | Should -Throw -ExpectedMessage "*Upload failed for file*" } It "SaveData with no records creates no files" { $r = New-TestRated -Path $script:TempPath $r.SaveData() $files = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" $files.Count | Should -Be 0 } It "PostSourceData with no records does not call Invoke-WebRequest" { $r = New-TestRated -Path $script:TempPath Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { } $r.PostSourceData() Should -Invoke Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -Times 0 -Exactly } It "saved .rdy file contains correct field names, ISO 8601 date, and Rated-specific fields" { $r = New-TestRated -Path $script:TempPath $date = [datetime]"2025-06-15T09:30:00" $r.AddRatedSourceData("ClientA","ProdB","RecC","UID-X",$date,10,20.5,5.25,"MyTag") $r.SaveData() $rdyFile = Get-ChildItem -Path (Join-Path $script:TempPath "rated") -Filter "*.rdy" | Select-Object -First 1 $record = Get-Content $rdyFile.FullName -Raw | ConvertFrom-Json $rawJson = Get-Content $rdyFile.FullName -Raw $record.ClientIdentifier | Should -Be "ClientA" $record.ProductIdentifier | Should -Be "ProdB" $record.RecordIdentifier | Should -Be "RecC" $record.UniqueIdentifier | Should -Be "UID-X" $record.Quantity | Should -Be 10 $record.Total | Should -Be 20.5 $record.Cost | Should -Be 5.25 $record.Tag | Should -Be "MyTag" # Verify ISO 8601 format is preserved in the raw JSON that will be sent to the API $rawJson | Should -Match '"LastSeenDate"\s*:\s*"\d{4}-\d{2}-\d{2}T' } It "writes a response log file to the log directory after a successful upload" { $r = New-TestRated -Path $script:TempPath $r.AddRatedSourceData("C1","P1","R1","U1",(Get-Date),10,20,5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{"result":"ok"}' } } $r.PostSourceData() $logFiles = Get-ChildItem -Path (Join-Path $script:TempPath "log") -Filter "*.json" $logFiles.Count | Should -Be 1 } It "posts to the Rated API endpoint" { $r = New-TestRated -Path $script:TempPath $r.AddRatedSourceData("C1","P1","R1","U1",(Get-Date),10,20,5,"") Mock -CommandName Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -MockWith { [PSCustomObject]@{ StatusCode = 200; Content = '{}' } } $r.PostSourceData() Should -Invoke Invoke-WebRequest -ModuleName 'Varibill.SourceCollector.Core' -Times 1 -Exactly ` -ParameterFilter { $Uri -like "*api/v1/SourceCollectors/Rated*" } } } } |