diff --git a/tests/vaults/VaultOps.test.ts b/tests/vaults/VaultOps.test.ts deleted file mode 100644 index 078676580..000000000 --- a/tests/vaults/VaultOps.test.ts +++ /dev/null @@ -1,672 +0,0 @@ -import type { VaultId } from '@/vaults/types'; -import type { Vault } from '@/vaults/Vault'; -import type KeyRing from '@/keys/KeyRing'; -import type { LevelPath } from '@matrixai/db'; -import type { FileTree } from '@/vaults/types'; -import type { ContentNode, TreeNode } from '@/vaults/types'; -import type { ErrorMessage } from '@/client/types'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { EncryptedFS, Stat } from 'encryptedfs'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { DB } from '@matrixai/db'; -import VaultInternal from '@/vaults/VaultInternal'; -import * as fileTree from '@/vaults/fileTree'; -import * as vaultOps from '@/vaults/VaultOps'; -import * as vaultsErrors from '@/vaults/errors'; -import * as vaultsUtils from '@/vaults/utils'; -import * as keysUtils from '@/keys/utils'; -import * as testNodesUtils from '../nodes/utils'; - -describe('VaultOps', () => { - const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); - - const secretName = 'secret'; - const secretNameNew = 'secret-new'; - const secretContent = 'secret-content'; - const secretContentNew = 'secret-content-new'; - const dirName = 'dir'; - - let dataDir: string; - let baseEfs: EncryptedFS; - let vaultId: VaultId; - let vaultInternal: VaultInternal; - let vault: Vault; - let db: DB; - let vaultsDbPath: LevelPath; - const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); - const dummyKeyRing = { - getNodeId: () => { - return testNodesUtils.generateRandomNodeId(); - }, - } as KeyRing; - - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - const dbPath = path.join(dataDir, 'efsDb'); - const dbKey = keysUtils.generateKey(); - baseEfs = await EncryptedFS.createEncryptedFS({ - dbKey, - dbPath, - logger, - }); - await baseEfs.start(); - - vaultId = vaultIdGenerator(); - await baseEfs.mkdir( - path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), - { - recursive: true, - }, - ); - db = await DB.createDB({ - dbPath: path.join(dataDir, 'db'), - logger, - }); - vaultsDbPath = ['vaults']; - vaultInternal = await VaultInternal.createVaultInternal({ - keyRing: dummyKeyRing, - vaultId, - efs: baseEfs, - logger: logger.getChild(VaultInternal.name), - fresh: true, - db, - vaultsDbPath: vaultsDbPath, - vaultName: 'VaultName', - }); - vault = vaultInternal as Vault; - }); - afterEach(async () => { - await vaultInternal.stop(); - await vaultInternal.destroy(); - await db.stop(); - await db.destroy(); - await baseEfs.stop(); - await baseEfs.destroy(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - async function writeSecret(secretPath: string, contents: string) { - return await vault.writeF(async (efs) => { - await vaultsUtils.mkdirExists(efs, path.dirname(secretPath)); - await efs.writeFile(secretPath, contents); - }); - } - - async function readSecret(path: string) { - return await vault.readF(async (efs) => { - return await efs.readFile(path); - }); - } - - async function expectSecret(path: string, contentsExpected: string) { - const contentsSecretP = readSecret(path); - await expect(contentsSecretP).resolves.toBeDefined(); - const contentsSecretValue = (await contentsSecretP).toString(); - expect(contentsSecretValue).toBe(contentsExpected); - } - - async function expectSecretNot(path: string) { - const contentsSecretP = readSecret(path); - await expect(contentsSecretP).rejects.toThrow( - 'ENOENT: no such file or directory', - ); - } - - async function mkdir(path: string) { - return await vault.writeF(async (efs) => { - await vaultsUtils.mkdirExists(efs, path); - }); - } - - async function expectDirExists(path: string) { - return await vault.readF(async (efs) => { - const dirP = efs.readdir(path); - await expect(dirP).resolves.toBeDefined(); - }); - } - - async function expectDirExistsNot(path: string) { - return await vault.readF(async (efs) => { - const dirP = efs.readdir(path); - await expect(dirP).rejects.toThrow('ENOENT'); - }); - } - - // Adding secrets - describe('addSecret', () => { - test('adding a secret', async () => { - await vaultOps.addSecret(vault, secretName, secretContent); - await expectSecret(secretName, secretContent); - }); - test('add a secret under an existing directory', async () => { - await mkdir(dirName); - const secretPath = path.join(dirName, secretName); - await vaultOps.addSecret(vault, secretPath, secretContent); - await expectSecret(secretPath, secretContent); - }); - test('add a secret creating directory', async () => { - const secretPath = path.join(dirName, secretName); - await vaultOps.addSecret(vault, secretPath, secretContent); - await expectSecret(secretPath, secretContent); - }); - test( - 'adding a secret multiple times', - async () => { - for (let i = 0; i < 5; i++) { - const name = `${secretName}+${i}`; - await vaultOps.addSecret(vault, name, secretContent); - await expectSecret(name, secretContent); - } - }, - globalThis.defaultTimeout * 4, - ); - test('adding a secret that already exists should fail', async () => { - await vaultOps.addSecret(vault, secretName, secretContent); - const addSecretP = vaultOps.addSecret(vault, secretName, secretContent); - await expect(addSecretP).rejects.toThrow( - vaultsErrors.ErrorSecretsSecretDefined, - ); - }); - }); - describe('updateSecret', () => { - test('updating secret content', async () => { - await writeSecret(secretName, secretContent); - await vaultOps.updateSecret(vault, secretName, secretContentNew); - await expectSecret(secretName, secretContentNew); - }); - test('updating secret content within a directory', async () => { - const secretPath = path.join(dirName, secretName); - await writeSecret(secretPath, secretContent); - await vaultOps.updateSecret(vault, secretPath, secretContentNew); - await expectSecret(secretPath, secretContentNew); - }); - test( - 'updating a secret multiple times', - async () => { - await vaultOps.addSecret(vault, 'secret-1', 'secret-content'); - await writeSecret(secretName, secretContent); - for (let i = 0; i < 5; i++) { - const contentNew = `${secretContentNew}${i}`; - await vaultOps.updateSecret(vault, secretName, contentNew); - await expectSecret(secretName, contentNew); - } - }, - globalThis.defaultTimeout * 2, - ); - test('updating a secret that does not exist should fail', async () => { - await expect( - vaultOps.updateSecret(vault, secretName, secretContentNew), - ).rejects.toThrow(vaultsErrors.ErrorSecretsSecretUndefined); - }); - }); - describe('renameSecret', () => { - test('renaming a secret', async () => { - await writeSecret(secretName, secretContent); - await vaultOps.renameSecret(vault, secretName, secretNameNew); - await expectSecretNot(secretName); - await expectSecret(secretNameNew, secretContent); - }); - test('renaming a secret within a directory', async () => { - const secretPath = path.join(dirName, secretName); - const secretPathNew = path.join(dirName, secretNameNew); - await writeSecret(secretPath, secretContent); - await vaultOps.renameSecret(vault, secretPath, secretPathNew); - await expectSecretNot(secretPath); - await expectSecret(secretPathNew, secretContent); - }); - test('renaming a secret that does not exist should fail', async () => { - await expect( - vaultOps.renameSecret(vault, secretName, secretNameNew), - ).rejects.toThrow(vaultsErrors.ErrorSecretsSecretUndefined); - }); - }); - describe('getSecret', () => { - test('can get a secret', async () => { - await writeSecret(secretName, secretContent); - const secret = await vaultOps.getSecret(vault, secretName); - expect(secret.toString()).toBe(secretContent); - }); - test('getting a secret that does not exist should fail', async () => { - await expect(vaultOps.getSecret(vault, secretName)).rejects.toThrow( - vaultsErrors.ErrorSecretsSecretUndefined, - ); - }); - test('getting a directory should fail', async () => { - await mkdir(dirName); - await expect(vaultOps.getSecret(vault, dirName)).rejects.toThrow( - vaultsErrors.ErrorSecretsIsDirectory, - ); - }); - }); - describe('statSecret', () => { - test('can get stat of a secret', async () => { - await writeSecret(secretName, secretContent); - const stat = await vaultOps.statSecret(vault, secretName); - expect(stat).toBeInstanceOf(Stat); - expect(stat.nlink).toBe(1); - }); - test('can get stat of a directory', async () => { - await mkdir(dirName); - const stat = await vaultOps.statSecret(vault, dirName); - expect(stat).toBeInstanceOf(Stat); - }); - test('getting stat of secret that does not exist should fail', async () => { - await expect(vaultOps.statSecret(vault, secretName)).rejects.toThrow( - vaultsErrors.ErrorSecretsSecretUndefined, - ); - }); - }); - describe('deleteSecret', () => { - test('deleting a secret', async () => { - await writeSecret(secretName, secretContent); - await vaultOps.deleteSecret(vault, [secretName]); - await expectSecretNot(secretName); - }); - test('deleting a secret in a directory', async () => { - const secretPath = path.join(dirName, secretName); - await writeSecret(secretPath, secretContent); - await vaultOps.deleteSecret(vault, [secretPath]); - await expectSecretNot(secretPath); - await expectDirExists(dirName); - }); - test('deleting a directory', async () => { - await mkdir(dirName); - await vaultOps.deleteSecret(vault, [dirName]); - await expectDirExistsNot(dirName); - }); - test('deleting a directory with a file should fail', async () => { - const secretPath = path.join(dirName, secretName); - await writeSecret(secretPath, secretContent); - await expect(vaultOps.deleteSecret(vault, [dirName])).rejects.toThrow( - vaultsErrors.ErrorVaultsRecursive, - ); - }); - test('deleting a directory with force', async () => { - const secretPath = path.join(dirName, secretName); - await writeSecret(secretPath, secretContent); - await vaultOps.deleteSecret(vault, [dirName], { recursive: true }); - await expectDirExistsNot(dirName); - }); - test('deleting a secret that does not exist should fail', async () => { - await expect(vaultOps.deleteSecret(vault, [secretName])).rejects.toThrow( - vaultsErrors.ErrorSecretsSecretUndefined, - ); - }); - test('deleting multiple secrets', async () => { - const secretNames = ['secret1', 'secret2', 'secret3']; - for (const secretName of secretNames) { - await writeSecret(secretName, secretName); - } - await vaultOps.deleteSecret(vault, secretNames); - for (const secretName of secretNames) { - await expectSecretNot(secretName); - } - }); - test('deleting multiple secrets should add only one new log message', async () => { - const secretNames = ['secret1', 'secret2', 'secret3']; - for (const secretName of secretNames) { - await writeSecret(secretName, secretName); - } - const logLength = (await vault.log()).length; - await vaultOps.deleteSecret(vault, secretNames); - for (const secretName of secretNames) { - await expectSecretNot(secretName); - } - expect((await vault.log()).length).toBe(logLength + 1); - }); - }); - describe('mkdir', () => { - test('can create directory', async () => { - const response = await vaultOps.mkdir(vault, dirName); - expect(response.type).toEqual('success'); - await expectDirExists(dirName); - }); - test('can create recursive directory', async () => { - const dirPath = path.join(dirName, dirName); - const response = await vaultOps.mkdir(vault, dirPath, { - recursive: true, - }); - expect(response.type).toEqual('success'); - await expectDirExists(dirPath); - }); - test('creating directories fails without recursive', async () => { - const dirPath = path.join(dirName, dirName); - const response = await vaultOps.mkdir(vault, dirPath); - expect(response.type).toEqual('error'); - const error = response as ErrorMessage; - expect(error.code).toEqual('ENOENT'); - await expectDirExistsNot(dirPath); - }); - test('creating existing directory should fail', async () => { - await mkdir(dirName); - const response = await vaultOps.mkdir(vault, dirName); - expect(response.type).toEqual('error'); - const error = response as ErrorMessage; - expect(error.code).toEqual('EEXIST'); - }); - test('creating existing secret should fail', async () => { - await writeSecret(secretName, secretContent); - const response = await vaultOps.mkdir(vault, secretName); - expect(response.type).toEqual('error'); - const error = response as ErrorMessage; - expect(error.code).toEqual('EEXIST'); - await expectSecret(secretName, secretContent); - }); - }); - describe('addSecretDirectory', () => { - test('adding a directory of 1 secret', async () => { - const secretDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'secret-directory-'), - ); - const secretDirName = path.basename(secretDir); - const name = 'secret'; - const content = keysUtils.getRandomBytes(5); - await fs.promises.writeFile(path.join(secretDir, name), content); - - await vaultOps.addSecretDirectory(vault, secretDir, fs); - await expect( - vault.readF((efs) => efs.readdir(secretDirName)), - ).resolves.toContain('secret'); - - await fs.promises.rm(secretDir, { - force: true, - recursive: true, - }); - }); - test('adding a directory with subdirectories and files', async () => { - const secretDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'secret-directory-'), - ); - const secretDirName = path.basename(secretDir); - await fs.promises.mkdir(path.join(secretDir, 'dir1')); - await fs.promises.mkdir(path.join(secretDir, 'dir1', 'dir2')); - await fs.promises.mkdir(path.join(secretDir, 'dir3')); - - await fs.promises.writeFile(path.join(secretDir, 'secret1'), 'secret1'); - await fs.promises.writeFile( - path.join(secretDir, 'dir1', 'secret2'), - 'secret2', - ); - await fs.promises.writeFile( - path.join(secretDir, 'dir1', 'dir2', 'secret3'), - 'secret3', - ); - await fs.promises.writeFile( - path.join(secretDir, 'dir3', 'secret4'), - 'secret4', - ); - await fs.promises.writeFile( - path.join(secretDir, 'dir3', 'secret5'), - 'secret5', - ); - - await vaultOps.addSecretDirectory(vault, path.join(secretDir), fs); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual( - [ - path.join(secretDirName, 'secret1'), - path.join(secretDirName, 'dir1', 'secret2'), - path.join(secretDirName, 'dir1', 'dir2', 'secret3'), - path.join(secretDirName, 'dir3', 'secret4'), - path.join(secretDirName, 'dir3', 'secret5'), - ].sort(), - ); - - await fs.promises.rm(secretDir, { - force: true, - recursive: true, - }); - }); - test('testing the errors handling of adding secret directories', async () => { - const secretDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'secret-directory-'), - ); - const secretDirName = path.basename(secretDir); - await fs.promises.mkdir(path.join(secretDir, 'dir1')); - await fs.promises.mkdir(path.join(secretDir, 'dir1', 'dir2')); - await fs.promises.mkdir(path.join(secretDir, 'dir3')); - await fs.promises.writeFile(path.join(secretDir, 'secret1'), 'secret1'); - await fs.promises.writeFile( - path.join(secretDir, 'dir1', 'secret2'), - 'secret2', - ); - await fs.promises.writeFile( - path.join(secretDir, 'dir1', 'dir2', 'secret3'), - 'secret3', - ); - await fs.promises.writeFile( - path.join(secretDir, 'dir3', 'secret4'), - 'secret4', - ); - await fs.promises.writeFile( - path.join(secretDir, 'dir3', 'secret5'), - 'secret5', - ); - - await vaultOps.mkdir(vault, secretDirName, { recursive: true }); - await vaultOps.addSecret( - vault, - path.join(secretDirName, 'secret1'), - 'blocking-secret', - ); - await vaultOps.addSecretDirectory(vault, secretDir, fs); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual( - [ - path.join(secretDirName, 'secret1'), - path.join(secretDirName, 'dir1', 'secret2'), - path.join(secretDirName, 'dir1', 'dir2', 'secret3'), - path.join(secretDirName, 'dir3', 'secret4'), - path.join(secretDirName, 'dir3', 'secret5'), - ].sort(), - ); - - await fs.promises.rm(secretDir, { - force: true, - recursive: true, - }); - }); - }); - describe('listSecrets', () => { - test('can list secrets', async () => { - const secretName1 = `${secretName}1`; - const secretName2 = `${secretName}2`; - await writeSecret(secretName1, secretContent); - await writeSecret(secretName2, secretContent); - - const secretList = await vaultOps.listSecrets(vault); - expect(secretList).toInclude(secretName1); - expect(secretList).toInclude(secretName2); - }); - test('empty directories are not listed', async () => { - const dirName1 = `${dirName}1`; - const dirName2 = `${dirName}2`; - await mkdir(dirName1); - await mkdir(dirName2); - - const secretList = await vaultOps.listSecrets(vault); - expect(secretList).toHaveLength(0); - }); - test('secrets in directories are listed', async () => { - const secretPath1 = path.join(dirName, `${secretName}1`); - const secretPath2 = path.join(dirName, `${secretName}2`); - await writeSecret(secretPath1, secretContent); - await writeSecret(secretPath2, secretContent); - - const secretList = await vaultOps.listSecrets(vault); - expect(secretList).toInclude(secretPath1); - expect(secretList).toInclude(secretPath2); - }); - test('empty vault list no secrets', async () => { - const secretList = await vaultOps.listSecrets(vault); - expect(secretList).toHaveLength(0); - }); - }); - describe('writeSecret', () => { - const secretName = 'secret'; - const secretContent = 'secret-content'; - const newSecretContent = 'updated-secret-content'; - - test('updates existing secret', async () => { - await vaultOps.addSecret(vault, secretName, secretContent); - await vaultOps.writeSecret(vault, secretName, newSecretContent); - const result = await vaultOps.getSecret(vault, secretName); - expect(result.toString()).toStrictEqual(newSecretContent); - }); - - test('creates new secret if it does not exist', async () => { - await vaultOps.writeSecret(vault, secretName, newSecretContent); - const result = await vaultOps.getSecret(vault, secretName); - expect(result.toString()).toStrictEqual(newSecretContent); - }); - }); - test('adding hidden files and directories', async () => { - await vaultOps.addSecret(vault, '.hiddenSecret', 'hidden_contents'); - await vaultOps.mkdir(vault, '.hiddenDir', { recursive: true }); - await vaultOps.addSecret( - vault, - '.hiddenDir/.hiddenInSecret', - 'hidden_inside', - ); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual( - ['.hiddenSecret', '.hiddenDir/.hiddenInSecret'].sort(), - ); - }); - test( - 'updating and deleting hidden files and directories', - async () => { - await vaultOps.addSecret(vault, '.hiddenSecret', 'hidden_contents'); - await vaultOps.mkdir(vault, '.hiddenDir', { recursive: true }); - await vaultOps.addSecret( - vault, - '.hiddenDir/.hiddenInSecret', - 'hidden_inside', - ); - await vaultOps.updateSecret(vault, '.hiddenSecret', 'change_contents'); - await vaultOps.updateSecret( - vault, - '.hiddenDir/.hiddenInSecret', - 'change_inside', - ); - await vaultOps.renameSecret(vault, '.hiddenSecret', '.hidingSecret'); - await vaultOps.renameSecret(vault, '.hiddenDir', '.hidingDir'); - let list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual( - ['.hidingSecret', '.hidingDir/.hiddenInSecret'].sort(), - ); - expect( - (await vaultOps.getSecret(vault, '.hidingSecret')).toString(), - ).toStrictEqual('change_contents'); - expect( - ( - await vaultOps.getSecret(vault, '.hidingDir/.hiddenInSecret') - ).toString(), - ).toStrictEqual('change_inside'); - await vaultOps.deleteSecret(vault, ['.hidingSecret'], { - recursive: true, - }); - await vaultOps.deleteSecret(vault, ['.hidingDir'], { recursive: true }); - list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([].sort()); - }, - globalThis.defaultTimeout * 4, - ); - - describe('fileTree', () => { - const relativeBase = '.'; - const dir1: string = 'dir1'; - const dir11: string = path.join(dir1, 'dir11'); - const file0b: string = 'file0.b'; - const file1a: string = path.join(dir1, 'file1.a'); - const file2b: string = path.join(dir1, 'file2.b'); - const file3a: string = path.join(dir11, 'file3.a'); - const file4b: string = path.join(dir11, 'file4.b'); - - beforeEach(async () => { - await vault.writeF(async (fs) => { - await fs.promises.mkdir(dir1); - await fs.promises.mkdir(dir11); - await fs.promises.writeFile(file0b, 'content-file0'); - await fs.promises.writeFile(file1a, 'content-file1'); - await fs.promises.writeFile(file2b, 'content-file2'); - await fs.promises.writeFile(file3a, 'content-file3'); - await fs.promises.writeFile(file4b, 'content-file4'); - }); - }); - - test('globWalk works with efs', async () => { - const files = await vault.readF(async (fs) => { - const tree: FileTree = []; - for await (const treeNode of fileTree.globWalk({ - fs: fs, - basePath: '.', - yieldDirectories: true, - yieldFiles: true, - yieldParents: true, - yieldRoot: true, - })) { - tree.push(treeNode); - } - return tree.map((v) => v.path ?? ''); - }); - expect(files).toContainAllValues([ - relativeBase, - dir1, - dir11, - file0b, - file1a, - file2b, - file3a, - file4b, - ]); - }); - test('serializer with content works with efs', async () => { - const data = await vault.readF(async (fs) => { - const fileTreeGen = fileTree.globWalk({ - fs, - yieldStats: false, - yieldRoot: false, - yieldFiles: true, - yieldParents: true, - yieldDirectories: true, - }); - const data: Array = []; - const parserTransform = fileTree.parserTransformStreamFactory(); - const serializedStream = fileTree.serializerStreamFactory( - fs, - fileTreeGen, - true, - ); - const outputStream = serializedStream.pipeThrough(parserTransform); - for await (const output of outputStream) { - data.push(output); - } - return data; - }); - const contents = data - .filter((v) => v instanceof Uint8Array) - .map((v) => Buffer.from(v as Uint8Array).toString()); - const contentHeaders = data.filter( - (v) => !(v instanceof Uint8Array) && v.type === 'CONTENT', - ) as Array; - expect(contents).toIncludeAllMembers([ - 'content-file0', - 'content-file1', - 'content-file2', - 'content-file3', - 'content-file4', - ]); - for (const contentHeader of contentHeaders) { - expect(contentHeader.dataSize).toBe(13n); - } - }); - }); -}); diff --git a/tests/vaults/VaultOps/addSecret.test.ts b/tests/vaults/VaultOps/addSecret.test.ts new file mode 100644 index 000000000..462eb6cbc --- /dev/null +++ b/tests/vaults/VaultOps/addSecret.test.ts @@ -0,0 +1,129 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('addSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('adding a secret', async () => { + await vaultOps.addSecret(vault, secretName, secretContent); + await testVaultsUtils.expectSecret(vault, secretName, secretContent); + }); + test('add a secret under an existing directory', async () => { + await testVaultsUtils.mkdir(vault, dirName); + const secretPath = path.join(dirName, secretName); + await vaultOps.addSecret(vault, secretPath, secretContent); + await testVaultsUtils.expectSecret(vault, secretPath, secretContent); + }); + test('add a secret creating directory', async () => { + const secretPath = path.join(dirName, secretName); + await vaultOps.addSecret(vault, secretPath, secretContent); + await testVaultsUtils.expectSecret(vault, secretPath, secretContent); + }); + test( + 'adding a secret multiple times', + async () => { + for (let i = 0; i < 5; i++) { + const name = `${secretName}+${i}`; + await vaultOps.addSecret(vault, name, secretContent); + await testVaultsUtils.expectSecret(vault, name, secretContent); + } + }, + globalThis.defaultTimeout * 4, + ); + test('adding a secret that already exists should fail', async () => { + await vaultOps.addSecret(vault, secretName, secretContent); + const addSecretP = vaultOps.addSecret(vault, secretName, secretContent); + await expect(addSecretP).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretDefined, + ); + }); + test('adding a hidden secret', async () => { + await vaultOps.addSecret(vault, secretNameHidden, secretContent); + const list = await vaultOps.listSecrets(vault); + expect(list).toContain(secretNameHidden); + }); +}); diff --git a/tests/vaults/VaultOps/addSecretDirectory.test.ts b/tests/vaults/VaultOps/addSecretDirectory.test.ts new file mode 100644 index 000000000..f7a8ff757 --- /dev/null +++ b/tests/vaults/VaultOps/addSecretDirectory.test.ts @@ -0,0 +1,196 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; + +describe('addSecretDirectory', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('adding a directory of 1 secret', async () => { + const secretDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'secret-directory-'), + ); + const secretDirName = path.basename(secretDir); + const name = 'secret'; + const content = keysUtils.getRandomBytes(5); + await fs.promises.writeFile(path.join(secretDir, name), content); + + await vaultOps.addSecretDirectory(vault, secretDir, fs); + await expect( + vault.readF((efs) => efs.readdir(secretDirName)), + ).resolves.toContain('secret'); + + await fs.promises.rm(secretDir, { + force: true, + recursive: true, + }); + }); + test('adding a directory with subdirectories and files', async () => { + const secretDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'secret-directory-'), + ); + const secretDirName = path.basename(secretDir); + await fs.promises.mkdir(path.join(secretDir, 'dir1')); + await fs.promises.mkdir(path.join(secretDir, 'dir1', 'dir2')); + await fs.promises.mkdir(path.join(secretDir, 'dir3')); + + await fs.promises.writeFile(path.join(secretDir, 'secret1'), 'secret1'); + await fs.promises.writeFile( + path.join(secretDir, 'dir1', 'secret2'), + 'secret2', + ); + await fs.promises.writeFile( + path.join(secretDir, 'dir1', 'dir2', 'secret3'), + 'secret3', + ); + await fs.promises.writeFile( + path.join(secretDir, 'dir3', 'secret4'), + 'secret4', + ); + await fs.promises.writeFile( + path.join(secretDir, 'dir3', 'secret5'), + 'secret5', + ); + + await vaultOps.addSecretDirectory(vault, path.join(secretDir), fs); + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual( + [ + path.join(secretDirName, 'secret1'), + path.join(secretDirName, 'dir1', 'secret2'), + path.join(secretDirName, 'dir1', 'dir2', 'secret3'), + path.join(secretDirName, 'dir3', 'secret4'), + path.join(secretDirName, 'dir3', 'secret5'), + ].sort(), + ); + + await fs.promises.rm(secretDir, { + force: true, + recursive: true, + }); + }); + test('testing the errors handling of adding secret directories', async () => { + const secretDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'secret-directory-'), + ); + const secretDirName = path.basename(secretDir); + await fs.promises.mkdir(path.join(secretDir, 'dir1')); + await fs.promises.mkdir(path.join(secretDir, 'dir1', 'dir2')); + await fs.promises.mkdir(path.join(secretDir, 'dir3')); + await fs.promises.writeFile(path.join(secretDir, 'secret1'), 'secret1'); + await fs.promises.writeFile( + path.join(secretDir, 'dir1', 'secret2'), + 'secret2', + ); + await fs.promises.writeFile( + path.join(secretDir, 'dir1', 'dir2', 'secret3'), + 'secret3', + ); + await fs.promises.writeFile( + path.join(secretDir, 'dir3', 'secret4'), + 'secret4', + ); + await fs.promises.writeFile( + path.join(secretDir, 'dir3', 'secret5'), + 'secret5', + ); + + await vaultOps.mkdir(vault, secretDirName, { recursive: true }); + await vaultOps.addSecret( + vault, + path.join(secretDirName, 'secret1'), + 'blocking-secret', + ); + await vaultOps.addSecretDirectory(vault, secretDir, fs); + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual( + [ + path.join(secretDirName, 'secret1'), + path.join(secretDirName, 'dir1', 'secret2'), + path.join(secretDirName, 'dir1', 'dir2', 'secret3'), + path.join(secretDirName, 'dir3', 'secret4'), + path.join(secretDirName, 'dir3', 'secret5'), + ].sort(), + ); + + await fs.promises.rm(secretDir, { + force: true, + recursive: true, + }); + }); +}); diff --git a/tests/vaults/VaultOps/deleteSecret.test.ts b/tests/vaults/VaultOps/deleteSecret.test.ts new file mode 100644 index 000000000..4ffbe1cdd --- /dev/null +++ b/tests/vaults/VaultOps/deleteSecret.test.ts @@ -0,0 +1,166 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('deleteSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + const dirNameHidden = '.dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('deleting a secret', async () => { + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + await vaultOps.deleteSecret(vault, [secretName]); + await testVaultsUtils.expectSecretNot(vault, secretName); + }); + test('deleting a secret in a directory', async () => { + const secretPath = path.join(dirName, secretName); + await testVaultsUtils.writeSecret(vault, secretPath, secretContent); + await vaultOps.deleteSecret(vault, [secretPath]); + await testVaultsUtils.expectSecretNot(vault, secretPath); + await testVaultsUtils.expectDirExists(vault, dirName); + }); + test('deleting a directory', async () => { + await testVaultsUtils.mkdir(vault, dirName); + await vaultOps.deleteSecret(vault, [dirName]); + await testVaultsUtils.expectDirExistsNot(vault, dirName); + }); + test('deleting a directory with a file should fail', async () => { + const secretPath = path.join(dirName, secretName); + await testVaultsUtils.writeSecret(vault, secretPath, secretContent); + await expect(vaultOps.deleteSecret(vault, [dirName])).rejects.toThrow( + vaultsErrors.ErrorVaultsRecursive, + ); + }); + test('deleting a directory with force', async () => { + const secretPath = path.join(dirName, secretName); + await testVaultsUtils.writeSecret(vault, secretPath, secretContent); + await vaultOps.deleteSecret(vault, [dirName], { recursive: true }); + await testVaultsUtils.expectDirExistsNot(vault, dirName); + }); + test('deleting a secret that does not exist should fail', async () => { + await expect(vaultOps.deleteSecret(vault, [secretName])).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretUndefined, + ); + }); + test('deleting multiple secrets', async () => { + const secretNames = ['secret1', 'secret2', 'secret3']; + for (const secretName of secretNames) { + await testVaultsUtils.writeSecret(vault, secretName, secretName); + } + await vaultOps.deleteSecret(vault, secretNames); + for (const secretName of secretNames) { + await testVaultsUtils.expectSecretNot(vault, secretName); + } + }); + test('deleting multiple secrets should add only one new log message', async () => { + const secretNames = ['secret1', 'secret2', 'secret3']; + for (const secretName of secretNames) { + await testVaultsUtils.writeSecret(vault, secretName, secretName); + } + const logLength = (await vault.log()).length; + await vaultOps.deleteSecret(vault, secretNames); + for (const secretName of secretNames) { + await testVaultsUtils.expectSecretNot(vault, secretName); + } + expect((await vault.log()).length).toBe(logLength + 1); + }); + test('deleting a hidden secret', async () => { + await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); + await vaultOps.deleteSecret(vault, [secretNameHidden]); + await testVaultsUtils.expectSecretNot(vault, secretNameHidden); + }); + test('deleting a hidden secret in a hidden directory', async () => { + const secretPathHidden = path.join(dirNameHidden, secretNameHidden); + await testVaultsUtils.writeSecret(vault, secretPathHidden, secretContent); + await vaultOps.deleteSecret(vault, [secretPathHidden]); + await testVaultsUtils.expectSecretNot(vault, secretPathHidden); + await testVaultsUtils.expectDirExists(vault, dirNameHidden); + }); + test('deleting a hidden directory', async () => { + await testVaultsUtils.mkdir(vault, dirNameHidden); + await vaultOps.deleteSecret(vault, [dirNameHidden]); + await testVaultsUtils.expectDirExistsNot(vault, dirNameHidden); + }); +}); diff --git a/tests/vaults/VaultOps/getSecret.test.ts b/tests/vaults/VaultOps/getSecret.test.ts new file mode 100644 index 000000000..bca1f1fd7 --- /dev/null +++ b/tests/vaults/VaultOps/getSecret.test.ts @@ -0,0 +1,112 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('getSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('can get a secret', async () => { + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + const secret = await vaultOps.getSecret(vault, secretName); + expect(secret.toString()).toBe(secretContent); + }); + test('getting a secret that does not exist should fail', async () => { + await expect(vaultOps.getSecret(vault, secretName)).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretUndefined, + ); + }); + test('getting a directory should fail', async () => { + await testVaultsUtils.mkdir(vault, dirName); + await expect(vaultOps.getSecret(vault, dirName)).rejects.toThrow( + vaultsErrors.ErrorSecretsIsDirectory, + ); + }); + test('can get a hidden secret', async () => { + await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); + const secret = await vaultOps.getSecret(vault, secretNameHidden); + expect(secret.toString()).toBe(secretContent); + }); +}); diff --git a/tests/vaults/VaultOps/listSecrets.test.ts b/tests/vaults/VaultOps/listSecrets.test.ts new file mode 100644 index 000000000..56f3aff0c --- /dev/null +++ b/tests/vaults/VaultOps/listSecrets.test.ts @@ -0,0 +1,122 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('listSecrets', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('can list secrets', async () => { + const secretName1 = `${secretName}1`; + const secretName2 = `${secretName}2`; + await testVaultsUtils.writeSecret(vault, secretName1, secretContent); + await testVaultsUtils.writeSecret(vault, secretName2, secretContent); + + const secretList = await vaultOps.listSecrets(vault); + expect(secretList).toInclude(secretName1); + expect(secretList).toInclude(secretName2); + }); + test('empty directories are not listed', async () => { + const dirName1 = `${dirName}1`; + const dirName2 = `${dirName}2`; + await testVaultsUtils.mkdir(vault, dirName1); + await testVaultsUtils.mkdir(vault, dirName2); + + const secretList = await vaultOps.listSecrets(vault); + expect(secretList).toHaveLength(0); + }); + test('secrets in directories are listed', async () => { + const secretPath1 = path.join(dirName, `${secretName}1`); + const secretPath2 = path.join(dirName, `${secretName}2`); + await testVaultsUtils.writeSecret(vault, secretPath1, secretContent); + await testVaultsUtils.writeSecret(vault, secretPath2, secretContent); + + const secretList = await vaultOps.listSecrets(vault); + expect(secretList).toInclude(secretPath1); + expect(secretList).toInclude(secretPath2); + }); + test('empty vault list no secrets', async () => { + const secretList = await vaultOps.listSecrets(vault); + expect(secretList).toHaveLength(0); + }); +}); diff --git a/tests/vaults/VaultOps/mkdir.test.ts b/tests/vaults/VaultOps/mkdir.test.ts new file mode 100644 index 000000000..9e0e0c0a1 --- /dev/null +++ b/tests/vaults/VaultOps/mkdir.test.ts @@ -0,0 +1,132 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import type { ErrorMessage } from '@/client/types'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('mkdir', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + const dirNameHidden = '.dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('can create directory', async () => { + const response = await vaultOps.mkdir(vault, dirName); + expect(response.type).toEqual('success'); + await testVaultsUtils.expectDirExists(vault, dirName); + }); + test('can create recursive directory', async () => { + const dirPath = path.join(dirName, dirName); + const response = await vaultOps.mkdir(vault, dirPath, { + recursive: true, + }); + expect(response.type).toEqual('success'); + await testVaultsUtils.expectDirExists(vault, dirPath); + }); + test('creating directories fails without recursive', async () => { + const dirPath = path.join(dirName, dirName); + const response = await vaultOps.mkdir(vault, dirPath); + expect(response.type).toEqual('error'); + const error = response as ErrorMessage; + expect(error.code).toEqual('ENOENT'); + await testVaultsUtils.expectDirExistsNot(vault, dirPath); + }); + test('creating existing directory should fail', async () => { + await testVaultsUtils.mkdir(vault, dirName); + const response = await vaultOps.mkdir(vault, dirName); + expect(response.type).toEqual('error'); + const error = response as ErrorMessage; + expect(error.code).toEqual('EEXIST'); + }); + test('creating existing secret should fail', async () => { + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + const response = await vaultOps.mkdir(vault, secretName); + expect(response.type).toEqual('error'); + const error = response as ErrorMessage; + expect(error.code).toEqual('EEXIST'); + await testVaultsUtils.expectSecret(vault, secretName, secretContent); + }); + test('can create a hidden directory', async () => { + const response = await vaultOps.mkdir(vault, dirNameHidden); + expect(response.type).toEqual('success'); + await testVaultsUtils.expectDirExists(vault, dirNameHidden); + }); +}); diff --git a/tests/vaults/VaultOps/renameSecret.test.ts b/tests/vaults/VaultOps/renameSecret.test.ts new file mode 100644 index 000000000..adb8c43bb --- /dev/null +++ b/tests/vaults/VaultOps/renameSecret.test.ts @@ -0,0 +1,126 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('renameSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretNameNew = 'secret-new'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + const dirNameHidden = '.dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('renaming a secret', async () => { + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + await vaultOps.renameSecret(vault, secretName, secretNameNew); + await testVaultsUtils.expectSecretNot(vault, secretName); + await testVaultsUtils.expectSecret(vault, secretNameNew, secretContent); + }); + test('renaming a secret within a directory', async () => { + const secretPath = path.join(dirName, secretName); + const secretPathNew = path.join(dirName, secretNameNew); + await testVaultsUtils.writeSecret(vault, secretPath, secretContent); + await vaultOps.renameSecret(vault, secretPath, secretPathNew); + await testVaultsUtils.expectSecretNot(vault, secretPath); + await testVaultsUtils.expectSecret(vault, secretPathNew, secretContent); + }); + test('renaming a secret that does not exist should fail', async () => { + await expect( + vaultOps.renameSecret(vault, secretName, secretNameNew), + ).rejects.toThrow(vaultsErrors.ErrorSecretsSecretUndefined); + }); + test('renaming a hidden secret', async () => { + await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); + await vaultOps.renameSecret(vault, secretNameHidden, secretNameNew); + await testVaultsUtils.expectSecretNot(vault, secretNameHidden); + await testVaultsUtils.expectSecret(vault, secretNameNew, secretContent); + }); + test('renaming a hidden secret within a hidden directory', async () => { + const secretPathHidden = path.join(dirNameHidden, secretNameHidden); + const secretPathNew = path.join(dirNameHidden, secretNameNew); + await testVaultsUtils.writeSecret(vault, secretPathHidden, secretContent); + await vaultOps.renameSecret(vault, secretPathHidden, secretPathNew); + await testVaultsUtils.expectSecretNot(vault, secretPathHidden); + await testVaultsUtils.expectSecret(vault, secretPathNew, secretContent); + }); +}); diff --git a/tests/vaults/VaultOps/statSecret.test.ts b/tests/vaults/VaultOps/statSecret.test.ts new file mode 100644 index 000000000..89c2165a0 --- /dev/null +++ b/tests/vaults/VaultOps/statSecret.test.ts @@ -0,0 +1,113 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS, Stat } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('statSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretContent = 'secret-content'; + const dirName = 'dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('can get stat of a secret', async () => { + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + const stat = await vaultOps.statSecret(vault, secretName); + expect(stat).toBeInstanceOf(Stat); + expect(stat.nlink).toBe(1); + }); + test('can get stat of a directory', async () => { + await testVaultsUtils.mkdir(vault, dirName); + const stat = await vaultOps.statSecret(vault, dirName); + expect(stat).toBeInstanceOf(Stat); + }); + test('getting stat of secret that does not exist should fail', async () => { + await expect(vaultOps.statSecret(vault, secretName)).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretUndefined, + ); + }); + test('can get stat of a hidden secret', async () => { + await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); + const stat = await vaultOps.statSecret(vault, secretNameHidden); + expect(stat).toBeInstanceOf(Stat); + expect(stat.nlink).toBe(1); + }); +}); diff --git a/tests/vaults/VaultOps/updatesecret.test.ts b/tests/vaults/VaultOps/updatesecret.test.ts new file mode 100644 index 000000000..89befde9a --- /dev/null +++ b/tests/vaults/VaultOps/updatesecret.test.ts @@ -0,0 +1,141 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsErrors from '@/vaults/errors'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; +import * as testVaultsUtils from '../utils'; + +describe('updateSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretContent = 'secret-content'; + const secretContentNew = 'secret-content-new'; + const dirName = 'dir'; + const dirNameHidden = '.dir'; + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('updating secret content', async () => { + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + await vaultOps.updateSecret(vault, secretName, secretContentNew); + await testVaultsUtils.expectSecret(vault, secretName, secretContentNew); + }); + test('updating secret content within a directory', async () => { + const secretPath = path.join(dirName, secretName); + await testVaultsUtils.writeSecret(vault, secretPath, secretContent); + await vaultOps.updateSecret(vault, secretPath, secretContentNew); + await testVaultsUtils.expectSecret(vault, secretPath, secretContentNew); + }); + test( + 'updating a secret multiple times', + async () => { + await vaultOps.addSecret(vault, 'secret-1', 'secret-content'); + await testVaultsUtils.writeSecret(vault, secretName, secretContent); + for (let i = 0; i < 5; i++) { + const contentNew = `${secretContentNew}${i}`; + await vaultOps.updateSecret(vault, secretName, contentNew); + await testVaultsUtils.expectSecret(vault, secretName, contentNew); + } + }, + globalThis.defaultTimeout * 2, + ); + test('updating a secret that does not exist should fail', async () => { + await expect( + vaultOps.updateSecret(vault, secretName, secretContentNew), + ).rejects.toThrow(vaultsErrors.ErrorSecretsSecretUndefined); + }); + test('updating hidden secret content', async () => { + await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); + await vaultOps.updateSecret(vault, secretNameHidden, secretContentNew); + await testVaultsUtils.expectSecret( + vault, + secretNameHidden, + secretContentNew, + ); + }); + test('updating hidden secret content within a hidden directory', async () => { + const secretPathHidden = path.join(dirNameHidden, secretNameHidden); + await testVaultsUtils.writeSecret(vault, secretPathHidden, secretContent); + await vaultOps.updateSecret(vault, secretPathHidden, secretContentNew); + await testVaultsUtils.expectSecret( + vault, + secretPathHidden, + secretContentNew, + ); + }); +}); diff --git a/tests/vaults/VaultOps/writeSecret.test.ts b/tests/vaults/VaultOps/writeSecret.test.ts new file mode 100644 index 000000000..c18f52253 --- /dev/null +++ b/tests/vaults/VaultOps/writeSecret.test.ts @@ -0,0 +1,105 @@ +import type { VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults/Vault'; +import type KeyRing from '@/keys/KeyRing'; +import type { LevelPath } from '@matrixai/db'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { EncryptedFS } from 'encryptedfs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import VaultInternal from '@/vaults/VaultInternal'; +import * as vaultOps from '@/vaults/VaultOps'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testNodesUtils from '../../nodes/utils'; + +describe('writeSecret', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); + + let dataDir: string; + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + const secretName = 'secret'; + const secretNameHidden = '.secret'; + const secretContent = 'secret-content'; + const newSecretContent = 'updated-secret-content'; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('updates existing secret', async () => { + await vaultOps.addSecret(vault, secretName, secretContent); + await vaultOps.writeSecret(vault, secretName, newSecretContent); + const result = await vaultOps.getSecret(vault, secretName); + expect(result.toString()).toStrictEqual(newSecretContent); + }); + test('creates new secret if it does not exist', async () => { + await vaultOps.writeSecret(vault, secretName, newSecretContent); + const result = await vaultOps.getSecret(vault, secretName); + expect(result.toString()).toStrictEqual(newSecretContent); + }); + test('updates existing hidden secret', async () => { + await vaultOps.addSecret(vault, secretNameHidden, secretContent); + await vaultOps.writeSecret(vault, secretNameHidden, newSecretContent); + const result = await vaultOps.getSecret(vault, secretNameHidden); + expect(result.toString()).toStrictEqual(newSecretContent); + }); +}); diff --git a/tests/vaults/fileTree.test.ts b/tests/vaults/fileTree.test.ts index f47831c72..62cd2a288 100644 --- a/tests/vaults/fileTree.test.ts +++ b/tests/vaults/fileTree.test.ts @@ -1,14 +1,24 @@ -import type { ContentNode, FileTree, TreeNode } from '@/vaults/types'; +import type { ContentNode, FileTree, TreeNode, VaultId } from '@/vaults/types'; +import type { Vault } from '@/vaults'; +import type KeyRing from '../../src/keys/KeyRing'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { ReadableStream } from 'stream/web'; import { test } from '@fast-check/jest'; import fc from 'fast-check'; +import { EncryptedFS } from 'encryptedfs'; +import { DB, type LevelPath } from '@matrixai/db'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import * as fileTree from '@/vaults/fileTree'; +import * as vaultsUtils from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; import * as vaultsTestUtils from './utils'; +import VaultInternal from '../../src/vaults/VaultInternal'; +import * as testNodesUtils from '../nodes/utils'; describe('fileTree', () => { + const logger = new Logger('VaultOps', LogLevel.WARN, [new StreamHandler()]); let dataDir: string; beforeEach(async () => { @@ -719,4 +729,151 @@ describe('fileTree', () => { // - empty files // - files larger than content chunks }); + describe('with EFS', () => { + const relativeBase = '.'; + const dir1: string = 'dir1'; + const dir11: string = path.join(dir1, 'dir11'); + const file0b: string = 'file0.b'; + const file1a: string = path.join(dir1, 'file1.a'); + const file2b: string = path.join(dir1, 'file2.b'); + const file3a: string = path.join(dir11, 'file3.a'); + const file4b: string = path.join(dir11, 'file4.b'); + + let baseEfs: EncryptedFS; + let vaultId: VaultId; + let vaultInternal: VaultInternal; + let vault: Vault; + let db: DB; + let vaultsDbPath: LevelPath; + const vaultIdGenerator = vaultsUtils.createVaultIdGenerator(); + const dummyKeyRing = { + getNodeId: () => { + return testNodesUtils.generateRandomNodeId(); + }, + } as KeyRing; + + beforeEach(async () => { + const dbPath = path.join(dataDir, 'efsDb'); + const dbKey = keysUtils.generateKey(); + baseEfs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath, + logger, + }); + await baseEfs.start(); + + vaultId = vaultIdGenerator(); + await baseEfs.mkdir( + path.join(vaultsUtils.encodeVaultId(vaultId), 'contents'), + { + recursive: true, + }, + ); + db = await DB.createDB({ + dbPath: path.join(dataDir, 'db'), + logger, + }); + vaultsDbPath = ['vaults']; + vaultInternal = await VaultInternal.createVaultInternal({ + keyRing: dummyKeyRing, + vaultId, + efs: baseEfs, + logger: logger.getChild(VaultInternal.name), + fresh: true, + db, + vaultsDbPath: vaultsDbPath, + vaultName: 'VaultName', + }); + vault = vaultInternal as Vault; + + await vault.writeF(async (fs) => { + await fs.promises.mkdir(dir1); + await fs.promises.mkdir(dir11); + await fs.promises.writeFile(file0b, 'content-file0'); + await fs.promises.writeFile(file1a, 'content-file1'); + await fs.promises.writeFile(file2b, 'content-file2'); + await fs.promises.writeFile(file3a, 'content-file3'); + await fs.promises.writeFile(file4b, 'content-file4'); + }); + }); + afterEach(async () => { + await vaultInternal.stop(); + await vaultInternal.destroy(); + await db.stop(); + await db.destroy(); + await baseEfs.stop(); + await baseEfs.destroy(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('globWalk works with efs', async () => { + const files = await vault.readF(async (fs) => { + const tree: FileTree = []; + for await (const treeNode of fileTree.globWalk({ + fs: fs, + basePath: '.', + yieldDirectories: true, + yieldFiles: true, + yieldParents: true, + yieldRoot: true, + })) { + tree.push(treeNode); + } + return tree.map((v) => v.path ?? ''); + }); + expect(files).toContainAllValues([ + relativeBase, + dir1, + dir11, + file0b, + file1a, + file2b, + file3a, + file4b, + ]); + }); + test('serializer with content works with efs', async () => { + const data = await vault.readF(async (fs) => { + const fileTreeGen = fileTree.globWalk({ + fs, + yieldStats: false, + yieldRoot: false, + yieldFiles: true, + yieldParents: true, + yieldDirectories: true, + }); + const data: Array = []; + const parserTransform = fileTree.parserTransformStreamFactory(); + const serializedStream = fileTree.serializerStreamFactory( + fs, + fileTreeGen, + true, + ); + const outputStream = serializedStream.pipeThrough(parserTransform); + for await (const output of outputStream) { + data.push(output); + } + return data; + }); + const contents = data + .filter((v) => v instanceof Uint8Array) + .map((v) => Buffer.from(v as Uint8Array).toString()); + const contentHeaders = data.filter( + (v) => !(v instanceof Uint8Array) && v.type === 'CONTENT', + ) as Array; + expect(contents).toIncludeAllMembers([ + 'content-file0', + 'content-file1', + 'content-file2', + 'content-file3', + 'content-file4', + ]); + for (const contentHeader of contentHeaders) { + expect(contentHeader.dataSize).toBe(13n); + } + }); + }); }); diff --git a/tests/vaults/utils.ts b/tests/vaults/utils.ts index 797374ae3..30b210034 100644 --- a/tests/vaults/utils.ts +++ b/tests/vaults/utils.ts @@ -1,12 +1,15 @@ +import type { Vault } from '@/vaults'; import type { VaultActions, HeaderContent, HeaderGeneric, } from '@/vaults/types'; import { TransformStream } from 'stream/web'; +import path from 'path'; import fc from 'fast-check'; import { vaultActions } from '@/vaults/types'; import { HeaderType } from '@/vaults/fileTree'; +import * as vaultsUtils from '@/vaults/utils'; const vaultActionArb = fc.constantFrom(...vaultActions); @@ -55,6 +58,57 @@ function binaryStreamToSnippedStream( }); } +async function writeSecret(vault: Vault, secretPath: string, contents: string) { + return await vault.writeF(async (efs) => { + await vaultsUtils.mkdirExists(efs, path.dirname(secretPath)); + await efs.writeFile(secretPath, contents); + }); +} + +async function readSecret(vault: Vault, path: string) { + return await vault.readF(async (efs) => { + return await efs.readFile(path); + }); +} + +async function expectSecret( + vault: Vault, + path: string, + contentsExpected: string, +) { + const contentsSecretP = readSecret(vault, path); + await expect(contentsSecretP).resolves.toBeDefined(); + const contentsSecretValue = (await contentsSecretP).toString(); + expect(contentsSecretValue).toBe(contentsExpected); +} + +async function expectSecretNot(vault: Vault, path: string) { + const contentsSecretP = readSecret(vault, path); + await expect(contentsSecretP).rejects.toThrow( + 'ENOENT: no such file or directory', + ); +} + +async function mkdir(vault: Vault, path: string) { + return await vault.writeF(async (efs) => { + await vaultsUtils.mkdirExists(efs, path); + }); +} + +async function expectDirExists(vault: Vault, path: string) { + return await vault.readF(async (efs) => { + const dirP = efs.readdir(path); + await expect(dirP).resolves.toBeDefined(); + }); +} + +async function expectDirExistsNot(vault: Vault, path: string) { + return await vault.readF(async (efs) => { + const dirP = efs.readdir(path); + await expect(dirP).rejects.toThrow('ENOENT'); + }); +} + export { vaultActionArb, vaultActionsArb, @@ -62,4 +116,11 @@ export { headerGenericArb, headerContentArb, binaryStreamToSnippedStream, + writeSecret, + readSecret, + expectSecret, + expectSecretNot, + mkdir, + expectDirExists, + expectDirExistsNot, };