Files
privacy.sexy/tests/unit/infrastructure/CodeRunner/ScriptFileCodeRunner.spec.ts
undergroundwires 8c17396285 Fix script cancellation with new dialog on Linux
This commit improves the management of script execution process by
enhancing the way terminal commands are handled, paving the way for
easier future modifications and providing clearer feedback to users when
scripts are cancelled.

Previously, the UI displayed a generic error message which could lead to
confusion if the user intentionally cancelled the script execution. Now,
a specific error dialog will appear, improving the user experience by
accurately reflecting the action taken by the user.

This change affects code execution on Linux where closing GNOME terminal
returns exit code `137` which is then treated by script cancellation by
privacy.sexy to show the accurate error dialog. It does not affect macOS
and Windows as curret commands result in success (`0`) exit code on
cancellation.

Additionally, this update encapsulates OS-specific logic into dedicated
classes, promoting better separation of concerns and increasing the
modularity of the codebase. This makes it simpler to maintain and extend
the application.

Key changes:

- Display a specific error message for script cancellations.
- Refactor command execution into dedicated classes.
- Improve file permission setting flexibility and avoid setting file
  permissions on Windows as it's not required to execute files.
- Introduce more granular error types for script execution.
- Increase logging for shell commands to aid in debugging.
- Expand test coverage to ensure reliability.
- Fix error dialogs not showing the error messages due to incorrect
  propagation of errors.

Other supported changes:

- Update `SECURITY.md` with details on script readback and verification.
- Fix a typo in `IpcRegistration.spec.ts`.
- Document antivirus scans in `desktop-vs-web-features.md`.
2024-04-30 15:04:59 +02:00

229 lines
7.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { ScriptFileCodeRunner } from '@/infrastructure/CodeRunner/ScriptFileCodeRunner';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
import type { Logger } from '@/application/Common/Log/Logger';
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
import type { ScriptFileExecutor } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
import { ScriptFileExecutorStub } from '@tests/unit/shared/Stubs/ScriptFileExecutorStub';
import type { ScriptFileCreator } from '@/infrastructure/CodeRunner/Creation/ScriptFileCreator';
import { ScriptFileCreatorStub } from '@tests/unit/shared/Stubs/ScriptFileCreatorStub';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
describe('ScriptFileCodeRunner', () => {
describe('runCode', () => {
describe('creating file', () => {
it('uses provided code', async () => {
// arrange
const expectedCode = 'expected code';
const fileCreator = new ScriptFileCreatorStub();
const context = new CodeRunnerTestSetup()
.withFileCreator(fileCreator)
.withCode(expectedCode);
// act
await context.runCode();
// assert
const createCalls = fileCreator.callHistory.filter((call) => call.methodName === 'createScriptFile');
expect(createCalls.length).to.equal(1);
const [actualCode] = createCalls[0].args;
expect(actualCode).to.equal(expectedCode);
});
it('uses provided extension', async () => {
// arrange
const expectedFileExtension = 'expected-file-extension';
const fileCreator = new ScriptFileCreatorStub();
const context = new CodeRunnerTestSetup()
.withFileCreator(fileCreator)
.withFileExtension(expectedFileExtension);
// act
await context.runCode();
// assert
const createCalls = fileCreator.callHistory.filter((call) => call.methodName === 'createScriptFile');
expect(createCalls.length).to.equal(1);
const [,scriptFileNameParts] = createCalls[0].args;
expectExists(scriptFileNameParts, JSON.stringify(`Call args: ${JSON.stringify(createCalls[0].args)}`));
expect(scriptFileNameParts.scriptFileExtension).to.equal(expectedFileExtension);
});
it('uses default script name', async () => {
// arrange
const expectedScriptName = ScriptFilename;
const fileCreator = new ScriptFileCreatorStub();
const context = new CodeRunnerTestSetup()
.withFileCreator(fileCreator);
// act
await context.runCode();
// assert
const createCalls = fileCreator.callHistory.filter((call) => call.methodName === 'createScriptFile');
expect(createCalls.length).to.equal(1);
const [,scriptFileNameParts] = createCalls[0].args;
expectExists(scriptFileNameParts, JSON.stringify(`Call args: ${JSON.stringify(createCalls[0].args)}`));
expect(scriptFileNameParts.scriptName).to.equal(expectedScriptName);
});
});
describe('executing file', () => {
it('executes at correct path', async () => {
// arrange
const expectedFilePath = 'expected script path';
const fileExecutor = new ScriptFileExecutorStub();
const context = new CodeRunnerTestSetup()
.withFileCreator(new ScriptFileCreatorStub().withCreatedFilePath(expectedFilePath))
.withFileExecutor(fileExecutor);
// act
await context.runCode();
// assert
const executeCalls = fileExecutor.callHistory.filter((call) => call.methodName === 'executeScriptFile');
expect(executeCalls.length).to.equal(1);
const [actualPath] = executeCalls[0].args;
expect(actualPath).to.equal(expectedFilePath);
});
});
describe('successful run', () => {
it('indicates success', async () => {
// arrange
const expectedSuccessResult = true;
const context = new CodeRunnerTestSetup();
// act
const { success: actualSuccessValue } = await context.runCode();
// assert
expect(actualSuccessValue).to.equal(expectedSuccessResult);
});
it('logs success message', async () => {
// arrange
const expectedMessagePart = 'Successfully ran script';
const logger = new LoggerStub();
const context = new CodeRunnerTestSetup()
.withLogger(logger);
// act
await context.runCode();
// assert
logger.assertLogsContainMessagePart('info', expectedMessagePart);
});
});
describe('error handling', () => {
const testScenarios: ReadonlyArray<{
readonly description: string;
readonly expectedErrorType: CodeRunErrorType;
readonly expectedErrorMessage: string;
buildFaultyContext(
setup: CodeRunnerTestSetup,
errorMessage: string,
errorType: CodeRunErrorType,
): CodeRunnerTestSetup;
}> = [
{
description: 'execution failure',
expectedErrorType: 'FilePermissionChangeError',
expectedErrorMessage: 'execution error',
buildFaultyContext: (setup, errorMessage, errorType) => {
const executor = new ScriptFileExecutorStub();
executor.executeScriptFile = () => Promise.resolve({
success: false,
error: {
message: errorMessage,
type: errorType,
},
});
return setup.withFileExecutor(executor);
},
},
{
description: 'creation failure',
expectedErrorType: 'FileWriteError',
expectedErrorMessage: 'creation error',
buildFaultyContext: (setup, errorMessage, errorType) => {
const creator = new ScriptFileCreatorStub();
creator.createScriptFile = () => Promise.resolve({
success: false,
error: {
message: errorMessage,
type: errorType,
},
});
return setup.withFileCreator(creator);
},
},
];
testScenarios.forEach(({
description, expectedErrorType, expectedErrorMessage, buildFaultyContext,
}) => {
it(`handles ${description}`, async () => {
// arrange
const context = buildFaultyContext(
new CodeRunnerTestSetup(),
expectedErrorMessage,
expectedErrorType,
);
// act
const { success, error } = await context.runCode();
// assert
expect(success).to.equal(false);
expectExists(error);
expect(error.message).to.include(expectedErrorMessage);
expect(error.type).to.equal(expectedErrorType);
});
});
});
});
});
class CodeRunnerTestSetup {
private code = `[${CodeRunnerTestSetup.name}]code`;
private fileExtension = `[${CodeRunnerTestSetup.name}]file-extension`;
private fileCreator: ScriptFileCreator = new ScriptFileCreatorStub();
private fileExecutor: ScriptFileExecutor = new ScriptFileExecutorStub();
private logger: Logger = new LoggerStub();
public runCode() {
const runner = new ScriptFileCodeRunner(
this.fileExecutor,
this.fileCreator,
this.logger,
);
return runner
.runCode(this.code, this.fileExtension);
}
public withFileExecutor(fileExecutor: ScriptFileExecutor): this {
this.fileExecutor = fileExecutor;
return this;
}
public withCode(code: string): this {
this.code = code;
return this;
}
public withLogger(logger: Logger): this {
this.logger = logger;
return this;
}
public withFileCreator(fileCreator: ScriptFileCreator): this {
this.fileCreator = fileCreator;
return this;
}
public withFileExtension(fileExtension: string): this {
this.fileExtension = fileExtension;
return this;
}
}