diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..85c1f2d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+FROM mcr.microsoft.com/powershell:7.4-mariner-2.0-arm64
+
+ENV APP_ROOT_DIR="/app"
+
+RUN pwsh -Command Set-PSRepository -Name PSGallery -InstallationPolicy Trusted && \
+ pwsh -Command Install-Module -Name ExchangeOnlineManagement -Scope AllUsers -RequiredVersion 3.5.0 && \
+ pwsh -Command Set-PSRepository -Name PSGallery -InstallationPolicy Untrusted
+
+RUN yum install -y nodejs npm
+
+# Set the working directory in the container
+WORKDIR /app
+
+# Copy package.json and package-lock.json
+COPY package*.json ./
+
+# Install dependencies
+RUN npm install
+
+# Copy the rest of the application code
+COPY . .
+
+# Command to run tests
+CMD ["npm", "run", "test-docker"]
\ No newline at end of file
diff --git a/README.md b/README.md
index e691eba..e4b1084 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ Node.js module that provides a registry and gateway for execution of pre-defined
* [Overview](#overview)
* [Concepts](#concepts)
* [Usage](#usage)
+* [Testing](#testing)
* [History](#history)
* [Related tools](#related)
@@ -54,9 +55,20 @@ Three sets of init commands are availiable as of version `1.1.0`:
7) There is also a unit-test (```test\all.js```) for the command registry in ```o365Utils.js``` which gives an example of usage for all thre possible Exchange connect variations.
+### Testing
+Project test can be executed by running `npm test` command on Windows machine. Connection to Exchange Online is required for the tests to pass.
+
+There is also option to run Docker based tests. You need to configure `environment` variables in `docker-compose.yml` file in order to define connection parameters. To run tests in Docker container, execute `docker-compose run test` command once the configuration is done.
+
+Exchange online tests will be skipped if the connection is not available.
+
+
### History
```
+v1.1.4 - 2024-11-22
+ - Extended testing and fixed escaping reserved variables and special characters in commands
+
v1.1.3 - 2024-11-14
- Added support for [multivalued parameters](https://learn.microsoft.com/en-us/exchange/modifying-multivalued-properties-exchange-2013-help) in commands
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a37e359
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3'
+services:
+ test:
+ build: .
+ volumes:
+ - .:/app
+ - /app/node_modules
+ environment:
+ - APPLICATION_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ - TENANT=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ - CERTIFICATE_PASSWORD=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ - CERTIFICATE=XXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ - O365_TENANT_DOMAIN_NAME=sample.com
\ No newline at end of file
diff --git a/o365Utils.js b/o365Utils.js
index 9707a4d..6b0477a 100644
--- a/o365Utils.js
+++ b/o365Utils.js
@@ -602,7 +602,13 @@ var o365CommandRegistry = {
'return': {
type: 'none'
}
- }
+ },
+ getStatus: {
+ command: 'Get-ConnectionInformation | ConvertTo-Json',
+ return: {
+ type: 'json'
+ }
+ },
};
module.exports.o365CommandRegistry = o365CommandRegistry;
diff --git a/package-lock.json b/package-lock.json
index a11abd3..03f427f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "powershell-command-executor",
- "version": "1.1.3",
+ "version": "1.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "powershell-command-executor",
- "version": "1.1.3",
+ "version": "1.1.4",
"license": "ISC",
"dependencies": {
"mustache": "^4.1.0",
diff --git a/package.json b/package.json
index 8e0df32..6e1f02f 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,14 @@
{
"name": "powershell-command-executor",
- "version": "1.1.3",
+ "version": "1.1.4",
"description": "Provides a registry and gateway for execution powershell commands through long-lived established remote PSSessions via a stateful-process-command-proxy pool of powershell processes",
"main": "psCommandService.js",
"directories": {
"test": "test"
},
"scripts": {
- "test": "mocha test/all.js"
+ "test": "mocha test/all.js",
+ "test-docker": "mocha test/unit.js"
},
"keywords": [
"command",
diff --git a/psCommandService.js b/psCommandService.js
index b12f89c..e195368 100644
--- a/psCommandService.js
+++ b/psCommandService.js
@@ -366,13 +366,15 @@ PSCommandService.prototype._sanitize = function (toSanitize, isQuoted) {
.replace(/\\n/g, "\\$&") // kill string based newline attempts
.replace(/[`#]/g, "`$&"); // escape stuff that could screw up variables
+ const sanitizeRegex = /[;\$\|\(\)\{\}\[\]\\]/g;
+ const multiValuedRegex = /@\{([^}]*)\}/g;
+
if (isQuoted) { // if quoted, escape all quotes
toSanitize = toSanitize.replace(/'/g, "'$&");
- } else if (
- !reservedVariableNames.includes(toSanitize) && // skip if this is reserved variable name
- multiValuedRegex.test(toSanitize) // process is this is multi-valued parameter
- ) {
+ } else if (multiValuedRegex.test(toSanitize)) {
+ // process is this is multi-valued parameter
const extractParams = (str, key) => {
+ // values must be wrapped in double quotes, so we can split them by comma
const match = str.match(new RegExp(`${key}="([^;]+)(?:";|"})`, "i"));
return match
? match[1]
@@ -385,18 +387,19 @@ PSCommandService.prototype._sanitize = function (toSanitize, isQuoted) {
const addItemsSanitized = extractParams(toSanitize, "Add");
const removeItemsSanitized = extractParams(toSanitize, "Remove");
-
- let result = "@{";
- if (addItemsSanitized.length > 0) {
- result += `Add="${addItemsSanitized.join('","')}"`;
- }
- if (removeItemsSanitized.length > 0) {
- if (addItemsSanitized.length > 0) result += "; ";
- result += `Remove="${removeItemsSanitized.join('","')}"`;
+ if (addItemsSanitized.length > 0 || removeItemsSanitized.length > 0) {
+ let result = "@{";
+ if (addItemsSanitized.length > 0) {
+ result += `Add="${addItemsSanitized.join('","')}"`;
+ }
+ if (removeItemsSanitized.length > 0) {
+ if (addItemsSanitized.length > 0) result += "; ";
+ result += `Remove="${removeItemsSanitized.join('","')}"`;
+ }
+ result += "}";
+ toSanitize = result;
}
- result += "}";
- toSanitize = result;
- } else {
+ } else if (!reservedVariableNames.includes(toSanitize)) { // skip if this is reserved variable name
toSanitize = toSanitize.replace(sanitizeRegex, "`$&");
}
diff --git a/test/all.js b/test/all.js
index 56e5630..c5dc463 100644
--- a/test/all.js
+++ b/test/all.js
@@ -463,6 +463,7 @@ describe('test PSCommandService w/ o365CommandRegistry', function () {
TENANT_ID,
10000, 30000, 60000), o365Utils.getO365PSDestroyCommands());
});
+ // The CertificateThumbprint parameter is supported only in Microsoft Windows.
it('Should test all group and mail contact commands then cleanup with Certificate Thumb Print based auth', function (done) {
this.timeout(120000);
testRun(done, o365Utils.getO365PSInitCommands(
diff --git a/test/unit.js b/test/unit.js
new file mode 100644
index 0000000..e274f79
--- /dev/null
+++ b/test/unit.js
@@ -0,0 +1,583 @@
+var assert = require("assert");
+var o365Utils = require("../o365Utils");
+var PSCommandService = require("../psCommandService");
+
+/**
+ * IMPORTANT!
+ * To run this test, you need to configure
+ * the following 4 variables!
+ *
+ * The credentials you are using to access o365 should
+ * be for a user that is setup as follows @:
+ * https://bitsofinfo.wordpress.com/2015/01/06/configuring-powershell-for-azure-ad-and-o365-exchange-management/
+ *
+ * @see https://github.com/bitsofinfo/powershell-credential-encryption-tools
+ */
+var O365_TENANT_DOMAIN_NAME =
+ process.env.O365_TENANT_DOMAIN_NAME || "somedomain.com";
+
+/**
+ * Following variables needed to test Certificate based connection to Exchange server
+ *
+ * @see https: //adamtheautomator.com/exchange-online-powershell-mfa/
+ * for setup instructions
+ */
+var CERTIFICATE = process.env.CERTIFICATE || "xxxxxxxxxx";
+var CERTIFICATE_PASSWORD = process.env.CERTIFICATE_PASSWORD || "xxxxxxxxxx";
+var APPLICATION_ID =
+ process.env.APPLICATION_ID || "00000000-00000000-00000000-00000000";
+var TENANT = process.env.TENANT || "your.exhange.domain.name";
+
+const initCommands = [
+ "$OutputEncoding = [System.Text.Encoding]::GetEncoding(65001)",
+ '$ErrorView = "NormalView"', // works for powershell 7.1
+ '$PSStyle.OutputRendering = "PlainText"', // works for powershell 7.2 and above
+ '$PSDefaultParameterValues["*:Encoding"] = "utf8"',
+];
+
+const initExchangeCommands = [
+ "$OutputEncoding = [System.Text.Encoding]::GetEncoding(65001)",
+ '$ErrorView = "NormalView"', // works for powershell 7.1
+ '$PSStyle.OutputRendering = "PlainText"', // works for powershell 7.2 and above
+ '$PSDefaultParameterValues["*:Encoding"] = "utf8"',
+
+ // #1 import some basics
+ "Import-Module ExchangeOnlineManagement",
+ // #2 create certificate password
+ `$CertificatePassword = (ConvertTo-SecureString -String "${CERTIFICATE_PASSWORD}" -AsPlainText -Force)`,
+ // #3 Import certificate from base64 string
+ `$Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2([Convert]::FromBase64String("${CERTIFICATE}"), $CertificatePassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]"PersistKeySet")`,
+ // #4 connect to exchange
+ `Connect-ExchangeOnline -ShowBanner:$false -ShowProgress:$false -Certificate $Certificate -CertificatePassword $CertificatePassword -AppID ${APPLICATION_ID} -Organization ${TENANT}`,
+];
+
+const preDestroyCommands = [
+ "Disconnect-ExchangeOnline -Confirm:$false",
+ "Remove-Module ExchangeOnlineManagement -Force",
+];
+
+const myLogFunction = (severity, origin, message) => {
+ console.log(severity.toUpperCase() + " " + origin + " " + message);
+};
+const logFunction = (severity, origin, msg) => {
+ if (origin != "Pool") {
+ console.log(severity.toUpperCase() + " " + origin + " " + msg);
+ }
+};
+
+const commandRegistry = {
+ setClipboard: {
+ command: "Set-Clipboard {{{arguments}}}",
+ arguments: {
+ 'Value': {
+ quoted: false,
+ },
+ },
+ return: {
+ type: 'none'
+ },
+ },
+ getClipboard: {
+ command: "Get-Clipboard",
+ arguments: {},
+ return: {
+ type: "text",
+ },
+ },
+};
+
+
+const StatefulProcessCommandProxy = require("stateful-process-command-proxy");
+
+const testRun = async (done, initCommands, preDestroyCommands) => {
+ const statefulProcessCommandProxy = new StatefulProcessCommandProxy({
+ name: "o365 RemotePSSession powershell pool",
+ max: 1,
+ min: 1,
+ idleTimeoutMS: 30000,
+
+ logFunction: logFunction,
+
+ processCommand: "pwsh",
+ processArgs: ["-Command", "-"],
+
+ processRetainMaxCmdHistory: 30,
+ processInvalidateOnRegex: {
+ any: [
+ {
+ regex: ".*nomatch.*",
+ flags: "i",
+ },
+ ],
+ stdout: [
+ {
+ regex: ".*nomatch.*",
+ },
+ ],
+ stderr: [
+ {
+ regex: ".*nomatch.*",
+ },
+ ],
+ },
+ processCwd: null,
+ processEnvMap: null,
+ processUid: null,
+ processGid: null,
+
+ initCommands: initCommands,
+
+ validateFunction: (processProxy) => processProxy.isValid(),
+
+ preDestroyCommands: preDestroyCommands,
+
+ processCmdBlacklistRegex: o365Utils.getO365BlacklistedCommands(),
+
+ processCmdWhitelistRegex: o365Utils.getO365WhitelistedCommands(),
+
+ autoInvalidationConfig: o365Utils.getO365AutoInvalidationConfig(30000),
+ });
+
+ const psCommandService = new PSCommandService(
+ statefulProcessCommandProxy,
+ o365Utils.o365CommandRegistry,
+ myLogFunction
+ );
+
+ const statusResponse = await psCommandService.execute("getStatus", {});
+ if (statusResponse.stderr == '' && statusResponse.stdout == '') {
+ console.log('Skipping test as getStatus command failed');
+ statefulProcessCommandProxy.shutdown();
+ done();
+ }
+
+ const random =
+ "unitTest" +
+ Math.abs(Math.floor(Math.random() * (1000 - 99999 + 1) + 1000));
+
+ const testMailContactName = "amailContact-" + random;
+ const testMailContactEmail =
+ testMailContactName + "@" + O365_TENANT_DOMAIN_NAME;
+
+ const testOwnerGroupName = "owneragroup-" + random;
+ const testOwnerGroupEmail =
+ testOwnerGroupName + "@" + O365_TENANT_DOMAIN_NAME;
+
+ const testGroupName = "agroup-" + random;
+ const testGroupEmail = testGroupName + "@" + O365_TENANT_DOMAIN_NAME;
+
+ const testGroupName2 = "agroup-2" + random;
+ const testGroupEmail2 = testGroupName2 + "@" + O365_TENANT_DOMAIN_NAME;
+
+ const cleanupAndShutdown = async (done, error) => {
+ await psCommandService.execute("removeDistributionGroup", {
+ Identity: testOwnerGroupEmail,
+ });
+ await psCommandService.execute("removeDistributionGroup", {
+ Identity: testGroupEmail,
+ });
+ await psCommandService.execute("removeDistributionGroup", {
+ Identity: testGroupEmail2,
+ });
+ await psCommandService.execute("removeMailContact", {
+ Identity: testMailContactEmail,
+ });
+
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+
+ setTimeout(() => {
+ if (error) {
+ done(error);
+ } else {
+ done();
+ }
+ }, 10000);
+
+ if (error) {
+ throw error;
+ }
+ };
+
+ try {
+ const ownerGroupCreateResult = await psCommandService.execute(
+ "newDistributionGroup",
+ {
+ Name: testOwnerGroupName,
+ DisplayName: testOwnerGroupName,
+ PrimarySmtpAddress: testOwnerGroupEmail,
+ }
+ );
+ assert.equal(ownerGroupCreateResult.stderr, "");
+
+ const testGroupCreateResult = await psCommandService.execute(
+ "newDistributionGroup",
+ {
+ Name: testGroupName,
+ DisplayName: testGroupName,
+ PrimarySmtpAddress: testGroupEmail,
+ ManagedBy: testOwnerGroupEmail,
+ }
+ );
+
+ assert.equal(testGroupCreateResult.stderr, "");
+ assert.equal(testGroupCreateResult.commandName, "newDistributionGroup");
+
+ const distributionGroup = JSON.parse(testGroupCreateResult.stdout);
+ try {
+ assert.equal(testGroupEmail, distributionGroup.PrimarySmtpAddress);
+ } catch (e) {
+ cleanupAndShutdown(done, e);
+ }
+ console.log(
+ "distributionGroup created OK: " + distributionGroup.PrimarySmtpAddress
+ );
+
+ const testGroup2CreateResult = await psCommandService.execute(
+ "newDistributionGroup",
+ {
+ Name: testGroupName2,
+ DisplayName: testGroupName2,
+ PrimarySmtpAddress: testGroupEmail2,
+ ManagedBy: testOwnerGroupEmail,
+ }
+ );
+
+ assert.equal(testGroup2CreateResult.stderr, "");
+ assert.equal(testGroup2CreateResult.commandName, "newDistributionGroup");
+
+ const distributionGroup2 = JSON.parse(testGroup2CreateResult.stdout);
+ try {
+ assert.equal(testGroupEmail2, distributionGroup2.PrimarySmtpAddress);
+ } catch (e) {
+ cleanupAndShutdown(done, e);
+ }
+ console.log(
+ "distributionGroup created OK: " + distributionGroup2.PrimarySmtpAddress
+ );
+
+ await psCommandService.executeAll([
+ {
+ commandName: "addDistributionGroupMember",
+ argMap: {
+ Identity: testGroupEmail,
+ Member: testGroupEmail2,
+ BypassSecurityGroupManagerCheck: null,
+ },
+ },
+ {
+ commandName: "addDistributionGroupMember",
+ argMap: {
+ Identity: testGroupEmail,
+ Member: testOwnerGroupEmail,
+ BypassSecurityGroupManagerCheck: null,
+ },
+ },
+ ]);
+ console.log("distributionGroupMembers added OK");
+
+ const groupMembersResult = await psCommandService.execute(
+ "getDistributionGroupMember",
+ {
+ Identity: testGroupEmail,
+ }
+ );
+
+ assert.equal(groupMembersResult.stderr, "");
+ assert.equal(groupMembersResult.commandName, "getDistributionGroupMember");
+
+ var members = JSON.parse(groupMembersResult.stdout);
+ try {
+ assert.equal(members.length, 2);
+ } catch (e) {
+ cleanupAndShutdown(done, e);
+ }
+ console.log("distributionGroup members fetched OK: " + members.length);
+ const removeResult = await psCommandService.execute(
+ "removeDistributionGroupMember",
+ {
+ Identity: testGroupEmail,
+ Member: testGroupEmail2,
+ }
+ );
+ assert.equal(removeResult.stderr, "");
+ assert.equal(removeResult.commandName, "removeDistributionGroupMember");
+
+ console.log(`distributionGroupMember (${testGroupEmail2}) removed OK`);
+
+ const refetchGroupMembersResult = await psCommandService.execute(
+ "getDistributionGroupMember",
+ {
+ Identity: testGroupEmail,
+ }
+ );
+ var members = JSON.parse("[" + refetchGroupMembersResult.stdout + "]");
+ try {
+ assert.equal(members.length, 1);
+ assert.equal(members[0].PrimarySmtpAddress, testOwnerGroupEmail);
+ } catch (e) {
+ return cleanupAndShutdown(done, e);
+ }
+ console.log(
+ "getDistributionGroupMember fetched OK: only owner group remains " +
+ members.length
+ );
+ const contactResult = await psCommandService.execute("newMailContact", {
+ Name: testMailContactName,
+ ExternalEmailAddress: testMailContactEmail,
+ });
+
+ assert.equal(contactResult.stderr, "");
+ assert.equal(contactResult.commandName, "newMailContact");
+
+ console.log("newMailContact added OK: " + testMailContactEmail);
+ const getContactResult = await psCommandService.execute("getMailContact", {
+ Identity: testMailContactEmail,
+ });
+
+ var contact = JSON.parse(getContactResult.stdout);
+ try {
+ assert.equal(testMailContactEmail, contact.PrimarySmtpAddress);
+ } catch (e) {
+ cleanupAndShutdown(done, e);
+ }
+ console.log("getMailContact fetched OK: " + testMailContactEmail);
+ await psCommandService.execute("addDistributionGroupMember", {
+ Identity: testGroupEmail,
+ Member: testMailContactEmail,
+ });
+
+ console.log(
+ "addDistributionGroupMember mailContact added OK: " + testMailContactEmail
+ );
+ const getGroupMembersResult = await psCommandService.execute(
+ "getDistributionGroupMember",
+ {
+ Identity: testGroupEmail,
+ }
+ );
+
+ var members = JSON.parse(getGroupMembersResult.stdout);
+ try {
+ assert.equal(members.length, 2);
+ } catch (e) {
+ cleanupAndShutdown(done, e);
+ }
+ console.log(
+ "getDistributionGroupMember fetched OK: one mail contact and one group exist " +
+ members.length
+ );
+ await psCommandService.execute("removeDistributionGroup", {
+ Identity: testGroupEmail,
+ });
+
+ console.log("distributionGroup removed OK: " + testGroupEmail);
+
+ done();
+ } catch (error) {
+ cleanupAndShutdown(done, error);
+ }
+};
+
+describe("test PSCommandService w/ o365CommandRegistry", function () {
+ it("Should test all group and mail contact commands then cleanup with Certificate based auth", function (done) {
+ this.timeout(120000);
+ testRun(done, initExchangeCommands, preDestroyCommands);
+ });
+ it("Should test whitelist", async function () {
+ this.timeout(10000);
+ const statefulProcessCommandProxy = new StatefulProcessCommandProxy({
+ name: "Powershell pool",
+ max: 1,
+ min: 1,
+ idleTimeoutMS: 30000,
+
+ logFunction: logFunction,
+ processCommand: "pwsh",
+ processArgs: ["-Command", "-"],
+ processRetainMaxCmdHistory: 30,
+ processCwd: null,
+ processEnvMap: null,
+ processUid: null,
+ processGid: null,
+ initCommands: initCommands,
+ processCmdWhitelistRegex: [{ regex: '^Set-Clipboard\\s+.*', flags: 'i' }],
+ validateFunction: (processProxy) => processProxy.isValid(),
+ });
+
+ const psCommandService = new PSCommandService(
+ statefulProcessCommandProxy,
+ commandRegistry,
+ myLogFunction
+ );
+
+ try {
+ const value = "'test clipboard value'";
+ const setResult = await psCommandService.execute("setClipboard", {
+ Value: value,
+ });
+ assert.equal(setResult.stderr, "");
+ try {
+ await psCommandService.execute("getClipboard", {});
+ } catch (e) {
+ assert.match(e.message, /Command cannot be executed it does not match our set of whitelisted commands/);
+ }
+
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+
+ return;
+ } catch (e) {
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+ throw e;
+ }
+ });
+ it("Should test blacklist", async function () {
+ this.timeout(10000);
+ const statefulProcessCommandProxy = new StatefulProcessCommandProxy({
+ name: "Powershell pool",
+ max: 1,
+ min: 1,
+ idleTimeoutMS: 30000,
+
+ logFunction: logFunction,
+ processCommand: "pwsh",
+ processArgs: ["-Command", "-"],
+ processRetainMaxCmdHistory: 30,
+ processCwd: null,
+ processEnvMap: null,
+ processUid: null,
+ processGid: null,
+ initCommands: initCommands,
+ processCmdBlacklistRegex: o365Utils.getO365BlacklistedCommands(),
+ validateFunction: (processProxy) => processProxy.isValid(),
+ });
+ const extendedCommandRegistry = {...commandRegistry, ...{
+ getHistory: {
+ command: "Get-History",
+ arguments: {},
+ return: {
+ type: "text",
+ },
+ },
+ }};
+
+ const psCommandService = new PSCommandService(
+ statefulProcessCommandProxy,
+ extendedCommandRegistry,
+ myLogFunction
+ );
+
+ const allowResult = await psCommandService.execute("getClipboard", {});
+ assert.equal(allowResult.stderr, "");
+ assert.equal(allowResult.stdout, "");
+ try {
+ await psCommandService.execute("getHistory", {});
+ } catch (e) {
+ assert.match(e.message, /Command cannot be executed as it matches a blacklist regex pattern/);
+ }
+
+ try {
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+
+ return;
+ } catch (e) {
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+ throw e;
+ }
+ });
+ it("Should test validation", async function () {
+ this.timeout(10000);
+ const statefulProcessCommandProxy = new StatefulProcessCommandProxy({
+ name: "Powershell pool",
+ max: 1,
+ min: 1,
+ idleTimeoutMS: 30000,
+
+ logFunction: logFunction,
+ processCommand: "pwsh",
+ processArgs: ["-Command", "-"],
+ processRetainMaxCmdHistory: 30,
+ processCwd: null,
+ processEnvMap: null,
+ processUid: null,
+ processGid: null,
+ initCommands: initCommands,
+ validateFunction: (processProxy) => processProxy.isValid(),
+ });
+
+ const psCommandService = new PSCommandService(
+ statefulProcessCommandProxy,
+ commandRegistry,
+ myLogFunction
+ );
+
+ const assertClipboard = async (value) => {
+ const setResult = await psCommandService.execute("setClipboard", {
+ Value: value,
+ });
+ assert.equal(setResult.stderr, "");
+ const getResult = await psCommandService.execute("getClipboard", {});
+ assert.equal(getResult.stderr, "");
+ return getResult;
+ }
+
+ try {
+ // non quoted value
+ var value = "plain text in clipboard";
+ var setResult = await psCommandService.execute("setClipboard", {
+ Value: value,
+ });
+ assert.equal(setResult.stdout, "");
+ assert.match(setResult.stderr, /A positional parameter cannot be found that accepts argument/);
+ await psCommandService.execute("getClipboard", {});
+ // simple multi param value
+ var res = await assertClipboard('@{add="test","test2";remove="test3","test4"}');
+ assert.equal(res.stdout, "System.Collections.Hashtable");
+ // multi params value with unsupported keys
+ value = '@{add="test","test2";remove="test3","test4";fake="test5","test6"}';
+ setResult = await psCommandService.execute("setClipboard", {
+ Value: value,
+ });
+ assert.equal(setResult.command, 'Set-Clipboard -Value @{Add="test","test2"; Remove="test3","test4"} ');
+ assert.equal(setResult.stderr, "");
+ getResult = await psCommandService.execute("getClipboard", {});
+ assert.equal(getResult.stderr, "");
+ assert.equal(getResult.stdout, "System.Collections.Hashtable");
+ // sample quoted test
+ res = await assertClipboard("'sample text'");
+ assert.equal(res.stdout, "sample text");
+
+ // espcaped quotes
+ value = "'; Get-ChildItem C:\; '";
+ setResult = await psCommandService.execute("setClipboard", {
+ Value: value,
+ });
+ assert.equal(setResult.stderr, "");
+ getResult = await psCommandService.execute("getClipboard", {});
+ assert.equal(getResult.stdout, "`; Get-ChildItem C:`;");
+ // reserved variable
+ var res = await assertClipboard('$true');
+ assert.equal(res.stdout, "True");
+
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+
+ return;
+ } catch (e) {
+ setTimeout(() => {
+ statefulProcessCommandProxy.shutdown();
+ }, 5000);
+ throw e;
+ }
+ });
+});