215 lines
6.7 KiB
TypeScript
215 lines
6.7 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as smartssh from '../ts/index.js';
|
|
import fs from 'fs-extra';
|
|
import type { AddressInfo } from 'net';
|
|
import * as path from 'path';
|
|
import ssh2 from 'ssh2';
|
|
import type { Server as TSsh2Server } from 'ssh2';
|
|
|
|
const { Server, utils } = ssh2;
|
|
|
|
const testDir = path.join(process.cwd(), '.nogit/test/temp');
|
|
const configPath = path.join(testDir, 'config');
|
|
|
|
let testSshInstance: smartssh.SshInstance;
|
|
let testSshKey: smartssh.SshKey;
|
|
let testSshServer: TSsh2Server;
|
|
let testSshServerPort: number;
|
|
|
|
const createTestSshServer = async () => {
|
|
const hostKeys = [utils.generateKeyPairSync('ed25519').private];
|
|
const server = new Server({ hostKeys }, (client) => {
|
|
client.on('authentication', (ctx) => {
|
|
if (ctx.method === 'password' && ctx.username === 'tester' && ctx.password === 'secret') {
|
|
ctx.accept();
|
|
return;
|
|
}
|
|
ctx.reject();
|
|
});
|
|
|
|
client.on('ready', () => {
|
|
client.on('session', (accept) => {
|
|
const session = accept();
|
|
session.on('exec', (acceptExec, _rejectExec, info) => {
|
|
const stream = acceptExec();
|
|
if (info.command === 'printf smartssh') {
|
|
stream.write('smartssh\n');
|
|
stream.stderr.write('warn\n');
|
|
stream.exit(0);
|
|
stream.end();
|
|
return;
|
|
}
|
|
|
|
stream.stderr.write(`unknown command: ${info.command}\n`);
|
|
stream.exit(127);
|
|
stream.end();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
|
|
const address = server.address() as AddressInfo;
|
|
return {
|
|
server,
|
|
port: address.port,
|
|
};
|
|
};
|
|
|
|
tap.test('should prepare a clean test directory', async () => {
|
|
await fs.emptyDir(testDir);
|
|
});
|
|
|
|
tap.test('should create a valid SshKey object', async () => {
|
|
testSshKey = new smartssh.SshKey({
|
|
host: 'example.com',
|
|
private: 'someExamplePrivateKey',
|
|
public: 'someExamplePublicKey',
|
|
});
|
|
expect(testSshKey).toBeInstanceOf(smartssh.SshKey);
|
|
});
|
|
tap.test('.type should be a valid type', async () => {
|
|
expect(testSshKey.type).toEqual('duplex');
|
|
});
|
|
tap.test('.publicKey should be public key', async () => {
|
|
expect(testSshKey.pubKey).toEqual('someExamplePublicKey');
|
|
});
|
|
tap.test('.privateKey should be private key', async () => {
|
|
expect(testSshKey.privKey).toEqual('someExamplePrivateKey');
|
|
});
|
|
tap.test('.publicKeyBase64 should be public key base 64 encoded', async () => {
|
|
// tslint:disable-next-line:no-unused-expression
|
|
testSshKey.pubKeyBase64;
|
|
});
|
|
tap.test('.store() should store the file to disk', async () => {
|
|
await testSshKey.store(testDir);
|
|
const privateKeyMode = (await fs.stat(path.join(testDir, 'example.com'))).mode & 0o777;
|
|
const publicKeyMode = (await fs.stat(path.join(testDir, 'example.com.pub'))).mode & 0o777;
|
|
expect(privateKeyMode).toEqual(0o600);
|
|
expect(publicKeyMode).toEqual(0o644);
|
|
});
|
|
tap.test('.store() should reject unsafe host aliases', async () => {
|
|
expect(() => {
|
|
new smartssh.SshKey({
|
|
host: '../evil',
|
|
private: 'somePrivateKey',
|
|
});
|
|
}).toThrow();
|
|
});
|
|
|
|
// SSH INstance
|
|
tap.test("'new' keyword should create a new SshInstance object from class", async () => {
|
|
testSshInstance = new smartssh.SshInstance({
|
|
sshDirPath: testDir,
|
|
});
|
|
expect(testSshInstance).toBeInstanceOf(smartssh.SshInstance);
|
|
});
|
|
tap.test('.addKey() should accept a new SshKey object', async () => {
|
|
testSshInstance.addKey(
|
|
new smartssh.SshKey({
|
|
public: 'somePublicKey',
|
|
private: 'somePrivateKey',
|
|
host: 'gitlab.com',
|
|
})
|
|
);
|
|
testSshInstance.addKey(
|
|
new smartssh.SshKey({
|
|
public: 'somePublicKey',
|
|
private: 'somePrivateKey',
|
|
host: 'bitbucket.org',
|
|
})
|
|
);
|
|
testSshInstance.addKey(
|
|
new smartssh.SshKey({
|
|
public: 'someGitHubPublicKey',
|
|
private: 'someGitHubPrivateKey',
|
|
host: 'github.com',
|
|
})
|
|
);
|
|
});
|
|
|
|
tap.test('.sshKeys should point to an array of sshKeys', async () => {
|
|
let sshKeyArray = testSshInstance.sshKeys;
|
|
expect(sshKeyArray).toBeInstanceOf(Array);
|
|
expect(sshKeyArray[0].host).toEqual('gitlab.com');
|
|
expect(sshKeyArray[1].host).toEqual('bitbucket.org');
|
|
expect(sshKeyArray[2].host).toEqual('github.com');
|
|
});
|
|
|
|
tap.test('.getKey() should get a specific key selected by host', async () => {
|
|
const sshKey = testSshInstance.getKey('github.com');
|
|
expect(sshKey).toBeInstanceOf(smartssh.SshKey);
|
|
expect(sshKey?.pubKey).toEqual('someGitHubPublicKey');
|
|
});
|
|
|
|
tap.test('.removeKey() should remove a key', async () => {
|
|
const sshKey = testSshInstance.getKey('bitbucket.org');
|
|
expect(sshKey).toBeInstanceOf(smartssh.SshKey);
|
|
testSshInstance.removeKey(sshKey!);
|
|
expect(testSshInstance.sshKeys[1].host).toEqual('github.com');
|
|
});
|
|
|
|
tap.test('it should store to disk', async () => {
|
|
testSshInstance.writeToDisk();
|
|
const config = await fs.readFile(configPath, 'utf8');
|
|
expect(config).toInclude(`IdentityFile ${path.join(testDir, 'github.com')}`);
|
|
expect(config).toInclude('StrictHostKeyChecking yes');
|
|
expect(config).not.toInclude('StrictHostKeyChecking no');
|
|
});
|
|
|
|
tap.test('it should read keys back from disk', async () => {
|
|
const readBackInstance = new smartssh.SshInstance({ sshDirPath: testDir });
|
|
readBackInstance.readFromDisk();
|
|
const githubKey = readBackInstance.getKey('github.com');
|
|
expect(githubKey).toBeInstanceOf(smartssh.SshKey);
|
|
expect(githubKey?.privKey).toEqual('someGitHubPrivateKey');
|
|
expect(githubKey?.pubKey).toEqual('someGitHubPublicKey');
|
|
});
|
|
|
|
tap.test('SshClient should require host validation by default', async () => {
|
|
let strictHostError: Error | undefined;
|
|
try {
|
|
await smartssh.SshClient.connect({
|
|
host: '127.0.0.1',
|
|
username: 'tester',
|
|
});
|
|
} catch (error) {
|
|
strictHostError = error as Error;
|
|
}
|
|
expect(strictHostError?.message).toInclude('strictHostKeyChecking');
|
|
});
|
|
|
|
tap.test('SshClient should execute a command over a real SSH server', async () => {
|
|
const testServer = await createTestSshServer();
|
|
testSshServer = testServer.server;
|
|
testSshServerPort = testServer.port;
|
|
|
|
const sshClient = await smartssh.SshClient.connect({
|
|
host: '127.0.0.1',
|
|
port: testSshServerPort,
|
|
username: 'tester',
|
|
password: 'secret',
|
|
strictHostKeyChecking: false,
|
|
});
|
|
const result = await sshClient.exec('printf smartssh');
|
|
await sshClient.close();
|
|
|
|
expect(result.code).toEqual(0);
|
|
expect(result.stdout).toEqual('smartssh\n');
|
|
expect(result.stderr).toEqual('warn\n');
|
|
});
|
|
|
|
tap.test('should close the real SSH test server', async () => {
|
|
await new Promise<void>((resolve, reject) => {
|
|
testSshServer.close((error?: Error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
export default tap.start();
|