ssh-client-wrapper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json ADDED
@@ -0,0 +1,162 @@
1
+ {
2
+ "root": true,
3
+ "extends": [
4
+ "eslint"
5
+ ],
6
+ "plugins": [
7
+ "node",
8
+ "jsdoc"
9
+ ],
10
+ "env": {
11
+ "node": true,
12
+ "es6": true
13
+ },
14
+ "rules": {
15
+ "no-param-reassign": "warn",
16
+ "arrow-parens": [
17
+ "error",
18
+ "always"
19
+ ],
20
+ "arrow-body-style": [
21
+ "error",
22
+ "always"
23
+ ],
24
+ "arrow-spacing": [
25
+ "error",
26
+ {
27
+ "before": false,
28
+ "after": false
29
+ }
30
+ ],
31
+ "camelcase": [
32
+ "error",
33
+ {
34
+ "properties": "never"
35
+ }
36
+ ],
37
+ "eqeqeq": [
38
+ "error",
39
+ "always",
40
+ {
41
+ "null": "ignore"
42
+ }
43
+ ],
44
+ "func-style": [
45
+ "error",
46
+ "declaration",
47
+ {
48
+ "allowArrowFunctions": true
49
+ }
50
+ ],
51
+ "indent": [
52
+ "error",
53
+ 2,
54
+ {
55
+ "SwitchCase": 1
56
+ }
57
+ ],
58
+ "lines-around-comment": [
59
+ "error",
60
+ {
61
+ "beforeBlockComment": true,
62
+ "beforeLineComment": false
63
+ }
64
+ ],
65
+ "lines-between-class-members": [
66
+ "error",
67
+ "always",
68
+ {
69
+ "exceptAfterSingleLine": true
70
+ }
71
+ ],
72
+ "newline-per-chained-call": "error",
73
+ "no-use-before-define": [
74
+ "error",
75
+ {
76
+ "functions": false
77
+ }
78
+ ],
79
+ "no-warning-comments": "warn",
80
+ "padded-blocks": [
81
+ "error",
82
+ "never"
83
+ ],
84
+ "padding-line-between-statements": [
85
+ "error",
86
+ {
87
+ "blankLine": "any",
88
+ "prev": [
89
+ "const",
90
+ "let",
91
+ "var"
92
+ ],
93
+ "next": "*"
94
+ },
95
+ {
96
+ "blankLine": "always",
97
+ "prev": "*",
98
+ "next": [
99
+ "block-like",
100
+ "class",
101
+ "do",
102
+ "for",
103
+ "function",
104
+ "multiline-block-like",
105
+ "switch",
106
+ "try",
107
+ "while"
108
+ ]
109
+ },
110
+ {
111
+ "blankLine": "any",
112
+ "prev": [
113
+ "const",
114
+ "let",
115
+ "var",
116
+ "for",
117
+ "while",
118
+ "do",
119
+ "block-like",
120
+ "multiline-block-like"
121
+ ],
122
+ "next": [
123
+ "block-like",
124
+ "do",
125
+ "for",
126
+ "multiline-block-like",
127
+ "switch",
128
+ "try",
129
+ "while"
130
+ ]
131
+ }
132
+ ],
133
+ "spaced-comment": [
134
+ "error",
135
+ "never"
136
+ ],
137
+ "node/exports-style": [
138
+ "error",
139
+ "module.exports"
140
+ ],
141
+ "require-unicode-regexp": "off",
142
+ "node/no-unsupported-features": [
143
+ "error",
144
+ {
145
+ "version": 10
146
+ }
147
+ ],
148
+ "jsdoc/require-hyphen-before-param-description": [
149
+ "warn",
150
+ "always"
151
+ ],
152
+ "jsdoc/require-param-description": "warn",
153
+ "jsdoc/require-param-name": "warn",
154
+ "jsdoc/require-param-type": "warn",
155
+ "jsdoc/require-jsdoc": "off",
156
+ "jsdoc/require-param": "off",
157
+ "jsdoc/require-returns": "off"
158
+ },
159
+ "parserOptions": {
160
+ "ecmaVersion": 2018
161
+ }
162
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Naoyuki Sogo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # ssh-client-wrapper
2
+ open ssh client wrapper library for nodejs
package/lib/fork.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ const debug = require("debug")("sshClientWrapper:fork");
3
+ const debugVerbose = require("debug")("sshClientWrapper:fork_verbose");
4
+ const debugSendPty = require("debug")("sshClientWrapper:fork_sendpty");
5
+ const { spawn } = require("node-pty");
6
+
7
+ const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String } = require("./util.js");
8
+
9
+ const defaultOpt = {
10
+
11
+ shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
12
+ windowsHide: true
13
+ };
14
+ async function fork(hostInfo, cmd, args, argOpt, outputCallback) {
15
+ return new Promise((resolve, reject)=>{
16
+ const opt = argOpt || defaultOpt;
17
+
18
+ const ptyProcess = spawn(cmd, args, opt);
19
+ debug(`cmd=${cmd}: args=${args}`);
20
+ ptyProcess.onData(buff2String.bind(null, debugVerbose));
21
+
22
+ ptyProcess.onData((data)=>{
23
+ const output = data.toString();
24
+
25
+ if (reNewHostPrompt.test(output)) {
26
+ debugSendPty("yes");
27
+ ptyProcess.write("yes\n");
28
+ }
29
+ if (rePwPrompt.test(output)) {
30
+ if (typeof hostInfo.password === "string") {
31
+ debugSendPty(hostInfo.password);
32
+ ptyProcess.write(`${hostInfo.password}\n`);
33
+ } else if (typeof hostInfo.password === "function") {
34
+ hostInfo.password(ptyProcess.write);
35
+ }
36
+ }
37
+ if (rePhPrompt.test(output)) {
38
+ if (typeof hostInfo.passphrase === "string") {
39
+ debugSendPty(hostInfo.passphrase);
40
+ ptyProcess.write(`${hostInfo.passphrase}\n`);
41
+ } else if (typeof hostInfo.passphrase === "function") {
42
+ hostInfo.passphrase(ptyProcess.write);
43
+ }
44
+ }
45
+ });
46
+
47
+ if (typeof outputCallback === "function") {
48
+ ptyProcess.onData(buff2String.bind(null, outputCallback));
49
+ }
50
+ ptyProcess.onExit(({ exitCode, signal })=>{
51
+ debug("onExit", exitCode, signal);
52
+
53
+ if (signal > 0) {
54
+ return reject(signal);
55
+ }
56
+ if ([0, 126, 127].includes(exitCode)) {
57
+ return resolve(exitCode);
58
+ }
59
+ return reject(exitCode);
60
+ });
61
+ });
62
+ }
63
+ module.exports = fork;
package/lib/index.js ADDED
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ const debugLib = require("debug");
3
+ const debug = debugLib("sshClientWrapper:interface");
4
+ const { sshExec, canConnect, disconnect } = require("./sshExec.js");
5
+ const { send, recv } = require("./rsyncExec.js");
6
+ const { isArrayOfString } = require("./util.js");
7
+
8
+ /**
9
+ * Integer means integer number but it is defined as Object for now
10
+ * workaround for eslint-plugin-jsdoc's no-undefined-types rule bug.
11
+ * @typedef {Object} Integer
12
+ */
13
+
14
+ /**
15
+ * Facade class.
16
+ * @constructor
17
+ * @param {Object} hostInfo - option object
18
+ * @param {string} hostInfo.host - destination
19
+ * @param {string} hostInfo.user - login name
20
+ * @param {number} hostInfo.port - port number
21
+ * @param {string | Function} hostInfo.password - password
22
+ * @param {string | Function} hostInfo.passphrase - passphrase
23
+ * @param {string} hostInfo.keyFile - private key filename
24
+ * @param {boolean} hostInfo.noStrictHostkeyChecking - bypass host key checking if true
25
+ * @param {Integer} hostInfo.renewInterval=0 - Connection renewal interval (sec).
26
+ */
27
+ class SshClientWrapper {
28
+ constructor(hostInfo) {
29
+ if (typeof hostInfo.host !== "string") {
30
+ return new Error("hostname must be specified!");
31
+ }
32
+ this.hostInfo = hostInfo;
33
+ }
34
+
35
+ /**
36
+ * Execute command on remote host.
37
+ * @param {string} cmd - Cmdline which will be executed.
38
+ * @param { Function } outputCallback - call back routine for output
39
+ * @returns {Integer} - Return code of cmd.
40
+ *
41
+ * If stdout and stderr is array, last 5 line of stdout and stderr is stored in them.
42
+ */
43
+ async exec(cmd, outputCallback) {
44
+ debug("exec called", cmd);
45
+
46
+ if (typeof cmd !== "string") {
47
+ return Promise.reject(new Error("cmd must be string"));
48
+ }
49
+ return sshExec(this.hostInfo, cmd, outputCallback);
50
+ }
51
+
52
+ /**
53
+ * Execute command repeatedly until specified keyword is found in stdout and/or stderr.
54
+ * @param {string} cmd - Cmdline which will be executed.
55
+ * @param {RegExp} regexp - End condition.
56
+ * @param {number} retryDelay - Duration between each try (in sec).
57
+ * @param {number} maxRetry - Max retry count.
58
+ * @returns {Integer} - Return code of cmd.
59
+ *
60
+ * If stdout and stderr is array, last 10 line of stdout and stderr is stored in them.
61
+ */
62
+ async watch(cmd, regexp, retryDelay = 3, maxRetry = null) {
63
+ debug("watch called", cmd, regexp, retryDelay, maxRetry);
64
+
65
+ if (typeof cmd !== "string") {
66
+ return Promise.reject(new Error("cmd must be string"));
67
+ }
68
+ if (!(regexp instanceof RegExp)) {
69
+ const err = new Error("illegal regexp specified");
70
+ err.regexp = regexp;
71
+ err.cmd = cmd;
72
+ err.retryDelay = retryDelay;
73
+ err.maxRetry = maxRetry;
74
+ return Promise.reject(err);
75
+ }
76
+ //TODO promise retry
77
+ }
78
+
79
+ /**
80
+ * Send file or directory and its child to server.
81
+ * @param {[string]} src - File or directory name which to be recieve.
82
+ * @param {string} dst - Destination path.
83
+ * @param {[string]} opt - option for rsync
84
+ * @returns {Promise} - Resolved with undefined when file transfer is done.
85
+ */
86
+ async send(src, dst, opt = {}) {
87
+ debug("send called", src, dst);
88
+
89
+ if (!isArrayOfString(src)) {
90
+ return new Error("src must be array of string");
91
+ }
92
+ if (typeof dst !== "string") {
93
+ return new Error("dst must be string");
94
+ }
95
+ if (!isArrayOfString(opt)) {
96
+ return new Error("opt must be array of string");
97
+ }
98
+ return send(this.hostInfo, src, dst, opt);
99
+ }
100
+
101
+ /**
102
+ * Get file or directory and its child from server.
103
+ * @param {[string]} src - File or directory name which to be recieve.
104
+ * @param {string} dst - Destination path.
105
+ * @param {[string]} opt - option for rsync
106
+ * @returns {Promise} - Resolved with undefined when file transfer is done.
107
+ */
108
+ async recv(src, dst, opt = {}) {
109
+ debug("recv called", src, dst);
110
+
111
+ if (!isArrayOfString(src)) {
112
+ return new Error("src must be array of string");
113
+ }
114
+ if (typeof dst !== "string") {
115
+ return new Error("dst must be string");
116
+ }
117
+ if (!isArrayOfString(opt)) {
118
+ return new Error("opt must be array of string");
119
+ }
120
+
121
+ return recv(this.hostInfo, src, dst, opt);
122
+ }
123
+
124
+ /**
125
+ * Check if you can connect to specified server.
126
+ * @param {Integer} timeout - timeout in secconds
127
+ * @returns {Promise} - Resolved with true on success, otherwise rejected with Error.
128
+ */
129
+ canConnect(timeout) {
130
+ debug("canConnect called timeout=", timeout);
131
+
132
+ if (!Number.isInteger(timeout) || timeout <= 0) {
133
+ return new Error("timeout must be positive integer");
134
+ }
135
+ return canConnect(this.hostInfo, timeout);
136
+ }
137
+
138
+ /**
139
+ * Disconnect master session
140
+ */
141
+ disconnect() {
142
+ debug("disconnect called");
143
+ disconnect(this.hostInfo);
144
+ }
145
+ }
146
+
147
+ module.exports = SshClientWrapper;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ const path = require("path");
3
+ const debug = require("debug")("sshClientWrapper:rsyncExec");
4
+ const debugVerbose = require("debug")("sshClientWrapper:rsyncExec_verbose");
5
+ const { glob } = require("glob");
6
+
7
+ const { sshExec } = require("./sshExec.js");
8
+ const fork = require("./fork.js");
9
+
10
+ const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
11
+
12
+ /**
13
+ * Send file or directory and its child to server.
14
+ * @param {Object} hostInfo - hostinfo object
15
+ * @param {[string]} argSrc - File or directory name which to be send.
16
+ * @param {string} dst - Destination path.
17
+ * @param {[string]} rsyncOpt - optional arguments for rsync
18
+ *
19
+ * this function execute rsync -avvm --copy-unsafe-links src rsync://[user@]host[:port]/dst
20
+ */
21
+ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
22
+ const promises = await Promise.allSettled(argSrc.map((e)=>{
23
+ return glob(e);
24
+ }));
25
+ const src = promises.flatMap((e)=>{
26
+ if (e.status !== "fulfilled") {
27
+ return [];
28
+ }
29
+ return e.value;
30
+ });
31
+
32
+ const sshArgs = getSshOption(hostInfo, true);
33
+ const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
34
+
35
+ const args = ["-amvv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, ...src, `${hostInfo.host}:${dst}`];
36
+ const { dir: remoteDir } = path.parse(dst);
37
+ if (remoteDir !== "" && remoteDir !== "/") {
38
+ debug(`mkdir ${remoteDir}`);
39
+ sshExec(hostInfo, `mkdir -p ${remoteDir}`, debugVerbose);
40
+ }
41
+ debug(`send ${src} to ${hostInfo.host}:${dst}`);
42
+ debugVerbose(`rsyncCmd=${rsyncCmd}: args=${args}`);
43
+ return fork(hostInfo, rsyncCmd, args);
44
+ }
45
+
46
+ /**
47
+ * Send file or directory and its child to server.
48
+ * @param {Object} hostInfo - hostinfo object
49
+ * @param {[string]} argSrc - File or directory name which to be send.
50
+ * @param {string} dst - Destination path.
51
+ * @param {[string]} rsyncOpt - optional arguments for rsync
52
+ *
53
+ * this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
54
+ */
55
+ async function recv(hostInfo, argSrc, dst, rsyncOpt = []) {
56
+ const src = `${hostInfo.host}:${argSrc.join(" ")}`;
57
+ const sshArgs = getSshOption(hostInfo, true);
58
+ const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
59
+
60
+ const args = ["-amvv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, src, dst];
61
+
62
+ debug(`recv ${src} to ${dst}`);
63
+ debugVerbose(`rsyncCmd=${rsyncCmd}: args=${args}`);
64
+ return fork(hostInfo, rsyncCmd, args);
65
+ }
66
+ module.exports = {
67
+ send,
68
+ recv
69
+ };
package/lib/sshExec.js ADDED
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ const crypto = require("crypto");
3
+ const debug = require("debug")("sshClientWrapper:sshExec");
4
+ const { sshCmd, getSshOption } = require("./util.js");
5
+ const fork = require("./fork.js");
6
+
7
+ /**
8
+ * Execute command on remote host.
9
+ * @param {Object} hostInfo - hostinfo object
10
+ * @param {string} cmd - Cmdline which will be executed on remote host.
11
+ * @param {Function} outputCallback - Callback routine for stdout and stderr.
12
+ */
13
+ async function sshExec(hostInfo, cmd, outputCallback, opt = null) {
14
+ debug("exec", cmd, "on remote server");
15
+ const args = getSshOption(hostInfo);
16
+ args.push(cmd);
17
+
18
+ return fork(hostInfo, sshCmd, args, opt, outputCallback);
19
+ }
20
+
21
+ /**
22
+ * Check if you can connect to sepecified server
23
+ * @param {Object} hostInfo - hostinfo object
24
+ * @param {Integer} timeout - timeout in secconds
25
+ */
26
+ async function canConnect(hostInfo, timeout) {
27
+ debug("check connectibity");
28
+ const words = crypto.randomUUID();
29
+ const args = getSshOption(hostInfo);
30
+ if (Number.isInteger(timeout) && timeout > 0) {
31
+ args.push(`-OConnectTimeout=${timeout}`);
32
+ }
33
+ args.push(`echo ${words}`);
34
+
35
+ let output = "";
36
+ const cb = (e)=>{
37
+ output += e;
38
+ };
39
+ await fork(hostInfo, sshCmd, args, null, cb);
40
+ const re = new RegExp(words);
41
+ return re.test(output);
42
+ }
43
+
44
+ /**
45
+ * remove master session
46
+ * @param {Object} hostInfo - hostinfo object
47
+ */
48
+ async function disconnect(hostInfo) {
49
+ const args = getSshOption(hostInfo);
50
+ args.push("-Oexit");
51
+
52
+ let output = "";
53
+ const cb = (e)=>{
54
+ output += e;
55
+ };
56
+ try {
57
+ await fork(hostInfo, sshCmd, args, null, cb);
58
+ } catch (err) {
59
+ if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
60
+ throw err;
61
+ }
62
+ }
63
+ }
64
+
65
+ module.exports = {
66
+ sshExec,
67
+ canConnect,
68
+ disconnect
69
+ };
package/lib/util.js ADDED
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ const { EOL } = require("os");
3
+
4
+ const sshCmd = "ssh";
5
+ const rsyncCmd = "rsync";
6
+
7
+ const rePwPrompt = /password:/;
8
+ const rePhPrompt = /Enter passphrase for key/;
9
+ const reNewHostPrompt = /Are you sure you want to continue connecting/;
10
+
11
+
12
+ /**
13
+ * bridge function to functions which need string
14
+ * @param {Function} func - function
15
+ * @param {data} data - binary data which can be converted to string
16
+ */
17
+ const buff2String = (func, data)=>{
18
+ func(data.toString().replace(/\r\n/g, EOL));
19
+ };
20
+
21
+ /**
22
+ * return ssh option with some hard corded options
23
+ * @param { Object} hostInfo - host information object defined in index.js
24
+ * @param {boolean} withoutDestination - return except for hostname
25
+ */
26
+ const getSshOption = (hostInfo, withoutDestination)=>{
27
+ const args = [];
28
+
29
+ if (!withoutDestination) {
30
+ args.push(hostInfo.host);
31
+ }
32
+ if (typeof hostInfo.sshopt === "string") {
33
+ args.push(hostInfo.sshopt);
34
+ }
35
+ if (typeof hostInfo.user === "string") {
36
+ args.push("-l");
37
+ args.push(hostInfo.user);
38
+ }
39
+ if (typeof hostInfo.port === "string" || typeof hostInfo.port === "number") {
40
+ args.push("-p");
41
+ args.push(hostInfo.port);
42
+ }
43
+ if (typeof hostInfo.keyFile === "string") {
44
+ args.push("-i");
45
+ args.push(hostInfo.keyFile);
46
+ }
47
+
48
+ if (hostInfo.noStrictHostkeyChecking) {
49
+ args.push("-oStrictHostKeyChecking=no");
50
+ }
51
+ const renewInterval = Number.isInteger(hostInfo.renewInterval) && hostInfo.renewInterval > 0 ? hostInfo.renewInterval : 180;
52
+ args.push("-oControlMaster=auto");
53
+ args.push("-oControlPath=~/.ssh/wheel-%r@%h:%p");
54
+ args.push(`-oControlPersist=${renewInterval}`);
55
+ return args;
56
+ };
57
+
58
+ /**
59
+ * sleep given time
60
+ * @param {number} time - duration in msec
61
+ */
62
+ async function sleep(time) {
63
+ return new Promise((resolve)=>{
64
+ setTimeout(()=>{
65
+ resolve();
66
+ }, time);
67
+ });
68
+ }
69
+
70
+
71
+ const isArrayOfString = (target)=>{
72
+ if (!Array.isArray(target)) {
73
+ return false;
74
+ }
75
+ return target.some((e)=>{
76
+ return typeof e !== "string";
77
+ });
78
+ };
79
+
80
+
81
+ module.exports = {
82
+ sshCmd,
83
+ rsyncCmd,
84
+ rePwPrompt,
85
+ rePhPrompt,
86
+ reNewHostPrompt,
87
+ buff2String,
88
+ getSshOption,
89
+ isArrayOfString
90
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "ssh-client-wrapper",
3
+ "version": "1.0.0",
4
+ "description": "wrapper library for open ssh client and rsync",
5
+ "main": "lib/index.js",
6
+ "scripts": {
7
+ "test": "mocha --recursive test",
8
+ "lint": "eslint --fix lib test",
9
+ "postversion": "git push origin $(git describe --abbrev=0 --tags)"
10
+ },
11
+ "engines": {
12
+ "node": ">=12.9"
13
+ },
14
+ "author": "Naoyuki Sogo <sogo@longtail-software.co.jp>",
15
+ "license": "MIT",
16
+ "husky": {
17
+ "hooks": {
18
+ "pre-commit": "lint-staged"
19
+ }
20
+ },
21
+ "lint-staged": {
22
+ "*.js": "eslint --fix"
23
+ },
24
+ "dependencies": {
25
+ "debug": "^4.3.4",
26
+ "glob": "^10.2.2",
27
+ "node-pty": "^0.10.1"
28
+ },
29
+ "devDependencies": {
30
+ "chai": "^4.3.7",
31
+ "chai-as-promised": "^7.1.1",
32
+ "chai-fs": "^2.0.0",
33
+ "eslint": "^8.38.0",
34
+ "eslint-config-eslint": "^7.0.0",
35
+ "eslint-plugin-chai-expect": "^3.0.0",
36
+ "eslint-plugin-chai-friendly": "^0.7.2",
37
+ "eslint-plugin-jsdoc": "^43.0.6",
38
+ "eslint-plugin-node": "^11.1.0",
39
+ "fs-extra": "^11.1.1",
40
+ "husky": "^8.0.3",
41
+ "lint-staged": "^13.2.1",
42
+ "mocha": "^10.2.0",
43
+ "sinon": "^15.0.3",
44
+ "sinon-chai": "^3.7.0"
45
+ }
46
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "mocha": true,
5
+ "es6": true
6
+ },
7
+ "settings": {
8
+ "node": {
9
+ "allowModules": [
10
+ "chai",
11
+ "chai-fs",
12
+ "chai-as-promised",
13
+ "sinon",
14
+ "sinon-chai",
15
+ "fs-extra",
16
+ "rewire"
17
+ ]
18
+ }
19
+ },
20
+ "plugins": [
21
+ "chai-expect",
22
+ "chai-friendly"
23
+ ],
24
+ "rules": {
25
+ "no-unused-expressions": 0,
26
+ "chai-friendly/no-unused-expressions": 2
27
+ }
28
+ }
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ const fs = require("fs-extra");
3
+ const path = require("path");
4
+
5
+ process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
6
+ Error.traceLimit = 100000;
7
+
8
+ //setup test framework
9
+ const chai = require("chai");
10
+ const { expect } = require("chai");
11
+ chai.use(require("chai-fs"));
12
+ chai.use(require("chai-as-promised"));
13
+
14
+ //testee
15
+ const { send, recv } = require("../lib/rsyncExec.js");
16
+
17
+ //test helpers
18
+ const { sshExec, disconnect } = require("../lib/sshExec.js");
19
+ const hostInfo = require("./util/hostInfo.js");
20
+ const {
21
+ clearLocalTestFiles,
22
+ clearRemoteTestFiles,
23
+ createLocalFiles,
24
+ localRoot,
25
+ localEmptyDir,
26
+ localFiles,
27
+ createRemoteFiles,
28
+ remoteRoot,
29
+ remoteEmptyDir,
30
+ remoteFiles
31
+ } = require("./util/testFiles");
32
+
33
+ const formatLsOutput = (array)=>{
34
+ return array.join(" ")
35
+ .replace(/\n/g, " ")
36
+ .trim()
37
+ .split(/\s+/);
38
+ };
39
+
40
+
41
+ //actual test start from here
42
+ describe("test rsync exec", function() {
43
+ this.timeout(10000);//eslint-disable-line no-invalid-this
44
+ const rt = [];
45
+ const output = (data)=>{
46
+ rt.push(data);
47
+ };
48
+ let remoteHome;
49
+ before(async ()=>{
50
+ await sshExec(hostInfo, "pwd", (data)=>{
51
+ remoteHome = data.trim();
52
+ });
53
+ });
54
+ beforeEach(async ()=>{
55
+ await clearRemoteTestFiles(hostInfo);
56
+ await createRemoteFiles(hostInfo);
57
+ await clearLocalTestFiles();
58
+ await createLocalFiles();
59
+ rt.splice(0, rt.length);
60
+ });
61
+ after(async ()=>{
62
+ if (!process.env.TEST_KEEP_FILES) {
63
+ await clearRemoteTestFiles(hostInfo);
64
+ await clearLocalTestFiles();
65
+ }
66
+ after(async ()=>{
67
+ await disconnect(hostInfo);
68
+ });
69
+ });
70
+ describe("#send", async ()=>{
71
+ describe("send single file", ()=>{
72
+ it("should accept relative src file and relative dst dir name", async ()=>{
73
+ await send(hostInfo, [localFiles[0]], remoteEmptyDir);
74
+
75
+ await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
76
+ expect(rt.map((e)=>{
77
+ return path.posix.basename(e);
78
+ })).to.have.members(["foo\n"]);
79
+ });
80
+ it("should accept absolute src file and relative dst dir name", async ()=>{
81
+ await send(hostInfo, [path.resolve(localFiles[3])], remoteEmptyDir);
82
+
83
+ await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
84
+ expect(rt.map((e)=>{
85
+ return path.posix.basename(e);
86
+ })).to.have.members(["piyo\n"]);
87
+ });
88
+ it("should accept relative src file and absolute dst dir name", async ()=>{
89
+ await send(hostInfo, [localFiles[0]], `${path.posix.join(remoteHome, remoteEmptyDir)}/`);
90
+
91
+ await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
92
+ expect(rt.map((e)=>{
93
+ return path.posix.basename(e);
94
+ })).to.have.members(["foo\n"]);
95
+ });
96
+ it("should accept absolute src file and absolute dst dir name", async ()=>{
97
+ await send(hostInfo, [path.resolve(localFiles[0])], path.posix.join(remoteHome, remoteEmptyDir));
98
+
99
+ await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
100
+ expect(rt.map((e)=>{
101
+ return path.posix.basename(e);
102
+ })).to.have.members(["foo\n"]);
103
+ });
104
+ it("should accept relative src file and relative dst file name", async ()=>{
105
+ await send(hostInfo, [localFiles[0]], path.posix.join(remoteEmptyDir, "hoge"));
106
+
107
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
108
+ expect(rt.map((e)=>{
109
+ return path.posix.basename(e);
110
+ })).to.have.members(["hoge\n"]);
111
+ });
112
+ it("should accept absolute src file and relative dst file name", async ()=>{
113
+ await send(hostInfo, [path.resolve(localFiles[0])], path.posix.join(remoteEmptyDir, "hoge"));
114
+
115
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
116
+ expect(rt.map((e)=>{
117
+ return path.posix.basename(e);
118
+ })).to.have.members(["hoge\n"]);
119
+ });
120
+ it("should accept relative src file and absolute dst file name", async ()=>{
121
+ await send(hostInfo, [localFiles[0]], path.posix.join(remoteHome, remoteEmptyDir, "hoge"));
122
+
123
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
124
+ expect(rt.map((e)=>{
125
+ return path.posix.basename(e);
126
+ })).to.have.members(["hoge\n"]);
127
+ });
128
+ it("should accept absolute src file and absolute dst file name", async ()=>{
129
+ await send(hostInfo, [path.resolve(localFiles[0])], path.posix.join(remoteHome, remoteEmptyDir, "hoge"));
130
+
131
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
132
+ expect(rt.map((e)=>{
133
+ return path.posix.basename(e);
134
+ })).to.have.members(["hoge\n"]);
135
+ });
136
+
137
+ it("should overwrite existing file", async ()=>{
138
+ const target = path.posix.join(remoteEmptyDir, "hoge");
139
+ await send(hostInfo, [localFiles[0]], target);
140
+ await sshExec(hostInfo, `ls ${target}`, output);
141
+ expect(rt.map((e)=>{
142
+ return path.posix.basename(e);
143
+ })).to.have.members(["hoge\n"]);
144
+ rt.splice(0, rt.length);
145
+
146
+ const time = new Date();
147
+ time.setMinutes(time.getMinutes() + 3);
148
+ await fs.utimes(localFiles[1], time, time);
149
+ await send(hostInfo, [localFiles[1]], target);
150
+ await sshExec(hostInfo, `ls ${target}`, output);
151
+ expect(rt.map((e)=>{
152
+ return path.posix.basename(e);
153
+ })).to.have.members(["hoge\n"]);
154
+
155
+ let result;
156
+ await sshExec(hostInfo, `cat ${target}`, (data)=>{
157
+ result = data;
158
+ });
159
+ expect(result).to.equal("ARssh_testLocalDir/bar\n");
160
+ });
161
+ });
162
+ describe("send directory tree", ()=>{
163
+ it("should accept relative src dirname and relative dst dirname", async ()=>{
164
+ await send(hostInfo, [localRoot], remoteEmptyDir);
165
+
166
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
167
+ expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
168
+
169
+ rt.splice(0, rt.length);
170
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
171
+ expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
172
+ });
173
+ it("should accept absolute src dirname and relative dst dirname", async ()=>{
174
+ await send(hostInfo, [path.resolve(localRoot)], remoteEmptyDir);
175
+
176
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
177
+ expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
178
+
179
+ rt.splice(0, rt.length);
180
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
181
+ expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
182
+ });
183
+ it("should accept relative src dirname and absolute dst dirname", async ()=>{
184
+ await send(hostInfo, [localRoot], path.posix.join(remoteHome, remoteEmptyDir));
185
+
186
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
187
+ expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
188
+
189
+ rt.splice(0, rt.length);
190
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
191
+ expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
192
+ });
193
+ it("should accept absolute src dirname and absolute dst dirname", async ()=>{
194
+ await send(hostInfo, [path.resolve(localRoot)], path.posix.join(remoteHome, remoteEmptyDir));
195
+
196
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
197
+ expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
198
+
199
+ rt.splice(0, rt.length);
200
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
201
+ expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
202
+ });
203
+ //only option need special treatment in rsyncExec. it's hard to set in WHEEL
204
+ it.skip("[not implemented] should send directory tree if only filter matched", async ()=>{
205
+ await send(hostInfo, [localRoot], remoteEmptyDir, [`--include=${localRoot}`, `--include=${localRoot}/ba*`, `--include=${localRoot}/hoge/*`, "--exclude=*"]);
206
+
207
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
208
+ expect(formatLsOutput(rt)).to.have.members(["hoge", "bar", "baz"]);
209
+
210
+ rt.splice(0, rt.length);
211
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
212
+ expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
213
+ });
214
+ it("should send directory tree if exclude filter not matched", async ()=>{
215
+ await send(hostInfo, [localRoot], remoteEmptyDir, ["--exclude=ba*", "--exclude=hoge*}"]);
216
+
217
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
218
+ expect(formatLsOutput(rt)).to.have.members(["foo", "hoge"]);
219
+ });
220
+ it.skip("[not implemented] should send directory tree if only filter matched but exclude filter not matched", async ()=>{
221
+ await send(hostInfo, [localRoot], remoteEmptyDir, "*/{ba*,hoge/*}", "**/poyo");
222
+
223
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
224
+ expect(formatLsOutput(rt)).to.have.members(["hoge", "bar", "baz"]);
225
+
226
+ rt.splice(0, rt.length);
227
+ await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
228
+ expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo"]);
229
+ });
230
+ it("shoud not send empty directory", async ()=>{
231
+ await send(hostInfo, [localEmptyDir], remoteEmptyDir);
232
+
233
+ await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
234
+ expect(rt).to.have.lengthOf(0);
235
+ });
236
+ it("should send files which match glob pattern", async ()=>{
237
+ await send(hostInfo, [path.join(localRoot, "b*")], remoteEmptyDir);
238
+
239
+ await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
240
+ expect(formatLsOutput(rt)).to.have.members(["bar", "baz"]);
241
+ });
242
+ });
243
+ describe("error case", ()=>{
244
+ it("should not send directory to existing file path", ()=>{
245
+ return expect(send(hostInfo, [localRoot], path.posix.join(remoteRoot, "foo"))).to.be.rejected;
246
+ });
247
+ });
248
+ });
249
+ describe("#recv", async ()=>{
250
+ describe("recieve single file", ()=>{
251
+ it("should accept relative src file and relative dst dir name", async ()=>{
252
+ await recv(hostInfo, [remoteFiles[3]], localEmptyDir);
253
+
254
+ const rt2 = await fs.readdir(localEmptyDir);
255
+ expect(rt2).to.have.members(["piyo"]);
256
+ });
257
+ it("should accept absolute src file and relative dst dir name", async ()=>{
258
+ await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[3])], localEmptyDir);
259
+
260
+ const rt2 = await fs.readdir(localEmptyDir);
261
+ expect(rt2).to.have.members(["piyo"]);
262
+ });
263
+ it("should accept relative src file and absolute dst dir name", async ()=>{
264
+ await recv(hostInfo, [remoteFiles[3]], path.resolve(localEmptyDir));
265
+
266
+ const rt2 = await fs.readdir(localEmptyDir);
267
+ expect(rt2).to.have.members(["piyo"]);
268
+ });
269
+ it("should accept absolute src file and absolute dst dir name", async ()=>{
270
+ await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[3])], path.resolve(localEmptyDir));
271
+
272
+ const rt2 = await fs.readdir(localEmptyDir);
273
+ expect(rt2).to.have.members(["piyo"]);
274
+ });
275
+ it("should accept relative src file and relative dst file name", async ()=>{
276
+ await recv(hostInfo, [remoteFiles[0]], path.join(localEmptyDir, "hoge"));
277
+
278
+ const rt2 = await fs.readdir(localEmptyDir);
279
+ expect(rt2).to.have.members(["hoge"]);
280
+ });
281
+ it("should accept absolute src file and relative dst file name", async ()=>{
282
+ await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[0])], path.join(localEmptyDir, "hoge"));
283
+
284
+ const rt2 = await fs.readdir(localEmptyDir);
285
+ expect(rt2).to.have.members(["hoge"]);
286
+ });
287
+ it("should accept relative src file and absolute dst file name", async ()=>{
288
+ await recv(hostInfo, [remoteFiles[0]], path.resolve(localEmptyDir, "hoge"));
289
+
290
+ const rt2 = await fs.readdir(localEmptyDir);
291
+ expect(rt2).to.have.members(["hoge"]);
292
+ });
293
+ it("should accept absolute src file and absolute dst file name", async ()=>{
294
+ await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[0])], path.resolve(localEmptyDir, "hoge"));
295
+
296
+ const rt2 = await fs.readdir(localEmptyDir);
297
+ expect(rt2).to.have.members(["hoge"]);
298
+ });
299
+ it("should accept glob pattern as src", async ()=>{
300
+ await recv(hostInfo, [path.posix.join(remoteRoot, "b*")], path.resolve(localEmptyDir));
301
+
302
+ const rt2 = await fs.readdir(localEmptyDir);
303
+ expect(rt2).to.have.members(["bar", "baz"]);
304
+ });
305
+ it("should accept glob pattern as src", async ()=>{
306
+ await recv(hostInfo, [path.posix.join(remoteRoot, "*/p[iu]yo")], path.resolve(localEmptyDir));
307
+
308
+ const rt2 = await fs.readdir(localEmptyDir);
309
+ expect(rt2).to.have.members(["piyo", "puyo"]);
310
+ });
311
+ it("should accept glob pattern as src", async ()=>{
312
+ await recv(hostInfo, [path.posix.join(remoteRoot, "hoge/p[iu]yo"), path.posix.join(remoteRoot, "foo")], path.resolve(localEmptyDir));
313
+
314
+ const rt2 = await fs.readdir(localEmptyDir);
315
+ expect(rt2).to.have.members(["piyo", "puyo", "foo"]);
316
+ });
317
+ });
318
+ describe("recieve directory tree", ()=>{
319
+ it("should get directory tree which match specified glob", async ()=>{
320
+ await recv(hostInfo, [path.posix.join(remoteRoot, "?o*")], localEmptyDir);
321
+
322
+ let rt2 = await fs.readdir(localEmptyDir);
323
+ expect(rt2).to.have.members(["foo", "hoge"]);
324
+ rt2 = await fs.readdir(path.join(localEmptyDir, "hoge"));
325
+ expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
326
+ });
327
+ it("should accept relative src dirname and relative dst dirname", async ()=>{
328
+ await recv(hostInfo, [remoteRoot], localEmptyDir);
329
+
330
+ let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
331
+ expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
332
+ rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
333
+ expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
334
+ });
335
+ it("should accept absolute src dirname and relative dst dirname", async ()=>{
336
+ await recv(hostInfo, [path.posix.join(remoteHome, remoteRoot)], localEmptyDir);
337
+
338
+ let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
339
+ expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
340
+ rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
341
+ expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
342
+ });
343
+ it("should accept relative src dirname and absolute dst dirname", async ()=>{
344
+ await recv(hostInfo, [remoteRoot], path.resolve(localEmptyDir));
345
+
346
+ let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
347
+ expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
348
+ rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
349
+ expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
350
+ });
351
+ it("should accept absolute src dirname and absolute dst dirname", async ()=>{
352
+ await recv(hostInfo, [path.posix.join(remoteHome, remoteRoot)], path.resolve(localEmptyDir));
353
+
354
+ let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
355
+ expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
356
+ rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
357
+ expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
358
+ });
359
+ it.skip("[not implemented] should recv files which matches only filter", async ()=>{
360
+ await recv(hostInfo, [remoteRoot], localEmptyDir, "*/{ba*,hoge/*}");
361
+
362
+ let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
363
+ expect(rt2).to.have.members(["bar", "baz", "hoge"]);
364
+ rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
365
+ expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
366
+ });
367
+ it("should not recv files which matches exclude filter", async ()=>{
368
+ await recv(hostInfo, [remoteRoot], localEmptyDir, ["--exclude=*/ba*", "--exclude=hoge"]);
369
+
370
+ const rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
371
+ expect(rt2).to.have.members(["foo"]);
372
+ });
373
+ it.skip("[not implemented] should recv files which matches only filter but should not recv which matches exclude filter", async ()=>{
374
+ await recv(hostInfo, [remoteRoot], localEmptyDir, "*/{ba*,hoge/*}", "**/piyo");
375
+
376
+ let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
377
+ expect(rt2).to.have.members(["bar", "baz", "hoge"]);
378
+ rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
379
+ expect(rt2).to.have.members(["puyo", "poyo"]);
380
+ });
381
+ });
382
+ describe("error case", ()=>{
383
+ it("should not send directory to existing file path", ()=>{
384
+ return expect(recv(hostInfo, [remoteRoot], localFiles[0])).to.be.rejected;
385
+ });
386
+ });
387
+ });
388
+ });
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
4
+ Error.traceLimit = 100000;
5
+
6
+ //setup test framework
7
+ const chai = require("chai");
8
+ const { expect } = require("chai");
9
+ const sinon = require("sinon");
10
+ chai.use(require("sinon-chai"));
11
+ chai.use(require("chai-as-promised"));
12
+
13
+ const sshout = sinon.stub();
14
+
15
+ //testee
16
+ const { sshExec, canConnect, disconnect } = require("../lib/sshExec.js");
17
+
18
+ //test helpers
19
+ const hostInfo = require("./util/hostInfo.js");
20
+ const { nonExisting } = require("./util/testFiles.js");
21
+
22
+
23
+ describe("test for ssh execution", function() {
24
+ this.timeout(10000);//eslint-disable-line no-invalid-this
25
+ beforeEach(()=>{
26
+ sshout.reset();
27
+ });
28
+ after(async ()=>{
29
+ await disconnect(hostInfo);
30
+ });
31
+ describe("#exec", ()=>{
32
+ const testText = "hoge";
33
+ it("should execute single command with stdout", async ()=>{
34
+ const rt = await sshExec(hostInfo, `echo ${testText}`, sshout);
35
+ expect(rt).to.equal(0);
36
+ expect(sshout).to.be.calledOnce;
37
+ expect(sshout).to.be.calledWith("hoge\n");
38
+ });
39
+ it("should execute single command with stderr", async ()=>{
40
+ const rt = await sshExec(hostInfo, `echo ${testText} >&2`, sshout);
41
+ expect(rt).to.equal(0);
42
+ expect(sshout).to.be.calledOnce;
43
+ expect(sshout).to.be.calledWith("hoge\n");
44
+ });
45
+ it("should execute single command with stdout & stderr", async ()=>{
46
+ const rt = await sshExec(hostInfo, `echo ${testText}; echo ${testText}>&2`, sshout);
47
+ expect(rt).to.equal(0);
48
+ expect(sshout).to.be.calledTwice;
49
+ const call0 = sshout.getCall(0);
50
+ const call1 = sshout.getCall(1);
51
+ expect(call0).to.be.calledWith("hoge\n");
52
+ expect(call1).to.be.calledWith("hoge\n");
53
+ });
54
+
55
+ //please note that exec() resolves with non-zero value
56
+ //(126 permisssion deny or 127 file not found)
57
+ //but does not reject in following 2 cases
58
+ it("should not execute command which do not have exec permission", async ()=>{
59
+ await sshExec(hostInfo, "echo echo hoge >hoge");
60
+ await sshExec(hostInfo, "chmod ugo-x hoge");
61
+ const rt = await sshExec(hostInfo, "./hoge");
62
+ expect(rt).to.equal(126);
63
+ });
64
+ it("should resolve with 127 if command is not found", async ()=>{
65
+ const rt = await sshExec(hostInfo, `./${nonExisting}`);
66
+ expect(rt).to.equal(127);
67
+ });
68
+ });
69
+ describe("#canConnect", ()=>{
70
+ let hostInfo2;
71
+ beforeEach(async ()=>{
72
+ hostInfo2 = { ...hostInfo };
73
+ await disconnect(hostInfo);
74
+ });
75
+ it("should be resolved with true", async ()=>{
76
+ expect(await canConnect(hostInfo)).to.be.true;
77
+ });
78
+ it("should be rejected if user does not exist", async ()=>{
79
+ hostInfo2.user = "xxxx";
80
+ return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
81
+ });
82
+ it("should be rejected if password is wrong", async ()=>{
83
+ hostInfo2.password = "xxxx";
84
+ return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
85
+ });
86
+ it("should be rejected if host does not exist", async ()=>{
87
+ hostInfo2.host = "foo.bar.example.com";
88
+ return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
89
+ });
90
+ it("should be rejected if host(ip address) does not exist", async ()=>{
91
+ hostInfo2.host = "192.0.2.1";
92
+ return expect(canConnect(hostInfo2, 5)).to.be.rejectedWith(255);
93
+ });
94
+ it("should be rejected if port number is out of range(-1)", async ()=>{
95
+ hostInfo2.port = -1;
96
+ return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
97
+ });
98
+ it("should be rejected if port number is out of range(65536)", async ()=>{
99
+ hostInfo2.port = 65536;
100
+ return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ module.exports = {
3
+ host: process.env.TEST_HOST || "localhost",
4
+ user: process.env.TEST_USER || null,
5
+ port: process.env.TEST_PORT || null,
6
+ password: process.env.TEST_PW,
7
+ passphrase: process.env.TEST_PH,
8
+ keyFile: process.env.TEST_PRIVATE_KEY,
9
+ noStrictHostkeyChecking: true
10
+ };
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ const fs = require("fs-extra");
3
+ const path = require("path");
4
+
5
+ const { sshExec } = require("../../lib/sshExec.js");
6
+
7
+ /*
8
+ * test directory tree
9
+ * ${ROOT}
10
+ * +-- huga/ (empty directory)
11
+ * +-- foo
12
+ * +-- bar
13
+ * +-- baz
14
+ * +-- hoge
15
+ * +-- piyo
16
+ * +-- puyo
17
+ * +-- poyo
18
+ *
19
+ * ${ROOT} is "ARssh_testLocalDir" on local side
20
+ * it is ARssh_testLocalDir on remote side
21
+ *
22
+ */
23
+
24
+ const localRoot = "ARssh_testLocalDir";
25
+ const localEmptyDir = path.join(localRoot, "huga");
26
+ const localFiles = [
27
+ path.join(localRoot, "foo"),
28
+ path.join(localRoot, "bar"),
29
+ path.join(localRoot, "baz"),
30
+ path.join(localRoot, "hoge", "piyo"),
31
+ path.join(localRoot, "hoge", "puyo"),
32
+ path.join(localRoot, "hoge", "poyo")
33
+ ];
34
+
35
+ const remoteRoot = "ARssh_testRemoteDir";
36
+ const remoteEmptyDir = `${remoteRoot}/huga`;
37
+ const remoteFiles = [
38
+ `${remoteRoot}/foo`,
39
+ `${remoteRoot}/bar`,
40
+ `${remoteRoot}/baz`,
41
+ `${remoteRoot}/hoge/piyo`,
42
+ `${remoteRoot}/hoge/puyo`,
43
+ `${remoteRoot}/hoge/poyo`
44
+ ];
45
+ const nonExisting = "ARSSH_nonExisting";
46
+
47
+ /*
48
+ * prepare local files which contain its filename
49
+ */
50
+ async function createLocalFiles() {
51
+ const localDir2 = path.join(localRoot, "hoge");
52
+ const promises = [];
53
+ await fs.mkdir(localRoot);
54
+ await fs.mkdir(localDir2);
55
+ promises.push(fs.mkdir(localEmptyDir));
56
+ localFiles.forEach((localFile)=>{
57
+ promises.push(fs.writeFile(localFile, `${localFile}\n`));
58
+ });
59
+ return Promise.all(promises);
60
+ }
61
+
62
+
63
+ async function clearLocalTestFiles() {
64
+ return fs.remove(localRoot);
65
+ }
66
+
67
+ async function createRemoteFiles(hostInfo) {
68
+ //create remote files
69
+ await sshExec(hostInfo, `mkdir -p ${remoteRoot}/hoge`);
70
+ await sshExec(hostInfo, `mkdir -p ${remoteEmptyDir}`);
71
+ const script = remoteFiles.reduce((a, remoteFile)=>{
72
+ return `${a}echo ${remoteFile} > ${remoteFile};`;
73
+ }, "");
74
+ return sshExec(hostInfo, `${script}`);
75
+ }
76
+
77
+ async function clearRemoteTestFiles(hostInfo) {
78
+ return sshExec(hostInfo, `rm -fr ${remoteRoot}`);
79
+ }
80
+
81
+ module.exports.createLocalFiles = createLocalFiles;
82
+ module.exports.createRemoteFiles = createRemoteFiles;
83
+ module.exports.clearLocalTestFiles = clearLocalTestFiles;
84
+ module.exports.clearRemoteTestFiles = clearRemoteTestFiles;
85
+ module.exports.localRoot = localRoot;
86
+ module.exports.localEmptyDir = localEmptyDir;
87
+ module.exports.localFiles = localFiles;
88
+ module.exports.nonExisting = nonExisting;
89
+ module.exports.remoteRoot = remoteRoot;
90
+ module.exports.remoteEmptyDir = remoteEmptyDir;
91
+ module.exports.remoteFiles = remoteFiles;