Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions __tests__/commands/wp-ssh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import Stream from 'node:stream';
import { Client } from 'ssh2';
import { PassThrough } from 'stream';

import { WPCliCommandOverSSH } from '../../src/commands/wp-ssh';
import API from '../../src/lib/api';
import { CommandTracker } from '../../src/lib/tracker';

const processExitMock = jest
.spyOn( process, 'exit' )
.mockImplementation( ( code?: string | number | null | undefined ) => {
throw new Error( `Process exited with code: ${ code }` );
} );

const consoleErrorMock = jest.spyOn( console, 'error' ).mockImplementation( () => {} );

const mockExec = jest.fn< Client[ 'exec' ] >();
const mockEnd = jest.fn< Client[ 'end' ] >();

const EventEmitter = jest.requireActual< typeof import('events') >( 'events' );

class MockClient extends EventEmitter {
public connect() {
this.emit( 'ready' );

return this;
}

public exec = mockExec;

public end = mockEnd;
}

jest.mock( 'ssh2', () => {
const original = jest.requireActual< typeof import('ssh2') >( 'ssh2' );

return {
...original,
__esModule: true,

Client: jest.fn().mockImplementation( () => {
return new MockClient();
} ),
};
} );

// Mock the API
const triggerWPCLIMutationMock = jest.fn( async () => {
return Promise.resolve( {
data: {
triggerWPCLICommandOnAppEnvironment: {
inputToken: 'test-token',
sshAuthentication: {
host: 'test-host',
port: 22,
username: 'test-user',
privateKey: 'test-key',
passphrase: 'test-passphrase',
},
command: {
guid: 'test-guid',
},
},
},
} );
} );

jest.mock( '../../src/lib/api' );
jest.mocked( API ).mockImplementation(
() =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
( {
mutate: triggerWPCLIMutationMock,
} as any )
);

// Mock tracker
const mockTracker = jest.fn() as CommandTracker;
jest.mock( '../../src/lib/tracker', () => ( {
makeCommandTracker: jest.fn( () => mockTracker ),
} ) );

describe( 'WPCommand', () => {
const app = { id: 123 };
const env = { id: 456 };
let cmd: WPCliCommandOverSSH;

beforeEach( () => {
jest.clearAllMocks();
cmd = new WPCliCommandOverSSH( app, env );
} );

describe( 'run', () => {
it( 'should pass the correct arguments to the SSH connection when executing a command', async () => {
const dummyStream = new PassThrough();

mockExec.mockImplementation( ( (
_cmd: string,
callback: ( err: undefined, stream: Stream ) => void
) => {
callback( undefined, dummyStream );

// Simulate the SSH connection closing right after the command is executed
dummyStream.emit( 'close' );
} ) as unknown as Client[ 'exec' ] );

await cmd.run( 'plugin list' );

expect( mockExec ).toHaveBeenCalledWith(
expect.stringMatching(
/GUID=test-guid INPUT_TOKEN=test-token VERSION=\S+ ROWS=\d+ COLUMNS=\d+ TTY=\S+/
),
expect.anything()
);

dummyStream.end();

expect( processExitMock ).not.toHaveBeenCalled();
} );

it( 'should throw an error when SSH connection failed', async () => {
const dummyStream = new PassThrough();

mockExec.mockImplementation( ( (
_cmd: string,
callback: ( err: Error, stream: Stream ) => void
) => {
callback( new Error( 'ops!' ), dummyStream );
} ) as unknown as Client[ 'exec' ] );

const result = cmd.run( 'plugin list' );

await expect( result ).rejects.toThrow( 'Process exited with code: 1' );

expect( consoleErrorMock ).toHaveBeenCalledWith( expect.stringMatching( /ops!/ ) );
} );

it( 'should throw an error when wp-cli command returned a non-zero status code', async () => {
const dummyStream = new PassThrough();

mockExec.mockImplementation( ( (
_cmd: string,
callback: ( err: undefined, stream: Stream ) => void
) => {
callback( undefined, dummyStream );

// Simulate the SSH connection closing right after the command is executed
dummyStream.emit( 'exit', 23 );
dummyStream.emit( 'close' );
} ) as unknown as Client[ 'exec' ] );

const result = cmd.run( 'plugin list' );

await expect( result ).rejects.toThrow( 'Process exited with code: 23' );
} );
} );
} );
14 changes: 8 additions & 6 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"@types/proxy-from-env": "^1.0.4",
"@types/semver": "^7.5.5",
"@types/shelljs": "^0.8.15",
"@types/ssh2": "^1.15.4",
"@types/tar": "^6.1.13",
"@types/update-notifier": "^6.0.8",
"@types/xml2js": "^0.4.14",
Expand Down Expand Up @@ -170,6 +171,7 @@
"socket.io-client": "^4.5.3",
"socket.io-stream": "npm:@wearemothership/socket.io-stream@^0.9.1",
"socks-proxy-agent": "^5.0.1",
"ssh2": "1.16.0",
"tar": "^7.4.0",
"update-notifier": "7.3.1",
"uuid": "11.1.0",
Expand Down
8 changes: 8 additions & 0 deletions src/bin/vip-wp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SocketIO from 'socket.io-client';
import IOStream from 'socket.io-stream';
import { Writable } from 'stream';

import { WPCliCommandOverSSH } from '../commands/wp-ssh';
import API, { API_HOST, disableGlobalGraphQLErrorHandling } from '../lib/api';
import commandWrapper, { getEnvIdentifier } from '../lib/cli/command';
import * as exit from '../lib/cli/exit';
Expand All @@ -29,6 +30,7 @@ const appQuery = `id, name,
appId
type
name
wpcliStrategy
primaryDomain {
name
}
Expand Down Expand Up @@ -368,6 +370,12 @@ commandWrapper( {

let countSIGINT = 0;

if ( opts.env.wpcliStrategy === 'ssh' ) {
const wpCommandRunner = new WPCliCommandOverSSH( opts.app, opts.env );
await wpCommandRunner.run( cmd, { command: commandForAnalytics } );
return;
}

const mutableStdout = new Writable( {
write( chunk, encoding, callback ) {
if ( ! this.muted ) {
Expand Down
Loading