Files
smartssh/test/test.node.ts
T

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();