import { describe, it, expect } from 'vitest'; import type { Logger } from '@/application/Common/Log/Logger'; import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub'; import type { FunctionKeys } from '@/TypeHelpers'; import { sequenceEqual } from '@/application/Common/Array'; import type { FileWriteErrorType } from '@/infrastructure/FileSystem/ReadbackFileWriter/ReadbackFileWriter'; import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import { NodeReadbackFileWriter } from '@/infrastructure/FileSystem/ReadbackFileWriter/NodeReadbackFileWriter'; import { FileSystemOperationsStub } from '@tests/unit/shared/Stubs/FileSystemOperationsStub'; import type { FileSystemOperations } from '@/infrastructure/FileSystem/FileSystemOperations'; describe('NodeReadbackFileWriter', () => { describe('writeAndVerifyFile', () => { describe('successful write and verify operations', () => { it('confirms successful operation', async () => { // arrange const context = new NodeReadbackFileWriterTestSetup(); // act const { success } = await context.writeAndVerifyFile(); // assert expect(success).to.equal(true); }); describe('file write operations', () => { it('writes to specified path', async () => { // arrange const expectedFilePath = 'test.txt'; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFilePath(expectedFilePath) .withFileSystem(fileSystemStub); // act await context.writeAndVerifyFile(); // assert const fileWriteCalls = fileSystemStub.callHistory.filter((c) => c.methodName === 'writeFile'); expect(fileWriteCalls).to.have.lengthOf(1); const [actualFilePath] = fileWriteCalls[0].args; expect(actualFilePath).to.equal(expectedFilePath); }); it('writes specified contents', async () => { // arrange const expectedFileContents = 'expected file contents'; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFileSystem(fileSystemStub) .withFileContents(expectedFileContents); // act await context.writeAndVerifyFile(); // assert const fileWriteCalls = fileSystemStub.callHistory.filter((c) => c.methodName === 'writeFile'); expect(fileWriteCalls).to.have.lengthOf(1); const [,actualFileContents] = fileWriteCalls[0].args; expect(actualFileContents).to.equal(expectedFileContents); }); it('uses correct encoding', async () => { // arrange const expectedEncoding: NodeJS.BufferEncoding = 'utf-8'; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFileSystem(fileSystemStub); // act await context.writeAndVerifyFile(); // assert const fileWriteCalls = fileSystemStub.callHistory.filter((c) => c.methodName === 'writeFile'); expect(fileWriteCalls).to.have.lengthOf(1); const [,,actualEncoding] = fileWriteCalls[0].args; expect(actualEncoding).to.equal(expectedEncoding); }); }); describe('existence verification', () => { it('checks correct path', async () => { // arrange const expectedFilePath = 'test-file-path'; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFileSystem(fileSystemStub) .withFilePath(expectedFilePath); // act await context.writeAndVerifyFile(); // assert const accessCalls = fileSystemStub.callHistory.filter((c) => c.methodName === 'isFileAvailable'); expect(accessCalls).to.have.lengthOf(1); const [actualFilePath] = accessCalls[0].args; expect(actualFilePath).to.equal(expectedFilePath); }); }); describe('content verification', () => { it('reads from correct path', async () => { // arrange const expectedFilePath = 'expected-file-path.txt'; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFileSystem(fileSystemStub) .withFilePath(expectedFilePath); // act await context.writeAndVerifyFile(); // assert const fileReadCalls = fileSystemStub.callHistory.filter((c) => c.methodName === 'readFile'); expect(fileReadCalls).to.have.lengthOf(1); const [actualFilePath] = fileReadCalls[0].args; expect(actualFilePath).to.equal(expectedFilePath); }); it('uses correct encoding', async () => { // arrange const expectedEncoding: NodeJS.BufferEncoding = 'utf-8'; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFileSystem(fileSystemStub); // act await context.writeAndVerifyFile(); // assert const fileReadCalls = fileSystemStub.callHistory.filter((c) => c.methodName === 'readFile'); expect(fileReadCalls).to.have.lengthOf(1); const [,actualEncoding] = fileReadCalls[0].args; expect(actualEncoding).to.equal(expectedEncoding); }); }); it('executes file system operations in correct sequence', async () => { // arrange const expectedOrder: ReadonlyArray> = [ 'writeFile', 'isFileAvailable', 'readFile', ]; const fileSystemStub = new FileSystemOperationsStub(); const context = new NodeReadbackFileWriterTestSetup() .withFileSystem(fileSystemStub); // act await context.writeAndVerifyFile(); // assert const actualOrder = fileSystemStub.callHistory.map((c) => c.methodName); expect(sequenceEqual(expectedOrder, actualOrder)).to.equal(true); }); }); describe('error handling', () => { const testScenarios: ReadonlyArray<{ readonly description: string; readonly expectedErrorType: FileWriteErrorType; readonly expectedErrorMessage: string; buildFaultyContext( setup: NodeReadbackFileWriterTestSetup, errorMessage: string, ): NodeReadbackFileWriterTestSetup; }> = [ { description: 'writing failure', expectedErrorType: 'WriteOperationFailed', expectedErrorMessage: 'Error when writing file', buildFaultyContext: (setup, errorMessage) => { const fileSystemStub = new FileSystemOperationsStub(); fileSystemStub.writeFile = () => Promise.reject(errorMessage); return setup .withFileSystem(fileSystemStub); }, }, { description: 'existence verification throws error', expectedErrorType: 'FileExistenceVerificationFailed', expectedErrorMessage: 'Access denied', buildFaultyContext: (setup, errorMessage) => { const fileSystemStub = new FileSystemOperationsStub(); fileSystemStub.isFileAvailable = () => Promise.reject(errorMessage); return setup .withFileSystem(fileSystemStub); }, }, { description: 'existence verification returnf alse', expectedErrorType: 'FileExistenceVerificationFailed', expectedErrorMessage: 'File does not exist.', buildFaultyContext: (setup) => { const fileSystemStub = new FileSystemOperationsStub(); fileSystemStub.isFileAvailable = () => Promise.resolve(false); return setup .withFileSystem(fileSystemStub); }, }, { description: 'reading failure', expectedErrorType: 'ReadVerificationFailed', expectedErrorMessage: 'Read error', buildFaultyContext: (setup, errorMessage) => { const fileSystemStub = new FileSystemOperationsStub(); fileSystemStub.readFile = () => Promise.reject(errorMessage); return setup .withFileSystem(fileSystemStub); }, }, { description: 'content match failure', expectedErrorType: 'ContentVerificationFailed', expectedErrorMessage: 'The contents of the written file do not match the expected contents.', buildFaultyContext: (setup) => { const fileSystemStub = new FileSystemOperationsStub(); fileSystemStub.readFile = () => Promise.resolve('different contents'); return setup .withFileSystem(fileSystemStub); }, }, ]; testScenarios.forEach(({ description, expectedErrorType, expectedErrorMessage, buildFaultyContext, }) => { it(`handles error - ${description}`, async () => { // arrange const context = buildFaultyContext( new NodeReadbackFileWriterTestSetup(), expectedErrorMessage, ); // act const { success, error } = await context.writeAndVerifyFile(); // assert expect(success).to.equal(false); expectExists(error); expect(error.message).to.include(expectedErrorMessage); expect(error.type).to.equal(expectedErrorType); }); it(`logs error - ${description}`, async () => { // arrange const loggerStub = new LoggerStub(); const context = buildFaultyContext( new NodeReadbackFileWriterTestSetup() .withLogger(loggerStub), expectedErrorMessage, ); // act await context.writeAndVerifyFile(); // assert loggerStub.assertLogsContainMessagePart('error', expectedErrorMessage); }); }); }); }); }); class NodeReadbackFileWriterTestSetup { private logger: Logger = new LoggerStub(); private fileSystem: FileSystemOperations = new FileSystemOperationsStub(); private filePath = '/test/file/path.txt'; private fileContents = 'test file contents'; public withLogger(logger: Logger): this { this.logger = logger; return this; } public withFileSystem(fileSystem: FileSystemOperations): this { this.fileSystem = fileSystem; return this; } public withFilePath(filePath: string): this { this.filePath = filePath; return this; } public withFileContents(fileContents: string): this { this.fileContents = fileContents; return this; } public writeAndVerifyFile(): ReturnType { const writer = new NodeReadbackFileWriter( this.logger, this.fileSystem, ); return writer.writeAndVerifyFile( this.filePath, this.fileContents, ); } }