Files
ihaleist/tests/document-delete-crash.test.mjs
T

84 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import {spawn} from 'node:child_process';
import {mkdtempSync,rmSync,existsSync,readFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
// All fault injection is confined to a disposable child process and temp volume.
async function killedDeletion(phase,dir) {
const moduleUrl=new URL('../server/user-store.mjs',import.meta.url).href;
const code=`
import fs from 'node:fs';
import {DatabaseSync} from 'node:sqlite';
const store=await import(${JSON.stringify(moduleUrl)});
const email='crash@example.test';
const bytes=Buffer.from('Kesinti sırasında korunacak gerçek belge baytları.');
const id=store.saveDocumentFile(email,{ad:'crash.txt'},bytes);
const path=store.BELGE_DIR+'/'+store.getDocument(email,id).dosya;
const pause=()=>{process.send({phase:${JSON.stringify(phase)},id,path});Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);};
if(${JSON.stringify(phase)}==='before-commit') {
const original=DatabaseSync.prototype.prepare;
DatabaseSync.prototype.prepare=function(sql){
const statement=original.call(this,sql);
if(sql==='DELETE FROM belgeler WHERE id=? AND email=?') {
const run=statement.run.bind(statement);
statement.run=(...args)=>{const result=run(...args);pause();return result;};
}
return statement;
};
} else {
const original=fs.unlinkSync;
fs.unlinkSync=function(candidate){if(candidate===path)pause();return original(candidate);};
}
store.deleteDocument(email,id);
process.exit(3);
`;
const child=spawn(process.execPath,['--input-type=module','-e',code],{
env:{...process.env,IHALEIST_USER_DATA_DIR:dir},stdio:['ignore','ignore','pipe','ipc'],
});
let stderr='';child.stderr.on('data',chunk=>{stderr+=chunk;});
let timer;
try {
const message=await new Promise((resolve,reject)=>{
child.once('message',resolve);
child.once('error',reject);
child.once('exit',(code,signal)=>reject(new Error(`Child exited before checkpoint: ${code}/${signal} ${stderr}`)));
timer=setTimeout(()=>reject(new Error('Child did not reach deletion checkpoint')),10000);
});
clearTimeout(timer);
const stopped=new Promise(resolve=>child.once('exit',(code,signal)=>resolve({code,signal})));
child.kill('SIGKILL');
assert.equal((await stopped).signal,'SIGKILL');
return message;
} finally {clearTimeout(timer);if(child.exitCode===null && child.signalCode===null)child.kill('SIGKILL');}
}
for(const phase of ['before-commit','after-commit']) {
test(`document deletion recovers after real SIGKILL ${phase}`,async()=>{
const dir=mkdtempSync(join(tmpdir(),'ihaleist-crash-'));
try {
const checkpoint=await killedDeletion(phase,dir);
assert.equal(existsSync(checkpoint.path),true,'file must still exist at the forced crash checkpoint');
// Inspect recovery in another process: no cached connection can mask WAL recovery.
const code=`
const store=await import(${JSON.stringify(new URL('../server/user-store.mjs',import.meta.url).href)});
const record=store.getDocument('crash@example.test',${JSON.stringify(checkpoint.id)});
const db=store.openDb();
const pending=db.prepare('SELECT COUNT(*) n FROM document_file_cleanup').get().n;
const integrity=db.prepare('PRAGMA integrity_check').get().integrity_check;
db.close();console.log(JSON.stringify({exists:!!record,pending,integrity}));
`;
const child=spawn(process.execPath,['--input-type=module','-e',code],{env:{...process.env,IHALEIST_USER_DATA_DIR:dir},stdio:['ignore','pipe','pipe']});
let output='',errors='';child.stdout.on('data',chunk=>output+=chunk);child.stderr.on('data',chunk=>errors+=chunk);
const exit=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('exit',resolve);});
assert.equal(exit,0,errors);
const result=JSON.parse(output);
assert.equal(result.integrity,'ok');assert.equal(result.pending,0);
assert.equal(result.exists,phase==='before-commit');
assert.equal(existsSync(checkpoint.path),phase==='before-commit');
if(phase==='before-commit')assert.equal(readFileSync(checkpoint.path,'utf8'),'Kesinti sırasında korunacak gerçek belge baytları.');
} finally {rmSync(dir,{recursive:true,force:true});}
});
}