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((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((resolve, reject) => { testSshServer.close((error?: Error) => { if (error) { reject(error); return; } resolve(); }); }); }); export default tap.start();