diff --git a/src/application/collections/linux.yaml b/src/application/collections/linux.yaml index 4709e1fc..9b9c750b 100644 --- a/src/application/collections/linux.yaml +++ b/src/application/collections/linux.yaml @@ -184,7 +184,7 @@ actions: > - Logs are valuable for diagnosing issues and understanding past actions [1]. > - Script files can help review changes made to the system and aid in reverting those changes if needed. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" children: - @@ -202,7 +202,7 @@ actions: > - This action is irreversible. Deleted script files cannot be retrieved. > - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" call: function: ClearDirectoryContents @@ -223,7 +223,7 @@ actions: > - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues. > - Logs can contain valuable information for technical support should you need assistance. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" call: function: ClearDirectoryContents @@ -2761,7 +2761,7 @@ actions: docs: |- Firefox provides an option for Enhanced Tracking Protection [1], which blocks trackers that gather information about your browsing behavior without disrupting site functionality [1]. - This feature also includes protections against harmful scripts such as malware that drains + This feature also includes protections against harmful scripts such as malware that drain your battery [1]. This script enables the `privacy.resistFingerprinting` preference, @@ -2791,7 +2791,7 @@ actions: This script enables the `privacy.resistFingerprinting` preference, activating anti-fingerprinting [1][2]. - As an experimental feature, it might cause some website breakage [2], such as impacting web + As an experimental feature, it might cause some website breakages [2], such as impacting web speech functionality [3] and favicons [4]. [1]: https://web.archive.org/web/20221025201025/https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting "Firefox's protection against fingerprinting | Firefox Help | support.mozilla.org" @@ -2876,7 +2876,7 @@ actions: It's configured to be enabled in nightly, aurora, beta, or default (developer) builds. In release builds, however, it's set to false [1]. This setting is hard-coded into the C++ - code to prevent easy disabling [2]. Developers have been approached about this issue but + code to prevent easy disabling [2]. Developers have been approached about this issue, but have rejected proposals to unlock it [3]. Mozilla's plan is to deprecate this setting eventually, followed by removal [1]. @@ -3012,7 +3012,7 @@ actions: recommend: standard docs: |- This script sets `toolkit.telemetry.server` to be empty. - This preference defines the server to which Telemetry pings are sent [1]. + This preference defines the server to which telemetry pings are sent [1]. [1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org" call: @@ -3133,7 +3133,7 @@ actions: name: Disable Firefox Pioneer study monitoring recommend: standard docs: |- - This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out. + This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out Firefox Pioneer program. This setting disables availability check for Firefox Pioneer studies [1]. diff --git a/src/application/collections/macos.yaml b/src/application/collections/macos.yaml index cb93e8a8..f2e18d0a 100644 --- a/src/application/collections/macos.yaml +++ b/src/application/collections/macos.yaml @@ -300,7 +300,7 @@ actions: > - Logs are valuable for diagnosing issues and understanding past actions [1]. > - Script files can help review changes made to the system and aid in reverting those changes if needed. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" children: - @@ -318,7 +318,7 @@ actions: > - This action is irreversible. Deleted script files cannot be retrieved. > - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" call: function: ClearDirectoryContents @@ -339,7 +339,7 @@ actions: > - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues. > - Logs can contain valuable information for technical support should you need assistance. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" call: function: ClearDirectoryContents diff --git a/src/application/collections/windows.yaml b/src/application/collections/windows.yaml index 2c72a427..04a5407b 100644 --- a/src/application/collections/windows.yaml +++ b/src/application/collections/windows.yaml @@ -212,7 +212,7 @@ actions: > - Logs are valuable for diagnosing issues and understanding past actions [1]. > - Script files can help review changes made to the system and aid in reverting those changes if needed. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" children: - @@ -230,7 +230,7 @@ actions: > - This action is irreversible. Deleted script files cannot be retrieved. > - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" call: function: ClearDirectoryContents @@ -251,7 +251,7 @@ actions: > - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues. > - Logs can contain valuable information for technical support should you need assistance. - [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" + [1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com" [2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com" call: function: ClearDirectoryContents diff --git a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.ts b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.ts new file mode 100644 index 00000000..ee7669e1 --- /dev/null +++ b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.ts @@ -0,0 +1,32 @@ +import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvokeShellCommandCreator'; + +/** + Encoding PowerShell commands resolve issues with quote handling. + + There are known problems with PowerShell's handling of double quotes in command line arguments: + - Quote stripping in PowerShell command line arguments: https://web.archive.org/web/20240507102706/https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments + - privacy.sexy double quotes issue when calling PowerShell from command line: https://web.archive.org/web/20240507102841/https://github.com/undergroundwires/privacy.sexy/issues/351 + - Challenges with single quotes in PowerShell command line: https://web.archive.org/web/20240507102047/https://stackoverflow.com/questions/20958388/command-line-escaping-single-quote-for-powershell + + Using the `EncodedCommand` parameter is recommended by Microsoft for handling + complex quoting scenarios. This approach helps avoid issues by encoding the entire + command as a Base64 string: + - Microsoft's documentation on using the `EncodedCommand` parameter: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand +*/ +export class EncodedPowerShellInvokeCmdCommandCreator +implements PowerShellInvokeShellCommandCreator { + public createCommandToInvokePowerShell(powerShellScript: string): string { + return generateEncodedPowershellCommand(powerShellScript); + } +} + +function generateEncodedPowershellCommand(powerShellScript: string): string { + const encodedCommand = encodeForPowershellExecution(powerShellScript); + return `PowerShell -EncodedCommand ${encodedCommand}`; +} + +function encodeForPowershellExecution(script: string): string { + // The string must be formatted using UTF-16LE character encoding, see: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand + const buffer = Buffer.from(script, 'utf16le'); + return buffer.toString('base64'); +} diff --git a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator.ts b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator.ts new file mode 100644 index 00000000..56f0394f --- /dev/null +++ b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator.ts @@ -0,0 +1,3 @@ +export interface PowerShellInvokeShellCommandCreator { + createCommandToInvokePowerShell(powerShellCommand: string): string; +} diff --git a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellArgumentEscaper.ts b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellArgumentEscaper.ts deleted file mode 100644 index e0e487df..00000000 --- a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellArgumentEscaper.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ShellArgumentEscaper } from './ShellArgumentEscaper'; - -export class CmdShellArgumentEscaper implements ShellArgumentEscaper { - public escapePathArgument(pathArgument: string): string { - return cmdShellPathArgumentEscape(pathArgument); - } -} - -function cmdShellPathArgumentEscape(pathArgument: string): string { - // - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe) - // to correctly handle paths containing spaces. - // - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped. - return `"${pathArgument}"`; -} diff --git a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.ts b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.ts new file mode 100644 index 00000000..a5f51e6b --- /dev/null +++ b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.ts @@ -0,0 +1,15 @@ +import type { ShellArgumentEscaper } from './ShellArgumentEscaper'; + +export class PowerShellArgumentEscaper implements ShellArgumentEscaper { + public escapePathArgument(pathArgument: string): string { + return powerShellPathArgumentEscape(pathArgument); + } +} + +function powerShellPathArgumentEscape(pathArgument: string): string { + // - Encloses the path in single quotes to handle spaces and most special characters. + // - Single quotes are used in PowerShell to ensure the string is treated as a literal string. + // - Paths in Windows can include single quotes ('), so any internal single quotes are escaped + // using double quotes. + return `'${pathArgument.replace(/'/g, "''")}'`; +} diff --git a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.ts b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.ts index b7e5121b..e72c5f4f 100644 --- a/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.ts +++ b/src/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.ts @@ -1,22 +1,29 @@ -import { CmdShellArgumentEscaper } from './ShellArgument/CmdShellArgumentEscaper'; +import type { Logger } from '@/application/Common/Log/Logger'; +import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; +import { PowerShellArgumentEscaper } from './ShellArgument/PowerShellArgumentEscaper'; +import { EncodedPowerShellInvokeCmdCommandCreator } from './PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator'; import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper'; import type { CommandDefinition } from '../CommandDefinition'; +import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvoke/PowerShellInvokeShellCommandCreator'; export class WindowsVisibleTerminalCommand implements CommandDefinition { constructor( - private readonly escaper: ShellArgumentEscaper = new CmdShellArgumentEscaper(), + private readonly escaper: ShellArgumentEscaper = new PowerShellArgumentEscaper(), + private readonly powershellCommandCreator: PowerShellInvokeShellCommandCreator + = new EncodedPowerShellInvokeCmdCommandCreator(), + private readonly logger: Logger = ElectronLogger, ) { } public buildShellCommand(filePath: string): string { - const command = [ - 'PowerShell', + const powershellCommand = [ 'Start-Process', '-Verb RunAs', // Run as administrator with GUI sudo prompt `-FilePath ${this.escaper.escapePathArgument(filePath)}`, ].join(' '); - return command; /* - 📝 Options: + Running PowerShell command is preferred due to its flexibility and the way it provides + GUI sudo prompt through `RunAs` argument. + Other options considered: `child_process.execFile()` "path", `cmd.exe /c "path"` ❌ Script execution in the background without a visible terminal. @@ -36,6 +43,8 @@ export class WindowsVisibleTerminalCommand implements CommandDefinition { `%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe. Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows */ + this.logger.info(`Building command for PowerShell execution:\n\tCommand: ${powershellCommand}`); + return this.powershellCommandCreator.createCommandToInvokePowerShell(powershellCommand); } public isExecutionTerminatedExternally(): boolean { diff --git a/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.spec.ts b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.spec.ts new file mode 100644 index 00000000..f3f31c9e --- /dev/null +++ b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator.spec.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { EncodedPowerShellInvokeCmdCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator'; + +describe('EncodedPowerShellInvokeCmdCommandCreator', () => { + describe('createCommandToInvokePowerShell', () => { + it('starts with PowerShell base command', () => { + // arrange + const sut = new EncodedPowerShellInvokeCmdCommandCreator(); + // act + const command = sut.createCommandToInvokePowerShell('non-important-command'); + // assert + expect(command.startsWith('PowerShell ')).to.equal(true); + }); + it('includes encoded command as parameter', () => { + // arrange + const expectedParameterName = '-EncodedCommand'; + const sut = new EncodedPowerShellInvokeCmdCommandCreator(); + // act + const command = sut.createCommandToInvokePowerShell('non-important-command'); + // assert + const args = parsePowerShellArgs(command); + const parameterNames = [...args.keys()]; + expect(parameterNames).to.include(expectedParameterName); + }); + it('correctly encode the command as utf16le base64', () => { + // arrange + const givenCode = 'Write-Output "Today is $(Get-Date -Format \'dddd, MMMM dd\')."'; + const expectedEncodedCommand = 'VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAiAFQAbwBkAGEAeQAgAGkAcwAgACQAKABHAGUAdAAtAEQAYQB0AGUAIAAtAEYAbwByAG0AYQB0ACAAJwBkAGQAZABkACwAIABNAE0ATQBNACAAZABkACcAKQAuACIA'; + const sut = new EncodedPowerShellInvokeCmdCommandCreator(); + // act + const command = sut.createCommandToInvokePowerShell(givenCode); + // assert + const args = parsePowerShellArgs(command); + const actualEncodedCommand = args.get('-EncodedCommand'); + expect(actualEncodedCommand).to.equal(expectedEncodedCommand); + }); + }); +}); + +function parsePowerShellArgs(command: string): Map { + const argsMap = new Map(); + const argRegex = /(-\w+)(\s+([^ ]+))?/g; + let match = argRegex.exec(command); + while (match !== null) { + const arg = match[1]; + const value = match[3]; + argsMap.set(arg, value); + match = argRegex.exec(command); + } + return argsMap; +} diff --git a/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellPathArgumentEscaper.spec.ts b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellPathArgumentEscaper.spec.ts deleted file mode 100644 index 41b273e7..00000000 --- a/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellPathArgumentEscaper.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe } from 'vitest'; -import { CmdShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellArgumentEscaper'; -import { runEscapeTests } from './ShellArgumentEscaperTestRunner'; - -describe('CmdShellArgumentEscaper', () => { - runEscapeTests(() => new CmdShellArgumentEscaper(), [ - { - description: 'encloses the path in double quotes', - givenPath: 'C:\\Program Files\\app.exe', - expectedPath: '"C:\\Program Files\\app.exe"', - }, - ]); -}); diff --git a/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.spec.ts b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.spec.ts new file mode 100644 index 00000000..f5881160 --- /dev/null +++ b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper.spec.ts @@ -0,0 +1,23 @@ +import { describe } from 'vitest'; +import { PowerShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper'; +import { runEscapeTests } from './ShellArgumentEscaperTestRunner'; + +describe('PowerShellArgumentEscaper', () => { + runEscapeTests(() => new PowerShellArgumentEscaper(), [ + { + description: 'encloses the path in single quotes', + givenPath: 'C:\\Program Files\\app.exe', + expectedPath: '\'C:\\Program Files\\app.exe\'', + }, + { + description: 'escapes single internal single quotes', + givenPath: 'C:\\Users\\O\'Reilly\\Documents', + expectedPath: '\'C:\\Users\\O\'\'Reilly\\Documents\'', + }, + { + description: 'escapes multiple internal single quotes', + givenPath: 'C:\\Program Files\\User\'s Files\\Today\'s Files', + expectedPath: '\'C:\\Program Files\\User\'\'s Files\\Today\'\'s Files\'', + }, + ]); +}); diff --git a/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.spec.ts b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.spec.ts index c06a75a4..a348820a 100644 --- a/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.spec.ts +++ b/tests/unit/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand.spec.ts @@ -2,24 +2,66 @@ import { describe, it, expect } from 'vitest'; import { WindowsVisibleTerminalCommand } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand'; import type { ShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/ShellArgumentEscaper'; import { ShellArgumentEscaperStub } from '@tests/unit/shared/Stubs/ShellArgumentEscaperStub'; +import type { Logger } from '@/application/Common/Log/Logger'; +import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub'; +import type { PowerShellInvokeShellCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator'; +import { PowerShellInvokeShellCommandCreatorStub } from '@tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub'; +import { expectExists } from '@tests/shared/Assertions/ExpectExists'; describe('WindowsVisibleTerminalCommand', () => { describe('buildShellCommand', () => { - it('returns expected command for given escaped file path', () => { + it('creates a PowerShell command with the escaped path', () => { // arrange const escapedFilePath = '/escaped/file/path'; - const expectedCommand = `PowerShell Start-Process -Verb RunAs -FilePath ${escapedFilePath}`; + const expectedCommand = `Start-Process -Verb RunAs -FilePath ${escapedFilePath}`; const escaper = new ShellArgumentEscaperStub(); escaper.escapePathArgument = () => escapedFilePath; + const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub(); const sut = new CommandBuilder() .withEscaper(escaper) + .withPowerShellCommandCreator(powerShellCommandCreator) + .build(); + // act + sut.buildShellCommand('unimportant'); + // assert + const calls = powerShellCommandCreator.callHistory.filter((c) => c.methodName === 'createCommandToInvokePowerShell'); + expect(calls).to.have.lengthOf(1); + const [actualCommand] = calls[0].args; + expect(actualCommand).to.equal(expectedCommand); + }); + it('returns a command to invoke PowerShell', () => { + // arrange + const expectedCommand = 'expected command from creator'; + const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub(); + powerShellCommandCreator.createCommandToInvokePowerShell = () => expectedCommand; + const sut = new CommandBuilder() + .withPowerShellCommandCreator(powerShellCommandCreator) .build(); // act const actualCommand = sut.buildShellCommand('unimportant'); // assert expect(actualCommand).to.equal(expectedCommand); }); - it('escapes provided file path correctly', () => { + it('logs the powershell command', () => { + // arrange + let expectedCommand: string | undefined; + const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub(); + powerShellCommandCreator.createCommandToInvokePowerShell = (command) => { + expectedCommand = command; + return 'unimportant command'; + }; + const logger = new LoggerStub(); + const sut = new CommandBuilder() + .withLogger(logger) + .withPowerShellCommandCreator(powerShellCommandCreator) + .build(); + // act + sut.buildShellCommand('unimportant'); + // assert + expectExists(expectedCommand); + logger.assertLogsContainMessagePart('info', expectedCommand); + }); + it('escapes the provided file path', () => { // arrange const expectedFilePath = '/input'; const escaper = new ShellArgumentEscaperStub(); @@ -61,14 +103,33 @@ describe('WindowsVisibleTerminalCommand', () => { class CommandBuilder { private escaper: ShellArgumentEscaper = new ShellArgumentEscaperStub(); + private logger: Logger = new LoggerStub(); + + private powerShellCommandCreator + : PowerShellInvokeShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub(); + public withEscaper(escaper: ShellArgumentEscaper): this { this.escaper = escaper; return this; } + public withPowerShellCommandCreator( + powerShellCommandCreator: PowerShellInvokeShellCommandCreator, + ): this { + this.powerShellCommandCreator = powerShellCommandCreator; + return this; + } + + public withLogger(logger: Logger): this { + this.logger = logger; + return this; + } + public build(): WindowsVisibleTerminalCommand { return new WindowsVisibleTerminalCommand( this.escaper, + this.powerShellCommandCreator, + this.logger, ); } } diff --git a/tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub.ts b/tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub.ts new file mode 100644 index 00000000..6fa99a12 --- /dev/null +++ b/tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub.ts @@ -0,0 +1,24 @@ +import type { PowerShellInvokeShellCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator'; +import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls'; + +export class PowerShellInvokeShellCommandCreatorStub + extends StubWithObservableMethodCalls + implements PowerShellInvokeShellCommandCreator { + private command: string | undefined; + + public withCreatedCommand(command: string): this { + this.command = command; + return this; + } + + public createCommandToInvokePowerShell(powerShellCommand: string): string { + this.registerMethodCall({ + methodName: 'createCommandToInvokePowerShell', + args: [powerShellCommand], + }); + if (this.command === undefined) { + return `[${PowerShellInvokeShellCommandCreatorStub.name}] ${powerShellCommand}`; + } + return this.command; + } +}