_TESTS/PSLocalBase.Audit.Tests.ps1

# Targeted regression probes from the security/functionality review.
# Run in a fresh PowerShell process. All databases are in memory.
# These checks describe the desired behavior; failures are unresolved findings.
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path (Split-Path $PSScriptRoot -Parent) 'PSLocalBase.psd1') -Force
$script:failed = 0
$script:passed = 0

function Test-AuditCase([string]$Name, [scriptblock]$Body) {
    $db = New-LocalBase
    try {
        & $Body $db
        $script:passed++
        Write-Host "[PASS] $Name"
    } catch {
        $script:failed++
        Write-Host "[FAIL] ${Name}: $($_.Exception.Message)"
    } finally {
        $db.close()
    }
}

Test-AuditCase 'F01 - orderBy().delete() must reject accidental collection drop' {
    param($db)
    $db.collection('users').doc('u1').set(@{ name = 'Alice' })
    $rejected = $false
    try { $db.collection('users').orderBy('name').delete() } catch { $rejected = $true }
    if (-not $rejected -or -not $db.collectionExists('users')) {
        throw "Rejected=$rejected; collection survives=$($db.collectionExists('users'))"
    }
}

Test-AuditCase 'F02 - upsert without identity must generate distinct keys' {
    param($db)
    $first = $db.collection('users').upsert([pscustomobject][ordered]@{ role = 'reader'; name = 'Alice' })
    $second = $db.collection('users').upsert([pscustomobject][ordered]@{ role = 'reader'; name = 'Bob' })
    $count = $db.collection('users').count()
    if ($first -eq $second -or $count -ne 2) {
        throw "First key=$first; second key=$second; documents=$count (expected 2)"
    }
}

Test-AuditCase 'F03 - ArrayRemove must distinguish adjacent Int64 values above 2^53' {
    param($db)
    $a = [long]9007199254740992
    $b = [long]9007199254740993
    $db.collection('items').doc('one').set(@{ ids = @($a, $b) })
    $db.collection('items').doc('one').update(@{ ids = [PSLocalBase]::ArrayRemove($a) })
    $remaining = @($db.collection('items').doc('one').get().ids)
    if ($remaining.Count -ne 1 -or $remaining[0] -ne $b) {
        throw "Remaining=[$($remaining -join ',')]; expected [$b]"
    }
}

Test-AuditCase 'F04 - dropIndex must resolve collection casing like SQLite' {
    param($db)
    [void]$db.collection('users').createIndex('email', @{ unique = $true })
    [void]$db.collection('USERS').dropIndex('email')
    $indexes = $db.query("SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%';")
    if ($indexes.Count -ne 0) {
        throw "Indexes still present: $($indexes.name -join ',')"
    }
}

Test-AuditCase 'F05 - union with a point lookup must agree between get and count' {
    param($db)
    $db.collection('a').doc('a1').set(@{ n = 1 })
    $db.collection('b').doc('b1').set(@{ n = 2 })
    $actual = @($db.collection('a').doc('a1').union($db.collection('b')).get())
    $count = $db.collection('a').doc('a1').union($db.collection('b')).count()
    if ($actual.Count -ne 2 -or $count -ne 2) {
        throw "get returned $($actual.Count); count returned $count; expected 2 for both"
    }
}

Test-AuditCase 'F06 - union must accept a filtered collection that does not exist' {
    param($db)
    $db.collection('b').doc('b1').set(@{ name = 'Bob' })
    $actual = @($db.collection('missing').where('name', '=', 'Alice').union($db.collection('b')).get())
    if ($actual.Count -ne 1 -or $actual[0].name -ne 'Bob') {
        throw "Expected the surviving branch containing Bob; got $($actual.Count) rows"
    }
}

Test-AuditCase 'F07 - array-contains must exclude scalar and object fields' {
    param($db)
    $db.collection('items').doc('array').set(@{ tags = @('admin') })
    $db.collection('items').doc('scalar').set(@{ tags = 'admin' })
    $db.collection('items').doc('object').set(@{ tags = @{ role = 'admin' } })
    $actual = $db.collection('items').where('tags', 'array-contains', 'admin').get(@{ keys = $true })
    if ($actual.Count -ne 1 -or $actual[0].key -ne 'array') {
        throw "Matched keys=[$($actual.key -join ',')]; expected [array]"
    }
}

Write-Host "Audit regressions: passed=$script:passed; failed=$script:failed"
if ($script:failed -gt 0) { exit 1 }