Files
fluxer/fluxer_desktop/src/main/DesktopRuntimeTransactionRegistry.test.mjs
T

347 lines
12 KiB
JavaScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createRequire} from 'node:module';
import {beforeEach, describe, test} from 'node:test';
import {fileURLToPath} from 'node:url';
import {installElectronStub, installTestModuleStub} from './LocalAppTestSupport.test.mjs';
installElectronStub();
const require = createRequire(import.meta.url);
const esbuild = require('esbuild');
const PROTOCOL_KEY = '__fluxerDesktopRuntimeTransactionTestProtocol__';
const DISCOVERY_KEY = '__fluxerDesktopRuntimeTransactionTestDiscovery__';
installTestModuleStub(
'@electron/main/LocalAppProtocol',
`export const getDesktopLocalAppProtocol = () => globalThis[${JSON.stringify(PROTOCOL_KEY)}];`,
);
installTestModuleStub(
'@electron/main/DesktopRuntimeDiscovery',
`export const resolveDesktopRuntimePlan = async (request) => await globalThis[${JSON.stringify(DISCOVERY_KEY)}](request);`,
);
const REGISTRY_SOURCE_PATH = fileURLToPath(new URL('./DesktopRuntimeTransactionRegistry.ts', import.meta.url));
const {DesktopRuntimeTransactionRegistry} = await import(
`data:text/javascript,${encodeURIComponent(
esbuild.transformSync(readFileSync(REGISTRY_SOURCE_PATH, 'utf8'), {
loader: 'ts',
format: 'esm',
platform: 'node',
target: 'node22',
}).code,
)}`
);
const MAX_RUNTIME_PREPARATIONS = 8;
const ONE = 'https://one.test/api';
const TWO = 'https://two.test/api';
const BASE = 'https://base.test/api';
function createPlan(instanceKey) {
return {
instanceKey,
document: {instance: instanceKey},
endpoints: {
apiEndpoint: instanceKey,
apiPublicEndpoint: null,
webAppEndpoint: null,
mediaEndpoint: null,
staticCdnEndpoint: null,
uploadRelayEndpoint: null,
gatewayEndpoint: null,
inviteEndpoint: null,
giftEndpoint: null,
},
selfHosted: true,
};
}
function createProtocol(activePlan = null) {
const shutdown = new AbortController();
let active = activePlan;
return {
activations: [],
getActivePlan: () => active,
findPlanForRoute: () => null,
getShutdownSignal: () => shutdown.signal,
activateRuntimePlan(plan) {
active = plan;
this.activations.push(plan.instanceKey);
},
deactivateRuntimePlan() {
active = null;
this.activations.push(null);
},
};
}
function createRendererDocument(name) {
const watchers = new Set();
const owner = {
sender: name,
frame: name,
isCurrent: () => true,
matchesEvent: (event) => event.document === owner,
requireCurrent: () => undefined,
watchInvalidation(onInvalidated) {
const watcher = {onInvalidated, dispose: () => watchers.delete(watcher)};
watchers.add(watcher);
return watcher;
},
};
return {owner, event: {document: owner}, liveWatchers: () => watchers.size};
}
const rendererDocuments = {capture: (event) => event.document};
function transactionRequest(preparation) {
return {
preparationId: preparation.preparationId,
instanceKey: preparation.instanceKey,
baseRevision: preparation.baseRevision,
};
}
function committedRequest(preparation, committedRevision) {
return {...transactionRequest(preparation), committedRevision};
}
function named(name) {
return (error) => {
assert.equal(error.name, name, `expected ${name}, got ${error.name}: ${error.message}`);
return true;
};
}
let protocol;
let discovered;
let document;
let registry;
beforeEach(() => {
protocol = createProtocol();
discovered = new Map([
[ONE, createPlan(ONE)],
[TWO, createPlan(TWO)],
]);
globalThis[PROTOCOL_KEY] = protocol;
globalThis[DISCOVERY_KEY] = async ({input}) => {
const plan = discovered.get(input);
if (plan == null) {
throw new Error(`no discovery fixture for ${input}`);
}
return plan;
};
document = createRendererDocument('document-one');
registry = new DesktopRuntimeTransactionRegistry(rendererDocuments);
});
describe('DesktopRuntimeTransactionRegistry.commit', () => {
test('a preparation cannot commit at a revision an earlier rollback already moved past', async () => {
const basePlan = createPlan(BASE);
protocol = createProtocol(basePlan);
globalThis[PROTOCOL_KEY] = protocol;
registry = new DesktopRuntimeTransactionRegistry(rendererDocuments);
registry.initialize();
const first = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const second = await registry.prepare(document.event, {preparationId: 'second', instanceKey: ONE});
assert.equal(second.baseRevision, first.baseRevision);
assert.equal(second.baseActiveInstanceKey, BASE);
registry.commit(document.event, transactionRequest(first));
registry.abort(document.event, {preparationId: 'first'});
assert.equal(protocol.getActivePlan(), basePlan);
assert.throws(
() => registry.commit(document.event, transactionRequest(second)),
named('DesktopRuntimePreparationSupersededError'),
);
assert.equal(protocol.getActivePlan(), basePlan);
assert.deepEqual(protocol.activations, [ONE, BASE]);
});
test('a preparation left behind by another commit is refused as superseded', async () => {
registry.initialize();
const first = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const second = await registry.prepare(document.event, {preparationId: 'second', instanceKey: TWO});
assert.equal(first.baseRevision, 0);
assert.equal(second.baseRevision, 0);
const commit = registry.commit(document.event, transactionRequest(first));
assert.deepEqual(commit, {preparationId: 'first', instanceKey: ONE, baseRevision: 0, revision: 1});
registry.finalize(document.event, committedRequest(first, 1));
assert.throws(
() => registry.commit(document.event, transactionRequest(second)),
named('DesktopRuntimePreparationSupersededError'),
);
assert.equal(protocol.getActivePlan().instanceKey, ONE);
assert.deepEqual(protocol.activations, [ONE]);
});
test('a commit whose base activation moved underneath it is refused on identity', async () => {
registry.initialize();
const prepared = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
assert.equal(prepared.baseActiveInstanceKey, null);
protocol.activateRuntimePlan(createPlan(BASE));
assert.throws(
() => registry.commit(document.event, transactionRequest(prepared)),
named('DesktopRuntimePreparationIdentityError'),
);
assert.equal(protocol.getActivePlan().instanceKey, BASE);
assert.deepEqual(protocol.activations, [BASE]);
});
test('a second provisional commit is refused while the first is still open', async () => {
registry.initialize();
const first = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const second = await registry.prepare(document.event, {preparationId: 'second', instanceKey: TWO});
registry.commit(document.event, transactionRequest(first));
assert.throws(
() => registry.commit(document.event, transactionRequest(second)),
named('DesktopRuntimeCommitInProgressError'),
);
assert.equal(protocol.getActivePlan().instanceKey, ONE);
});
});
describe('DesktopRuntimeTransactionRegistry.abort', () => {
test('aborting a committed preparation restores the exact plan it replaced', async () => {
const basePlan = createPlan(BASE);
protocol = createProtocol(basePlan);
globalThis[PROTOCOL_KEY] = protocol;
registry = new DesktopRuntimeTransactionRegistry(rendererDocuments);
registry.initialize();
const prepared = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
assert.equal(prepared.baseRevision, 1);
assert.equal(prepared.baseActiveInstanceKey, BASE);
registry.commit(document.event, transactionRequest(prepared));
assert.equal(protocol.getActivePlan().instanceKey, ONE);
const aborted = registry.abort(document.event, {preparationId: 'first'});
assert.deepEqual(aborted, {disposition: 'rolled-back', revision: 3, activeInstanceKey: BASE});
assert.equal(protocol.getActivePlan(), basePlan);
assert.deepEqual(protocol.activations, [ONE, BASE]);
assert.equal(document.liveWatchers(), 0);
});
test('aborting a committed preparation that replaced nothing deactivates the runtime again', async () => {
registry.initialize();
const prepared = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
registry.commit(document.event, transactionRequest(prepared));
const aborted = registry.abort(document.event, {preparationId: 'first'});
assert.deepEqual(aborted, {disposition: 'rolled-back', revision: 2, activeInstanceKey: null});
assert.equal(protocol.getActivePlan(), null);
assert.deepEqual(protocol.activations, [ONE, null]);
});
test('aborting a prepared-but-uncommitted preparation leaves the runtime alone', async () => {
registry.initialize();
await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const aborted = registry.abort(document.event, {preparationId: 'first'});
assert.deepEqual(aborted, {disposition: 'aborted', revision: 0, activeInstanceKey: null});
assert.deepEqual(protocol.activations, []);
assert.deepEqual(registry.abort(document.event, {preparationId: 'first'}), {
disposition: 'absent',
revision: 0,
activeInstanceKey: null,
});
});
});
describe('DesktopRuntimeTransactionRegistry.rollback', () => {
test('rolling back a committed preparation restores the exact plan it replaced', async () => {
const basePlan = createPlan(BASE);
protocol = createProtocol(basePlan);
globalThis[PROTOCOL_KEY] = protocol;
registry = new DesktopRuntimeTransactionRegistry(rendererDocuments);
registry.initialize();
const prepared = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const commit = registry.commit(document.event, transactionRequest(prepared));
const rollback = registry.rollback(document.event, committedRequest(prepared, commit.revision));
assert.deepEqual(rollback, {revision: commit.revision + 1, activeInstanceKey: BASE});
assert.equal(protocol.getActivePlan(), basePlan);
assert.throws(
() => registry.rollback(document.event, committedRequest(prepared, commit.revision)),
named('DesktopRuntimePreparationNotFoundError'),
);
});
test('a finalized preparation can no longer be rolled back', async () => {
registry.initialize();
const prepared = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const commit = registry.commit(document.event, transactionRequest(prepared));
registry.finalize(document.event, committedRequest(prepared, commit.revision));
assert.throws(
() => registry.rollback(document.event, committedRequest(prepared, commit.revision)),
named('DesktopRuntimePreparationNotFoundError'),
);
assert.equal(protocol.getActivePlan().instanceKey, ONE);
assert.deepEqual(protocol.activations, [ONE]);
});
});
describe('DesktopRuntimeTransactionRegistry.prepare', () => {
test('a renderer cannot hold more preparations than the registry admits', async () => {
registry.initialize();
for (let index = 0; index < MAX_RUNTIME_PREPARATIONS; index += 1) {
discovered.set(`https://instance-${index}.test/api`, createPlan(`https://instance-${index}.test/api`));
const prepared = await registry.prepare(document.event, {
preparationId: `preparation-${index}`,
instanceKey: `https://instance-${index}.test/api`,
});
assert.equal(prepared.instanceKey, `https://instance-${index}.test/api`);
}
await assert.rejects(
registry.prepare(document.event, {preparationId: 'overflow', instanceKey: ONE}),
named('DesktopRuntimePreparationCapacityError'),
);
assert.equal(document.liveWatchers(), MAX_RUNTIME_PREPARATIONS);
assert.deepEqual(registry.abort(document.event, {preparationId: 'overflow'}), {
disposition: 'absent',
revision: 0,
activeInstanceKey: null,
});
});
test('a duplicate preparation id is refused before it can shadow the live one', async () => {
registry.initialize();
await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
await assert.rejects(
registry.prepare(document.event, {preparationId: 'first', instanceKey: TWO}),
named('DesktopRuntimePreparationAlreadyExistsError'),
);
assert.equal(document.liveWatchers(), 1);
});
test('a preparation belongs to the renderer document that opened it', async () => {
registry.initialize();
const prepared = await registry.prepare(document.event, {preparationId: 'first', instanceKey: ONE});
const other = createRendererDocument('document-two');
assert.throws(
() => registry.commit(other.event, transactionRequest(prepared)),
named('DesktopRuntimePreparationOwnershipError'),
);
assert.deepEqual(protocol.activations, []);
});
});