_TESTS/PSLocalBase.PrepareIsolation.Tests.ps1
|
$ErrorActionPreference='Stop' Import-Module (Join-Path (Split-Path $PSScriptRoot -Parent) 'PSLocalBase.psd1') -Force $passed=0; $failed=0 function Assert-True($condition,$message) { if(-not $condition){throw $message} } function Test-Case($name,[scriptblock]$action) { $db=New-LocalBase try { & $action $db; $script:passed++; Write-Host "[PASS] $name" } catch {$script:failed++; Write-Host "[FAIL] $name : $($_.Exception.Message)"} finally {$db.close()} } foreach($pragma in @('trusted_schema','ignore_check_constraints','writable_schema')) { foreach($position in @('first','last')) { Test-Case "$pragma $position" { param($db) $null=$db.query("PRAGMA $pragma=OFF;") $sql=if($position -eq 'first'){"PRAGMA $pragma=ON; SELECT 1;"}else{"SELECT 1; PRAGMA $pragma=ON;"} $rejected=$false try {$null=$db.query($sql)}catch [System.InvalidOperationException] {$rejected=$true} Assert-True $rejected 'Compound SQL accepted' Assert-True (($db.query("PRAGMA $pragma;")).$pragma -eq 0) 'Rejected SQL changed pragma' } } } foreach($position in @('first','last')) { Test-Case "CHECK survives $position" { param($db) $null=$db.query("CREATE TABLE bounded(key TEXT PRIMARY KEY,doc TEXT CHECK(json_extract(doc,'$.n')>0));") $db.collection('bounded').doc('a').set(@{n=1}) $sql=if($position -eq 'first'){'PRAGMA ignore_check_constraints=ON; SELECT 1;'}else{'SELECT 1; PRAGMA ignore_check_constraints=ON;'} $rejected=$false try {$null=$db.query($sql)}catch [System.InvalidOperationException] {$rejected=$true} Assert-True $rejected 'Compound accepted' $blocked=$false try {$db.collection('bounded').doc('a').set(@{n=-1})}catch [System.InvalidOperationException] {$blocked=$true} Assert-True $blocked 'CHECK disabled after rejected SQL' Assert-True ($db.collection('bounded').doc('a').get().n -eq 1) 'Document changed' } } Test-Case 'Quoted tokens and comments' { param($db) $r=$db.query("/* ; PRAGMA trusted_schema=ON; */ SELECT 'a;''b' AS [semi;colon]; -- ; SELECT 2;") Assert-True ($r.'semi;colon' -ceq "a;'b") 'Quoted content changed' $r=$db.query('SELECT 7 AS "a;b", 8 AS `c;d`; /* tail ; */') Assert-True ($r.'a;b' -eq 7 -and $r.'c;d' -eq 8) 'Quoted identifiers changed' } Test-Case 'Trigger body remains one statement' { param($db) $null=$db.query('CREATE TABLE src(n INTEGER);') $null=$db.query('CREATE TABLE dst(n INTEGER);') $null=$db.query("CREATE TEMP TRIGGER audit_insert AFTER INSERT ON src BEGIN INSERT INTO dst VALUES(NEW.n); INSERT INTO dst VALUES(CASE WHEN NEW.n>0 THEN 99 ELSE 0 END); END; -- tail") $null=$db.query('INSERT INTO src VALUES(1);') Assert-True (@($db.query('SELECT n FROM dst ORDER BY n;')).Count -eq 2) 'Trigger did not execute both statements' $blocked=$false try {$null=$db.query('CREATE TRIGGER bad AFTER INSERT ON src BEGIN SELECT 1; END; PRAGMA trusted_schema=ON;')}catch [System.InvalidOperationException] {$blocked=$true} Assert-True $blocked 'Trigger plus pragma accepted' Assert-True ($db.query('PRAGMA trusted_schema;').trusted_schema -eq 0) 'Trigger tail changed pragma' Assert-True (@($db.query("SELECT name FROM sqlite_master WHERE name='bad';")).Count -eq 0) 'Rejected trigger created' } Test-Case 'Single pragma and NUL contracts' { param($db) $null=$db.query('PRAGMA trusted_schema=ON;') Assert-True ($db.query('PRAGMA trusted_schema;').trusted_schema -eq 1) 'Single pragma lost effect' $blocked=$false try {$null=$db.query("SELECT 1;`0SELECT 2;")}catch [System.InvalidOperationException] {$blocked=$true} Assert-True $blocked 'NUL SQL accepted' Assert-True ($db.query('SELECT ? AS value;',@("a`0b")).value -ceq "a`0b") 'NUL parameter changed' } foreach($tail in @('INSERT INTO items VALUES(2);','UPDATE items SET n=2;','DELETE FROM items;','DROP TABLE items;','CREATE TABLE unexpected(n);')) { Test-Case "DML/DDL $tail" { param($db) $null=$db.query('CREATE TABLE items(n INTEGER);') $null=$db.query('INSERT INTO items VALUES(1);') $blocked=$false try {$null=$db.query("SELECT 1; $tail")}catch [System.InvalidOperationException] {$blocked=$true} Assert-True $blocked 'Compound SQL accepted' $rows=@($db.query('SELECT n FROM items;')) Assert-True ($rows.Count -eq 1 -and $rows[0].n -eq 1) 'Rows changed' Assert-True (@($db.query("SELECT name FROM sqlite_master WHERE name='unexpected';")).Count -eq 0) 'Table created' } } Test-Case 'Repeated rejection releases native memory' { param($db) Add-Type 'using System.Runtime.InteropServices; public class PrepareMemory { [DllImport("winsqlite3.dll",EntryPoint="sqlite3_memory_used",CallingConvention=CallingConvention.Cdecl)] public static extern long Used(); }' try {$null=$db.query('SELECT 1; PRAGMA trusted_schema=ON;')}catch [System.InvalidOperationException] {} $before=[PrepareMemory]::Used() for($i=0;$i -lt 100;$i++) { $blocked=$false try {$null=$db.query('SELECT 1; PRAGMA trusted_schema=ON;')}catch [System.InvalidOperationException] {$blocked=$true} Assert-True $blocked 'Compound accepted' } Assert-True ([PrepareMemory]::Used() -eq $before) 'Native allocations retained' } Write-Host "PrepareIsolation: passed=$passed; failed=$failed" if($failed -gt 0){exit 1} |