|
1 | 1 | import type { TLSConfig } from '@/network/types'; |
2 | 2 | import type { FileSystem } from '@/types'; |
3 | | -import type { VaultId } from '@/ids'; |
| 3 | +import { VaultId } from '@/ids'; |
4 | 4 | import type NodeManager from '@/nodes/NodeManager'; |
5 | 5 | import type { |
6 | 6 | LogEntryMessage, |
7 | 7 | SecretContentMessage, |
| 8 | + SecretIdentifierMessageTagged, |
| 9 | + SecretsRemoveHeaderMessage, |
8 | 10 | VaultListMessage, |
9 | 11 | VaultPermissionMessage, |
10 | 12 | } from '@/client/types'; |
| 13 | +import fc from 'fast-check'; |
11 | 14 | import fs from 'fs'; |
12 | 15 | import path from 'path'; |
13 | 16 | import os from 'os'; |
| 17 | +import { test } from '@fast-check/jest'; |
14 | 18 | import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; |
15 | 19 | import { DB } from '@matrixai/db'; |
16 | 20 | import { RPCClient } from '@matrixai/rpc'; |
17 | | -import { ErrorRPCTimedOut } from '@matrixai/rpc/dist/errors'; |
18 | 21 | import { WebSocketClient } from '@matrixai/ws'; |
19 | 22 | import TaskManager from '@/tasks/TaskManager'; |
20 | 23 | import ACL from '@/acl/ACL'; |
@@ -74,6 +77,8 @@ import * as vaultsErrors from '@/vaults/errors'; |
74 | 77 | import * as clientErrors from '@/client/errors'; |
75 | 78 | import * as networkUtils from '@/network/utils'; |
76 | 79 | import * as testsUtils from '../../utils'; |
| 80 | +import type { ContextTimed } from '@matrixai/contexts'; |
| 81 | +import { timedCancellable as timedCancellableF } from '@matrixai/contexts/dist/functions'; |
77 | 82 |
|
78 | 83 | describe('vaultsClone', () => { |
79 | 84 | const logger = new Logger('vaultsClone test', LogLevel.WARN, [ |
@@ -2464,62 +2469,6 @@ describe('vaultsSecretsRemove', () => { |
2464 | 2469 | vaultsErrors.ErrorVaultsVaultUndefined, |
2465 | 2470 | ); |
2466 | 2471 | }); |
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 | | - }); |
2523 | 2472 | test('fails deleting vault root', async () => { |
2524 | 2473 | // Create secrets |
2525 | 2474 | const secretName = 'test-secret1'; |
@@ -2867,6 +2816,94 @@ describe('vaultsSecretsRemove', () => { |
2867 | 2816 | }); |
2868 | 2817 | }); |
2869 | 2818 | }); |
| 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 | + ); |
2870 | 2907 | }); |
2871 | 2908 | describe('vaultsSecretsNewDir and vaultsSecretsList', () => { |
2872 | 2909 | const logger = new Logger('vaultsSecretsNewDirList test', LogLevel.WARN, [ |
|
0 commit comments