_TESTS/PSLocalBase.Reevaluation.Tests.ps1
|
# Independent reevaluation probes. Run in a fresh PowerShell process. # Only in-memory databases are used. Failing assertions are unresolved findings. $ErrorActionPreference = 'Stop' Import-Module (Join-Path (Split-Path $PSScriptRoot -Parent) 'PSLocalBase.psd1') -Force $script:passes = 0 $script:failures = 0 function Test-Independent([string]$Name, [scriptblock]$Body) { $db = New-LocalBase try { & $Body $db $script:passes++ Write-Host "[PASS] $Name" } catch { $script:failures++ Write-Host "[FAIL] ${Name}: $($_.Exception.Message)" } finally { $db.close() } } Test-Independent 'R01a - composite point delete must preserve untargeted documents' { param($db) $db.collection('a').doc('a1').set(@{ n = 1 }) $db.collection('a').doc('a2').set(@{ n = 2 }) $db.collection('b').doc('b1').set(@{ n = 3 }) try { $db.collection('a').doc('a1').union($db.collection('b')).delete() } catch [System.InvalidOperationException] { } if (-not $db.collectionExists('a') -or -not $db.collection('a').doc('a2').exists()) { throw 'Collection a / unrelated document a2 was lost.' } } Test-Independent 'R01b - composite point set must not become a bulk replacement' { param($db) $db.collection('a').doc('a1').set(@{ n = 1 }) $db.collection('a').doc('a2').set(@{ n = 2 }) [void]$db.collection('b').create() try { $db.collection('a').doc('a1').union($db.collection('b')).set(@(@{ n = 9 })) } catch [System.InvalidOperationException] { } if (-not $db.collection('a').doc('a2').exists()) { throw 'Unrelated document a2 was lost through bulk replacement.' } } Test-Independent 'R02 - dropping profile.code must retain UNIQUE profile_code' { param($db) [void]$db.collection('users').createIndex('profile.code') [void]$db.collection('users').createIndex('profile_code', @{ unique = $true }) [void]$db.collection('users').add(@{ profile_code = 'same' }, 'u1') [void]$db.collection('USERS').dropIndex('profile.code') $rejected = $false try { [void]$db.collection('users').add(@{ profile_code = 'same' }, 'u2') } catch [System.InvalidOperationException] { $rejected = $true } if (-not $rejected) { throw 'Dropping a different field removed the UNIQUE constraint; duplicate accepted.' } } Test-Independent 'R03 - a stored index name must never execute additional SQL' { param($db) [void]$db.collection('users').create() $db.collection('victim').doc('v1').set(@{ value = 'keep' }) # Simulates schema supplied by another SQLite producer. The attack is the # subsequent innocent dropIndex call, not this deliberate fixture creation. [void]$db.query('CREATE INDEX "idx_users__email_probe]; DROP TABLE victim; --" ON users (key);') try { [void]$db.collection('users').dropIndex('email') } catch [System.InvalidOperationException] { } if (-not $db.collectionExists('victim')) { throw 'Stored schema name executed DROP TABLE victim.' } } Test-Independent 'R04a - mixed Int64/double equality must not erase a distinct integer' { param($db) $db.collection('items').doc('one').set(@{ ids = @([long]9007199254740993) }) $db.collection('items').doc('one').update(@{ ids = [PSLocalBase]::ArrayRemove([double]9007199254740992) }) $actual = @($db.collection('items').doc('one').get().ids) if ($actual.Count -ne 1 -or $actual[0] -ne [long]9007199254740993) { throw "Distinct integer removed; remaining=[$($actual -join ',')]" } } Test-Independent 'R04b - unsigned 64-bit array union must preserve exact idempotence' { param($db) $value = [uint64]::MaxValue $db.collection('items').doc('one').set(@{ ids = @($value) }) $db.collection('items').doc('one').update(@{ ids = [PSLocalBase]::ArrayUnion($value) }) $actual = @($db.collection('items').doc('one').get().ids) if ($actual.Count -ne 1) { throw "UInt64 union duplicated its member: count=$($actual.Count); value=$($actual -join ',')" } } Test-Independent 'R05 - upgrading UNIQUE through different casing must replace old index' { param($db) [void]$db.collection('users').createIndex('email') [void]$db.collection('USERS').createIndex('email', @{ unique = $true }) [void]$db.collection('users').createIndex('email', @{ unique = $false }) [void]$db.collection('users').add(@{ email = 'same' }, 'a') [void]$db.collection('users').add(@{ email = 'same' }, 'b') } Test-Independent 'R06 - projection must preserve JSON boolean values' { param($db) $db.collection('users').doc('one').set(@{ enabled = $true; disabled = $false }) $actual = $db.collection('users').doc('one').select('enabled', 'disabled').get() if ($actual.enabled -isnot [bool] -or $actual.disabled -isnot [bool]) { throw "Projection converted booleans to $($actual.enabled.GetType().Name)/$($actual.disabled.GetType().Name)." } } Test-Independent 'R07 - collectionGroup doc must scope reads or reject before creating schema' { param($db) $db.collection('audit_a').doc('wanted').set(@{ value = 1 }) $db.collection('audit_a').doc('other').set(@{ value = 2 }) $db.collection('audit_b').doc('wanted').set(@{ value = 3 }) $rejected = $false $actual = @() try { $actual = @($db.collectionGroup('audit_*').doc('wanted').get(@{ keys = $true })) } catch [System.InvalidOperationException] { $rejected = $true } if ($db.collectionExists('audit_*')) { throw "Created literal wildcard table audit_*; point read returned $($actual.Count) rows, keys=[$($actual.key -join ',')]." } if (-not $rejected -and ($actual.Count -ne 2 -or @($actual | Where-Object { $_.key -cne 'wanted' }).Count -ne 0)) { throw "Point-scoped group returned $($actual.Count) rows instead of 2." } } Test-Independent 'R08 - point-scoped aggregation must honor key or reject unsupported scope' { param($db) $db.collection('items').doc('one').set(@{ amount = 10 }) $db.collection('items').doc('two').set(@{ amount = 20 }) try { $actual = $db.collection('items').doc('one').sum('amount') } catch [System.InvalidOperationException] { return } if ($actual -ne 10) { throw "Point-scoped sum returned $actual instead of 10." } } Test-Independent 'C07 - SQL aliasing in populate must preserve a field named doc' { param($db) $db.collection('users').doc('u1').set(@{ name = 'Alice' }) $db.collection('posts').doc('p1').set(@{ user = 'u1'; doc = @{ status = 'ready' } }) $actual = @($db.collection('posts').where('doc.status', '=', 'ready').populate('user', 'users').get()) if ($actual.Count -ne 1) { throw "Expected 1 populated row, got $($actual.Count)." } } Test-Independent 'C01 - rejected ordered delete preserves documents and indexes' { param($db) [void]$db.collection('users').createIndex('name', @{ unique = $true }) $db.collection('users').doc('u1').set(@{ name = 'Alice' }) $before = $db.query("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='users' ORDER BY name;").name -join ',' $rejected = $false try { $db.collection('users').where('name', '=', 'Alice').orderBy('name').delete() } catch [System.InvalidOperationException] { $rejected = $true } $after = $db.query("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='users' ORDER BY name;").name -join ',' if (-not $rejected -or $before -cne $after -or $db.collection('users').count() -ne 1) { throw 'Ordered deletion changed storage or was accepted.' } } Test-Independent 'C02 - explicit identities, GUIDs, NUL validation and failed upserts' { param($db) $keys = [System.Collections.Generic.HashSet[string]]::new() for ($i = 0; $i -lt 100; $i++) { $key = $db.collection('users').upsert(@{ role = 'reader'; n = $i }) if ($key -notmatch '^[0-9a-f]{32}$' -or -not $keys.Add($key)) { throw 'Invalid or repeated generated identity.' } } foreach ($field in @('id', 'key', '_key')) { $payload = @{ name = 'Alice' }; $payload[$field] = $field if ($db.collection('users').upsert($payload) -cne $field) { throw 'Explicit identity was not respected.' } } $rejected = $false try { [void]$db.collection('users').upsert(@{ id = "key`0evil" }) } catch [System.ArgumentException] { $rejected = $true } if (-not $rejected -or $db.collection('users').count() -ne 103) { throw 'NUL identity modified storage.' } } Test-Independent 'C03 - missing composite branches across union and unionAll' { param($db) $db.collection('present').doc('k').set(@{ value = 'safe' }) foreach ($method in @('union', 'unionAll')) { foreach ($terminal in @('get', 'count', 'exists', 'first')) { $left = $db.collection('present').where('value', '=', 'safe') $right = $db.collection('missing').where('value', '=', 'absent') $query = $left.$method($right) $actual = $query.$terminal() if ($null -eq $actual -or ($terminal -eq 'count' -and $actual -ne 1) -or ($terminal -eq 'exists' -and -not $actual)) { throw "Bad result from $method/$terminal" } } } } Test-Independent 'C04 - array-contains-any deletion retains all non-array field types' { 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 = @{ x = 'admin' } }) $db.collection('items').doc('null').set(@{ tags = $null }) $db.collection('items').doc('missing').set(@{ name = 'missing' }) $db.collection('items').where('tags', 'array-contains-any', @('admin')).delete() $keys = @($db.collection('items').get(@{ keys = $true }).key) if ($keys.Count -ne 4 -or $keys -contains 'array') { throw "Incorrect survivors: $($keys -join ',')" } } Test-Independent 'C05 - SQL and identifier injection corpus cannot destroy data' { param($db) $db.collection('safe').doc('one').set(@{ value = 'safe' }) $payloads = @("' OR 1=1 --", "x']; DROP TABLE safe; --", "value`0suffix") foreach ($payload in $payloads) { $db.collection('payloads').doc([Guid]::NewGuid().ToString('N')).set(@{ value = $payload }) if ($db.collection('payloads').where('value', '=', $payload).count() -ne 1) { throw 'Literal did not round-trip.' } $rejected = $false try { [void]$db.collection($payload) } catch [System.ArgumentException] { $rejected = $true } if (-not $rejected) { throw 'Malicious collection name accepted.' } $rejected = $false try { [void]$db.collection('safe').where($payload, '=', 'x') } catch [System.ArgumentException] { $rejected = $true } if (-not $rejected) { throw 'Malicious JSON path accepted.' } } if ($db.collection('safe').count() -ne 1) { throw 'Protected document lost.' } } Test-Independent 'C06 - failed unique write preserves an outer transaction' { param($db) [void]$db.collection('users').createIndex('name', @{ unique = $true }) $db.collection('users').doc('u1').set(@{ name = 'Alice' }) $db.beginTransaction() $db.collection('users').doc('u2').set(@{ name = 'Bob' }) $rejected = $false try { $db.collection('users').doc('u2').update(@{ name = 'Alice' }) } catch [System.InvalidOperationException] { $rejected = $true } if (-not $rejected -or -not $db.InTransaction -or $db.collection('users').doc('u2').get().name -cne 'Bob') { throw 'Failed update corrupted outer transaction.' } $db.rollback() if ($db.collection('users').count() -ne 1) { throw 'Outer rollback did not restore storage.' } } Write-Host "Independent probes: passed=$script:passes; failed=$script:failures" if ($script:failures -gt 0) { exit 1 } |