Skip to content

Commit bd45c30

Browse files
committed
chore: working on dynamic handler cancellation
1 parent 4780cd8 commit bd45c30

File tree

2 files changed

+119
-60
lines changed

2 files changed

+119
-60
lines changed

tests/client/handlers/vaults.test.ts

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import type { TLSConfig } from '@/network/types';
22
import type { FileSystem } from '@/types';
3-
import type { VaultId } from '@/ids';
3+
import { VaultId } from '@/ids';
44
import type NodeManager from '@/nodes/NodeManager';
55
import type {
66
LogEntryMessage,
77
SecretContentMessage,
8+
SecretIdentifierMessageTagged,
9+
SecretsRemoveHeaderMessage,
810
VaultListMessage,
911
VaultPermissionMessage,
1012
} from '@/client/types';
13+
import fc from 'fast-check';
1114
import fs from 'fs';
1215
import path from 'path';
1316
import os from 'os';
17+
import { test } from '@fast-check/jest';
1418
import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger';
1519
import { DB } from '@matrixai/db';
1620
import { RPCClient } from '@matrixai/rpc';
17-
import { ErrorRPCTimedOut } from '@matrixai/rpc/dist/errors';
1821
import { WebSocketClient } from '@matrixai/ws';
1922
import TaskManager from '@/tasks/TaskManager';
2023
import ACL from '@/acl/ACL';
@@ -74,6 +77,8 @@ import * as vaultsErrors from '@/vaults/errors';
7477
import * as clientErrors from '@/client/errors';
7578
import * as networkUtils from '@/network/utils';
7679
import * as testsUtils from '../../utils';
80+
import type { ContextTimed } from '@matrixai/contexts';
81+
import { timedCancellable as timedCancellableF } from '@matrixai/contexts/dist/functions';
7782

7883
describe('vaultsClone', () => {
7984
const logger = new Logger('vaultsClone test', LogLevel.WARN, [
@@ -2464,62 +2469,6 @@ describe('vaultsSecretsRemove', () => {
24642469
vaultsErrors.ErrorVaultsVaultUndefined,
24652470
);
24662471
});
2467-
test('should fail when cancelled', async () => {
2468-
// Inducing a cancellation by a timeout
2469-
const response = await rpcClient.methods.vaultsSecretsRemove({
2470-
timer: 100,
2471-
});
2472-
// Read response
2473-
const consumeP = async () => {
2474-
for await (const _ of response.readable) {
2475-
// Consume values
2476-
}
2477-
};
2478-
await expect(consumeP()).rejects.toThrow(ErrorRPCTimedOut);
2479-
});
2480-
test('should cancel in the midst of an operation', async () => {
2481-
// Create secrets
2482-
const secretName = 'test-secret1';
2483-
const vaultId = await vaultManager.createVault('test-vault');
2484-
const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId);
2485-
await vaultManager.withVaults([vaultId], async (vault) => {
2486-
await vault.writeF(async (efs) => {
2487-
await efs.writeFile(secretName, secretName);
2488-
});
2489-
});
2490-
// Inducing a cancellation by a timeout
2491-
const response = await rpcClient.methods.vaultsSecretsRemove({
2492-
timer: 100,
2493-
});
2494-
// Header message
2495-
const writer = response.writable.getWriter();
2496-
await writer.write({
2497-
type: 'VaultNamesHeaderMessage',
2498-
vaultNames: [vaultIdEncoded],
2499-
});
2500-
// Set a timeout so that the method will execute after RPC timeout
2501-
setTimeout(async () => {
2502-
// Content messages
2503-
await writer.write({
2504-
type: 'SecretIdentifierMessage',
2505-
nameOrId: vaultIdEncoded,
2506-
secretName: secretName,
2507-
});
2508-
await writer.close();
2509-
// Read response
2510-
const consumeP = async () => {
2511-
for await (const _ of response.readable) {
2512-
// Consume values
2513-
}
2514-
};
2515-
await expect(consumeP()).rejects.toThrow(ErrorRPCTimedOut);
2516-
await vaultManager.withVaults([vaultId], async (vault) => {
2517-
await vault.readF(async (efs) => {
2518-
expect(await efs.exists(secretName)).toBeTruthy();
2519-
});
2520-
});
2521-
}, 150);
2522-
});
25232472
test('fails deleting vault root', async () => {
25242473
// Create secrets
25252474
const secretName = 'test-secret1';
@@ -2867,6 +2816,94 @@ describe('vaultsSecretsRemove', () => {
28672816
});
28682817
});
28692818
});
2819+
test.prop([testsUtils.vaultNameArb(), testsUtils.fileNameArb()], { numRuns: 10 })(
2820+
'cancellation should abort the handler',
2821+
async (vaultName, fileNames) => {
2822+
const maxLogicalSteps = fc.sample(
2823+
fc.integer({ min: 0, max: fileNames.length - 1 }),
2824+
1
2825+
)[0];
2826+
2827+
const vaultId = await vaultManager.createVault(vaultName);
2828+
const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId);
2829+
await vaultManager.withVaults([vaultId], async (vault) => {
2830+
await vault.writeF(async (efs) => {
2831+
for (const file of fileNames) {
2832+
await efs.writeFile(file, file);
2833+
}
2834+
});
2835+
});
2836+
2837+
const inputStream = async function* (): AsyncGenerator<
2838+
SecretsRemoveHeaderMessage | SecretIdentifierMessageTagged,
2839+
void,
2840+
void
2841+
> {
2842+
yield {
2843+
type: 'VaultNamesHeaderMessage',
2844+
vaultNames: [vaultIdEncoded],
2845+
};
2846+
for (const file of fileNames) {
2847+
yield {
2848+
type: 'SecretIdentifierMessage',
2849+
nameOrId: vaultIdEncoded,
2850+
secretName: file,
2851+
};
2852+
}
2853+
};
2854+
2855+
let logicalStepsCounter = 0;
2856+
const handler = new VaultsSecretsRemove({
2857+
db: db,
2858+
vaultManager: vaultManager,
2859+
});
2860+
2861+
// Wrap the handler call in a wrapper which creates a usable scoped
2862+
// context object.
2863+
const abortController = new AbortController();
2864+
const f = async (ctx: ContextTimed) => {
2865+
// The `cancel` and `meta` aren't being used here, so dummy objects can
2866+
// be passed instead.
2867+
const result = handler.handle(inputStream(), () => {}, {}, ctx);
2868+
2869+
let aborted = false;
2870+
for await (const _ of result) {
2871+
// If we have already aborted, then the handler should not be sending
2872+
// any further information.
2873+
if (aborted) {
2874+
fail('The handler should not continue after cancellation');
2875+
}
2876+
// If we are on a logical step that matches what we have to abort on,
2877+
// then send an abort signal. Next loop should throw an error.
2878+
if (logicalStepsCounter == maxLogicalSteps) {
2879+
abortController.abort('cancel');
2880+
aborted = true;
2881+
console.error('aborting');
2882+
}
2883+
logicalStepsCounter++;
2884+
}
2885+
};
2886+
2887+
// TODO: This loop is used because simply using the promise inside a
2888+
// `await expect(fProm).toReject()` does not work.
2889+
try {
2890+
// Create a context for the tests to run
2891+
await timedCancellableF(f, true)({ signal: abortController.signal });
2892+
console.log(fileNames.length, maxLogicalSteps);
2893+
} catch (e) {
2894+
if (e !== 'cancel') fail('The test does not properly cancel');
2895+
}
2896+
2897+
// [DONE] get a list of secrets we need to make
2898+
// [DONE] make the secrets in the vault root
2899+
// [DONE] randomise a number from 0 to the number of secrets
2900+
// [DONE] automate sending over the header and the content messages
2901+
// [DONE] use a counter to keep track of the current "logical step"
2902+
// [DONE] when the step matches the random number, then send an abort signal
2903+
// [DONE] expect a timeout to be thrown from the handler side
2904+
// [DONE] make sure that the call never resolves - we are testing cancellation
2905+
},
2906+
);
28702907
});
28712908
describe('vaultsSecretsNewDir and vaultsSecretsList', () => {
28722909
const logger = new Logger('vaultsSecretsNewDirList test', LogLevel.WARN, [

tests/utils/fastCheck.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { fc } from '@fast-check/jest';
1+
import { fc } from '@fast-check/jest';
22
import * as utils from '@/utils';
33

44
class SleepCommand implements fc.AsyncCommand<any, any> {
@@ -25,4 +25,26 @@ class SleepCommand implements fc.AsyncCommand<any, any> {
2525
const scheduleCall = <T>(s: fc.Scheduler, f: () => Promise<T>) =>
2626
s.schedule(Promise.resolve()).then(() => f());
2727

28-
export { SleepCommand, scheduleCall };
28+
/**
29+
* Creates an array of ASCII file names
30+
*/
31+
const fileNameArb = (maxLength: number = 10, minLength: number = 1) =>
32+
fc
33+
.array(
34+
fc.char().filter((ch) => !/[<>:"/\\|?*\x00-\x1F]/.test(ch)),
35+
{ minLength, maxLength },
36+
)
37+
.map((chars) => chars.join(''))
38+
.filter((name) => name.trim().length > 0)
39+
.noShrink();
40+
41+
/**
42+
* Creates an array of ASCII file names
43+
*/
44+
const vaultNameArb = (maxLength: number = 10, minLength: number = 6) =>
45+
fc
46+
.stringOf(fc.char(), { minLength: minLength, maxLength: maxLength })
47+
.map((value) => `vault-${value}`)
48+
.noShrink();
49+
50+
export { SleepCommand, scheduleCall, fileNameArb, vaultNameArb };

0 commit comments

Comments
 (0)