Tests/Private/Search.Tests.ps1
|
BeforeDiscovery { Import-Module (Join-Path $PSScriptRoot '..\..\Use-Onyphe.psd1') -DisableNameChecking -Force } Describe 'Private/Search wrappers' -Tag 'Unit' { InModuleScope 'Use-Onyphe' { BeforeEach { $script:SavedAPIKey = $global:OnypheAPIKey $global:OnypheAPIKey = $null $script:OutFilePath = Join-Path $TestDrive 'search-output.json' } AfterEach { $global:OnypheAPIKey = $script:SavedAPIKey } Context 'Invoke-APIOnypheSearch' { It 'builds the request from -AdvancedSearch criteria' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'datascan' -AdvancedSearch @('product:Apache', 'port:443', 'os:Windows') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($request -eq 'v2/search/') -and ($QueryValue -eq 'category:datascan product:Apache port:443 os:Windows') -and ($APIInfo -eq 'search/datascan') -and ($APIKeyrequired -eq $true) -and (@($APIInput)[0] -eq 'product:Apache port:443 os:Windows') } } It 'builds the request from -SearchValue/-SearchFilter' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($request -eq 'v2/search/') -and ($QueryValue -eq 'category:threatlist country:RU') -and ($APIInfo -eq 'search/threatlist') -and ($APIKeyrequired -eq $true) -and (@($APIInput)[0] -eq 'country:RU') } } It 'passes the OQL query as -QueryValue rather than embedding it in the URL path (so a literal "?" OR-prefix cannot be mistaken for the start of the query string)' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'resolver' -AdvancedSearch @('?domain:a.com', '?domain:b.com') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($request -eq 'v2/search/') -and ($QueryValue -eq 'category:resolver ?domain:a.com ?domain:b.com') } } It 'passes OQLv2 condition-group parentheses through untouched when "(" and ")" are their own -AdvancedSearch elements' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'resolver' -AdvancedSearch @('(', '?domain:a.com', '?domain:b.com', ')', '(', '?tld:fr', ')') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($request -eq 'v2/search/') -and ($QueryValue -eq 'category:resolver ( ?domain:a.com ?domain:b.com ) ( ?tld:fr )') } } It 'documents the known gotcha: an OQLv2 group and a filter:value pair combined in one -AdvancedSearch string element get corrupted by multi-word auto-quoting' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } # "(" and the condition must be separate array elements (see the test above) - combining them # in one element like this is a real, documented footgun, not a supported usage. Invoke-APIOnypheSearch -SearchType 'resolver' -AdvancedSearch @('( domain:a.com )') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { $QueryValue -eq 'category:resolver ( domain:"a.com )"' } } It 'appends a page query parameter when -Page is supplied' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -Page '2' | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { $page -eq '2' } } It 'calls Set-OnypheAPIKey when -APIKey is supplied' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Mock Set-OnypheAPIKey { } Invoke-APIOnypheSearch -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -APIKey ('x' * 40) | Out-Null Should -Invoke Set-OnypheAPIKey -Times 1 -Exactly -ParameterFilter { $APIKey -eq ('x' * 40) } } It 'does not throw when an -AdvancedFilter entry has no colon' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } { Invoke-APIOnypheSearch -SearchType 'datascan' -AdvancedSearch @('port:443') -AdvancedFilter @('exists') } | Should -Not -Throw } It 'passes -Size through to Invoke-OnypheAPIV2 as -size' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -Size 500 | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { $size -eq 500 } } It 'passes -TrackQuery and -Calculated through to Invoke-OnypheAPIV2' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -TrackQuery -Calculated | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($TrackQuery -eq $true) -and ($Calculated -eq $true) } } It 'passes a "!"-prefixed filter name through -AdvancedSearch unchanged (OQL NOT)' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'threatlist' -AdvancedSearch @('category:threatlist', '!country:RU') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { (@($APIInput)[0]) -eq 'category:threatlist !country:RU' } } It 'passes a "?"-prefixed filter name through -AdvancedSearch unchanged (OQL OR)' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'threatlist' -AdvancedSearch @('?country:RU', '?country:CN') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { (@($APIInput)[0]) -eq '?country:RU ?country:CN' } } It 'keeps each -orwildcard condition as its own function occurrence (repeated -AdvancedFilter entries, not comma-packed)' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'resolver' -AdvancedFilter @('orwildcard:domain,g?ogle.com', 'orwildcard:domain,googl?.com') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { (@($APIInput)[0]) -eq '-orwildcard:domain,g?ogle.com -orwildcard:domain,googl?.com' } } It 'quotes the whole value after the first comma when it itself contains a comma and needs quoting (was previously only checking the 2nd segment, dropping quoting for anything after a 2nd comma)' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheSearch -SearchType 'ctl' -AdvancedFilter @('wildcard:organization,foo,bar baz') | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { (@($APIInput)[0]) -eq '-wildcard:organization,"foo,bar baz"' } } } Context 'Invoke-APIOnypheExport' { It 'builds the request from -SearchValue/-SearchFilter and streams to -OutFile' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheExport -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -OutFile $script:OutFilePath | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($request -eq 'v2/export/') -and ($QueryValue -eq 'category:threatlist country:RU') -and ($APIInfo -eq 'export/threatlist') -and ($APIKeyrequired -eq $true) -and ($Stream -eq $true) -and ($OutFile -eq $script:OutFilePath) -and (@($APIInput)[0] -eq 'country:RU') } } It 'throws when -OutFile already exists' { $existingOutFile = Join-Path $TestDrive 'already-there.json' Set-Content -Path $existingOutFile -Value '{}' { Invoke-APIOnypheExport -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -OutFile $existingOutFile } | Should -Throw } It 'does not throw when an -AdvancedFilter entry has no colon' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } { Invoke-APIOnypheExport -SearchType 'datascan' -AdvancedSearch @('port:443') -AdvancedFilter @('exists') -OutFile (Join-Path $TestDrive 'export-advfilter.json') } | Should -Not -Throw } It 'passes -TrackQuery and -Calculated through to Invoke-OnypheAPIV2' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheExport -SearchType 'threatlist' -SearchValue 'RU' -SearchFilter 'country' -TrackQuery -Calculated -OutFile $script:OutFilePath | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($TrackQuery -eq $true) -and ($Calculated -eq $true) } } It 'keeps each -orwildcard condition as its own function occurrence (repeated -AdvancedFilter entries, not comma-packed)' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheExport -SearchType 'resolver' -AdvancedFilter @('orwildcard:domain,g?ogle.com', 'orwildcard:domain,googl?.com') -OutFile $script:OutFilePath | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { (@($APIInput)[0]) -eq '-orwildcard:domain,g?ogle.com -orwildcard:domain,googl?.com' } } It 'passes OQLv2 condition-group parentheses through untouched when "(" and ")" are their own -AdvancedSearch elements' { Mock Invoke-OnypheAPIV2 { [pscustomobject]@{ status = 'ok' } } Invoke-APIOnypheExport -SearchType 'resolver' -AdvancedSearch @('(', '?domain:a.com', '?domain:b.com', ')', '(', '?tld:fr', ')') -OutFile $script:OutFilePath | Out-Null Should -Invoke Invoke-OnypheAPIV2 -Times 1 -Exactly -ParameterFilter { ($request -eq 'v2/export/') -and ($QueryValue -eq 'category:resolver ( ?domain:a.com ?domain:b.com ) ( ?tld:fr )') } } } } } |