nuthatch/web/test/recursive-delete.test.mjs

190 lines
4.6 KiB
JavaScript
Raw Permalink Normal View History

import assert from 'node:assert/strict';
import test from 'node:test';
import {
RecursiveDeletePageLimitError,
runRecursiveDelete
} from '../src/workflows/recursive-delete.ts';
import {
browserRecursiveDeletePersistence
} from '../src/workflows/recursive-delete-persistence.ts';
const session = {
user_id: 'user-1',
display: null,
csrf: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
};
test('recursive delete continues from the first page through concurrent additions', async () => {
const persistence = memoryPersistence();
const plans = [
{ paths: ['tree/a'], continuation: 'page-2' },
{ paths: ['tree/b'], continuation: null },
{ paths: ['tree/new'], continuation: null },
{ paths: [], continuation: null }
];
const continuations = [];
const batches = [];
const client = recursiveClient({
plan(request) {
continuations.push(request.continuation);
return plans.shift();
},
batch(items) {
batches.push(items.map((item) => item.path));
return succeeded(items);
}
});
let operation = 0;
const result = await runRecursiveDelete(
client,
persistence,
session,
'personal',
'tree',
() => `operation-${++operation}`
);
assert.deepEqual(result, { deleted: 3, pages: 3 });
assert.deepEqual(continuations, [undefined, 'page-2', undefined, undefined]);
assert.deepEqual(batches, [['tree/a'], ['tree/b'], ['tree/new']]);
assert.equal(persistence.read(), null);
});
test('recursive delete reload preserves pending IDs and restarts terminal IDs', async () => {
const storage = new MemoryStorage();
const firstAdapter = browserRecursiveDeletePersistence(storage);
firstAdapter.save('shared', 'tree', [
{ path: 'tree/pending', operation_id: 'pending-id' },
{ path: 'tree/failed', operation_id: 'failed-id' }
]);
const reloaded = browserRecursiveDeletePersistence(storage);
const client = recursiveClient({
plan() {
throw new Error('saved batch should replay before planning');
},
batch(items) {
return [
{
path: items[0].path,
operation_id: items[0].operation_id,
result: 'pending',
error: null
},
{
path: items[1].path,
operation_id: items[1].operation_id,
result: 'failed',
error: null
}
];
}
});
await assert.rejects(
runRecursiveDelete(
client,
reloaded,
session,
'shared',
'tree',
() => 'replacement-id'
),
/2 objects remain/
);
assert.deepEqual(reloaded.read()?.items, [
{ path: 'tree/pending', operation_id: 'pending-id' },
{ path: 'tree/failed', operation_id: 'replacement-id' }
]);
});
test('recursive delete enforces the page bound and retains resumable work', async () => {
const persistence = memoryPersistence();
const client = recursiveClient({
plan() {
return { paths: ['tree/item'], continuation: null };
},
batch(items) {
return succeeded(items);
}
});
let operation = 0;
await assert.rejects(
runRecursiveDelete(
client,
persistence,
session,
'personal',
'tree',
() => `operation-${++operation}`,
100
),
(error) =>
error instanceof RecursiveDeletePageLimitError && error.pages === 100
);
assert.equal(operation, 100);
});
test('invalid persisted state is removed on reload', () => {
const storage = new MemoryStorage();
storage.setItem('nuthatch-recursive-delete', '{"items":[null]}');
const persistence = browserRecursiveDeletePersistence(storage);
assert.equal(persistence.read(), null);
assert.equal(storage.length, 0);
});
function recursiveClient(behavior) {
return {
async deletePlan(request) {
const plan = behavior.plan(request);
return {
ok: true,
value: plan
};
},
async deleteBatch({ body }) {
return {
ok: true,
value: { results: behavior.batch(body.items) }
};
}
};
}
function succeeded(items) {
return items.map((item) => ({
path: item.path,
operation_id: item.operation_id,
result: 'succeeded',
error: null
}));
}
function memoryPersistence() {
return browserRecursiveDeletePersistence(new MemoryStorage());
}
class MemoryStorage {
values = new Map();
get length() {
return this.values.size;
}
clear() {
this.values.clear();
}
getItem(key) {
return this.values.get(key) ?? null;
}
key(index) {
return [...this.values.keys()][index] ?? null;
}
removeItem(key) {
this.values.delete(key);
}
setItem(key, value) {
this.values.set(key, value);
}
}