diff --git a/packages/backend-test-utils/src/files/DirectoryMocker.test.ts b/packages/backend-test-utils/src/files/DirectoryMocker.test.ts index cb0e52125c..1a50e38b47 100644 --- a/packages/backend-test-utils/src/files/DirectoryMocker.test.ts +++ b/packages/backend-test-utils/src/files/DirectoryMocker.test.ts @@ -59,6 +59,66 @@ describe('DirectoryMocker', () => { }); }); + it('should be able to add content', async () => { + await mocker.setContent({ + 'a.txt': 'a', + b: {}, + }); + + await expect(mocker.getContent()).resolves.toEqual({ + 'a.txt': 'a', + b: {}, + }); + + await mocker.addContent({ + 'b.txt': 'b', + b: { + 'c.txt': 'c', + }, + }); + + await expect(mocker.getContent()).resolves.toEqual({ + 'a.txt': 'a', + 'b.txt': 'b', + b: { + 'c.txt': 'c', + }, + }); + }); + + it('should replace existing files', async () => { + await mocker.setContent({ + 'a.txt': 'a', + }); + + await mocker.addContent({ + 'a.txt': 'a2', + }); + + await expect(mocker.getContent()).resolves.toEqual({ + 'a.txt': 'a2', + }); + }); + + it('should not override directories', async () => { + await mocker.setContent({ + 'a.txt': 'a', + b: {}, + }); + + await expect( + mocker.addContent({ + 'a.txt': {}, + }), + ).rejects.toThrow('EEXIST'); + + await expect( + mocker.addContent({ + b: 'b', + }), + ).rejects.toThrow('EISDIR'); + }); + describe('cleanup', () => { let cleanupMocker: DirectoryMocker; diff --git a/packages/backend-test-utils/src/files/DirectoryMocker.ts b/packages/backend-test-utils/src/files/DirectoryMocker.ts index 7bf23aa2de..87538183ec 100644 --- a/packages/backend-test-utils/src/files/DirectoryMocker.ts +++ b/packages/backend-test-utils/src/files/DirectoryMocker.ts @@ -25,10 +25,16 @@ import { resolve as resolvePath, } from 'path'; -type MockEntry = { - path: string; - content: Buffer; -}; +type MockEntry = + | { + type: 'file'; + path: string; + content: Buffer; + } + | { + type: 'dir'; + path: string; + }; export type MockDirectory = { [name in string]: MockDirectory | string | Buffer; @@ -65,7 +71,7 @@ export class DirectoryMocker { process.on('beforeExit', mocker.#cleanupSync); try { - afterAll(mocker.cleanup); + afterAll(mocker.removeContent); } catch { /* ignore */ } @@ -85,14 +91,23 @@ export class DirectoryMocker { } async setContent(root: MockDirectory) { + await this.removeContent(); + + return this.addContent(root); + } + + async addContent(root: MockDirectory) { const entries = this.#transformInput(root); - await this.cleanup(); + for (const entry of entries) { + const fullPath = resolveSafeChildPath(this.#root, entry.path.slice(1)); // trim leading slash - for (const { path, content } of entries) { - const fullPath = resolveSafeChildPath(this.#root, path.slice(1)); // trim leading slash - await fs.ensureDir(dirname(fullPath)); - await fs.writeFile(fullPath, content); + if (entry.type === 'dir') { + await fs.ensureDir(fullPath); + } else if (entry.type === 'file') { + await fs.ensureDir(dirname(fullPath)); + await fs.writeFile(fullPath, entry.content); + } } } @@ -104,27 +119,27 @@ export class DirectoryMocker { ? () => options?.shouldReadAsText : options?.shouldReadAsText) ?? ((path: string) => textextensions.includes(extname(path).slice(1))); + async function read(path: string): Promise { if (!(await fs.pathExists(path))) { return undefined; } - const entries = await fs.readdir(path); + const entries = await fs.readdir(path, { withFileTypes: true }); return Object.fromEntries( await Promise.all( entries.map(async entry => { - const fullPath = resolvePath(path, entry); - const stat = await fs.stat(fullPath); + const fullPath = resolvePath(path, entry.name); - if (stat.isDirectory()) { - return [entry, await read(fullPath)]; + if (entry.isDirectory()) { + return [entry.name, await read(fullPath)]; } const content = await fs.readFile(fullPath); if (shouldReadAsText(fullPath, content)) { - return [entry, content.toString('utf8')]; + return [entry.name, content.toString('utf8')]; } - return [entry, content]; + return [entry.name, content]; }), ), ); @@ -133,15 +148,24 @@ export class DirectoryMocker { return read(this.#root); } + removeContent = async () => { + await fs.rm(this.#root, { recursive: true, force: true }); + }; + #transformInput(input: MockDirectory[string]): MockEntry[] { const entries: MockEntry[] = []; function traverse(node: MockDirectory[string], path: string) { if (typeof node === 'string') { - entries.push({ path, content: Buffer.from(node, 'utf8') }); + entries.push({ + type: 'file', + path, + content: Buffer.from(node, 'utf8'), + }); } else if (node instanceof Buffer) { - entries.push({ path, content: node }); + entries.push({ type: 'file', path, content: node }); } else { + entries.push({ type: 'dir', path }); for (const [name, child] of Object.entries(node)) { traverse(child, `${path}/${name}`); } @@ -153,10 +177,6 @@ export class DirectoryMocker { return entries; } - cleanup = async () => { - await fs.rm(this.#root, { recursive: true, force: true }); - }; - #cleanupSync = () => { fs.rmSync(this.#root, { recursive: true, force: true }); };