Skip to content

Commit aad6d60

Browse files
fix(express): replace sinon stubs with proxyquire for ESM compatibility
Replace sinon.stub() with proxyquire for mocking clientRoutes module imports. This fixes tests now that we use tsx which runs in ESM mode - ES modules are immutable and cannot be modified at runtime, preventing sinon from stubbing them. Proxyquire works by intercepting module resolution at the CommonJS level. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> TICKET: WP-5599
1 parent 12ba9f6 commit aad6d60

File tree

9 files changed

+220
-130
lines changed

9 files changed

+220
-130
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import {
3232
} from '@bitgo/sdk-core';
3333
import { BitGo, BitGoOptions, Coin, CustomSigningFunction, SignedTransaction, SignedTransactionRequest } from 'bitgo';
3434
import * as bodyParser from 'body-parser';
35-
import * as debugLib from 'debug';
36-
import * as express from 'express';
35+
import debugLib from 'debug';
36+
import express from 'express';
3737
import type { ParamsDictionary } from 'express-serve-static-core';
3838
import * as _ from 'lodash';
3939
import * as url from 'url';

modules/express/src/expressApp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
/**
22
* @prettier
33
*/
4-
import * as express from 'express';
4+
import express from 'express';
55
import * as path from 'path';
66
import * as _ from 'lodash';
7-
import * as debugLib from 'debug';
7+
import debugLib from 'debug';
88
import * as https from 'https';
99
import * as http from 'http';
10-
import * as morgan from 'morgan';
10+
import morgan, { token as morganToken } from 'morgan';
1111
import * as fs from 'fs';
1212
import type { Request as StaticRequest } from 'express-serve-static-core';
13-
import * as timeout from 'connect-timeout';
13+
import timeout from 'connect-timeout';
1414
import * as bodyParser from 'body-parser';
1515

1616
import { Config, config } from './config';
@@ -51,7 +51,7 @@ function setupLogging(app: express.Application, config: Config): void {
5151
}
5252

5353
app.use(middleware);
54-
morgan.token('remote-user', function (req: StaticRequest) {
54+
morganToken('remote-user', function (req: StaticRequest) {
5555
return req.isProxy ? 'proxy' : 'local_express';
5656
});
5757
}

modules/express/test/unit/bitgoExpress.ts

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
import * as should from 'should';
1+
import should from 'should';
22
import 'should-http';
33
import 'should-sinon';
44
import '../lib/asserts';
55

6-
import * as nock from 'nock';
76
import * as sinon from 'sinon';
7+
import * as nock from 'nock';
8+
import proxyquire from 'proxyquire';
89

9-
import * as fs from 'fs';
10-
import * as http from 'http';
11-
import * as https from 'https';
12-
import * as debugLib from 'debug';
13-
import * as path from 'path';
10+
import debugLib from 'debug';
1411
import { Environments } from 'bitgo';
1512

1613
import { SSL_OP_NO_TLSv1 } from 'constants';
1714
import { TlsConfigurationError, NodeEnvironmentError } from '../../src/errors';
1815

19-
nock.disableNetConnect();
20-
2116
import { app as expressApp, startup, createServer, createBaseUri, prepareIpc } from '../../src/expressApp';
22-
import * as clientRoutes from '../../src/clientRoutes';
17+
18+
// commonJS imports to allow stubbing
19+
const path = require('path');
20+
const http = require('http');
21+
const https = require('https');
22+
const fs = require('fs');
23+
24+
nock.disableNetConnect();
25+
proxyquire.noPreserveCache();
2326

2427
describe('Bitgo Express', function () {
2528
describe('server initialization', function () {
@@ -371,7 +374,7 @@ describe('Bitgo Express', function () {
371374
});
372375

373376
it('should remove the socket before binding if IPC socket exists and is a socket', async () => {
374-
const statStub = sinon.stub(fs, 'statSync').returns({ isSocket: () => true } as unknown as fs.Stats);
377+
const statStub = sinon.stub(fs, 'statSync').returns({ isSocket: () => true });
375378
const unlinkStub = sinon.stub(fs, 'unlinkSync');
376379
await prepareIpc('testipc').should.be.resolved();
377380
unlinkStub.calledWithExactly('testipc').should.be.true();
@@ -381,7 +384,7 @@ describe('Bitgo Express', function () {
381384
});
382385

383386
it('should fail if IPC socket is not actually a socket', async () => {
384-
const statStub = sinon.stub(fs, 'statSync').returns({ isSocket: () => false } as unknown as fs.Stats);
387+
const statStub = sinon.stub(fs, 'statSync').returns({ isSocket: () => false });
385388
const unlinkStub = sinon.stub(fs, 'unlinkSync');
386389
await prepareIpc('testipc').should.be.rejectedWith(/IPC socket is not actually a socket/);
387390
unlinkStub.notCalled.should.be.true();
@@ -404,58 +407,81 @@ describe('Bitgo Express', function () {
404407
});
405408

406409
it('should only call setupAPIRoutes when running in regular mode', () => {
410+
const setupAPIRoutesStub = sinon.stub();
411+
const setupSigningRoutesStub = sinon.stub();
412+
413+
const { app } = proxyquire('../../src/expressApp', {
414+
'./clientRoutes': {
415+
setupAPIRoutes: setupAPIRoutesStub,
416+
setupSigningRoutes: setupSigningRoutesStub,
417+
setupLightningSignerNodeRoutes: sinon.stub(),
418+
},
419+
});
420+
407421
const args: any = {
408422
env: 'test',
409423
signerMode: undefined,
410424
};
411425

412-
const apiStub = sinon.stub(clientRoutes, 'setupAPIRoutes');
413-
const signerStub = sinon.stub(clientRoutes, 'setupSigningRoutes');
414-
415-
expressApp(args);
416-
apiStub.should.have.been.calledOnce();
417-
signerStub.called.should.be.false();
418-
apiStub.restore();
419-
signerStub.restore();
426+
app(args);
427+
setupAPIRoutesStub.should.have.been.calledOnce();
428+
setupSigningRoutesStub.called.should.be.false();
420429
});
421430

422431
it('should only call setupLightningSignerNodeRoutes when running with lightningSignerFileSystemPath', () => {
432+
const setupAPIRoutesStub = sinon.stub();
433+
const setupSigningRoutesStub = sinon.stub();
434+
const setupLightningSignerNodeRoutesStub = sinon.stub();
435+
const { app } = proxyquire('../../src/expressApp', {
436+
'./clientRoutes': {
437+
setupAPIRoutes: setupAPIRoutesStub,
438+
setupSigningRoutes: setupSigningRoutesStub,
439+
setupLightningSignerNodeRoutes: setupLightningSignerNodeRoutesStub,
440+
},
441+
fs: {
442+
readFileSync: () => {
443+
return validLightningSignerConfigJSON;
444+
},
445+
},
446+
});
447+
423448
const args: any = {
424449
env: 'test',
425450
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
426451
};
427452

428-
const readValidStub = sinon.stub(fs, 'readFileSync').returns(validLightningSignerConfigJSON);
429-
const lightningSignerStub = sinon.stub(clientRoutes, 'setupLightningSignerNodeRoutes');
430-
const apiStub = sinon.stub(clientRoutes, 'setupAPIRoutes');
431-
const signerStub = sinon.stub(clientRoutes, 'setupSigningRoutes');
432-
433-
expressApp(args);
434-
lightningSignerStub.should.have.been.calledOnce();
435-
apiStub.should.have.been.calledOnce();
436-
signerStub.called.should.be.false();
437-
apiStub.restore();
438-
signerStub.restore();
439-
readValidStub.restore();
453+
app(args);
454+
setupLightningSignerNodeRoutesStub.should.have.been.calledOnce();
455+
setupAPIRoutesStub.should.have.been.calledOnce();
456+
setupSigningRoutesStub.called.should.be.false();
440457
});
441458

442459
it('should only call setupSigningRoutes when running in signer mode', () => {
460+
const setupAPIRoutesStub = sinon.stub();
461+
const setupSigningRoutesStub = sinon.stub();
462+
463+
const { app } = proxyquire('../../src/expressApp', {
464+
'./clientRoutes': {
465+
setupAPIRoutes: setupAPIRoutesStub,
466+
setupSigningRoutes: setupSigningRoutesStub,
467+
setupLightningSignerNodeRoutes: sinon.stub(),
468+
},
469+
fs: {
470+
readFileSync: () => {
471+
return validPrvJSON;
472+
},
473+
},
474+
});
475+
443476
const args: any = {
444477
env: 'test',
445478
signerMode: 'signerMode',
446479
signerFileSystemPath: 'signerFileSystemPath',
447480
};
448481

449-
const apiStub = sinon.stub(clientRoutes, 'setupAPIRoutes');
450-
const signerStub = sinon.stub(clientRoutes, 'setupSigningRoutes');
451-
const readFileStub = sinon.stub(fs, 'readFileSync').returns(validPrvJSON);
452-
453-
expressApp(args);
454-
signerStub.should.have.been.calledOnce();
455-
apiStub.called.should.be.false();
456-
apiStub.restore();
457-
signerStub.restore();
458-
readFileStub.restore();
482+
app(args);
483+
setupSigningRoutesStub.should.have.been.calledOnce();
484+
setupAPIRoutesStub.called.should.be.false();
459485
});
460486

461487
it('should require a signerFileSystemPath and signerMode are both set when running in signer mode', function () {
@@ -555,7 +581,7 @@ describe('Bitgo Express', function () {
555581

556582
it('should set keepAliveTimeout and headersTimeout if specified in config for HTTP server', async function () {
557583
const createServerStub = sinon.stub(http, 'createServer').callsFake(() => {
558-
return { listen: sinon.stub(), setTimeout: sinon.stub() } as unknown as http.Server;
584+
return { listen: sinon.stub(), setTimeout: sinon.stub() };
559585
});
560586

561587
const args: any = {
@@ -575,7 +601,7 @@ describe('Bitgo Express', function () {
575601

576602
it('should set keepAliveTimeout and headersTimeout if specified in config for HTTPS server', async function () {
577603
const createServerStub = sinon.stub(https, 'createServer').callsFake(() => {
578-
return { listen: sinon.stub(), setTimeout: sinon.stub() } as unknown as https.Server;
604+
return { listen: sinon.stub(), setTimeout: sinon.stub() };
579605
});
580606

581607
const args: any = {
@@ -597,7 +623,7 @@ describe('Bitgo Express', function () {
597623

598624
it('should not set keepAliveTimeout and headersTimeout if not specified in config for HTTP server', async function () {
599625
const createServerStub = sinon.stub(http, 'createServer').callsFake(() => {
600-
return { listen: sinon.stub(), setTimeout: sinon.stub() } as unknown as http.Server;
626+
return { listen: sinon.stub(), setTimeout: sinon.stub() };
601627
});
602628

603629
const args: any = {
@@ -616,7 +642,7 @@ describe('Bitgo Express', function () {
616642

617643
it('should not set keepAliveTimeout and headersTimeout if not specified in config for HTTPS server', async function () {
618644
const createServerStub = sinon.stub(https, 'createServer').callsFake(() => {
619-
return { listen: sinon.stub(), setTimeout: sinon.stub() } as unknown as https.Server;
645+
return { listen: sinon.stub(), setTimeout: sinon.stub() };
620646
});
621647

622648
const args: any = {

modules/express/test/unit/clientRoutes/externalSign.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
MPCv2SignatureShareRound3Input,
3636
} from '@bitgo/public-types';
3737
import * as assert from 'assert';
38-
import * as nock from 'nock';
38+
import nock from 'nock';
3939
import * as fs from 'fs';
4040
import * as express from 'express';
4141

@@ -504,7 +504,7 @@ describe('External signer', () => {
504504
const walletPassphrase = 'testPass';
505505

506506
const [userShare, backupShare, bitgoShare] = await DklsUtils.generateDKGKeyShares();
507-
assert(backupShare, 'backupShare is not defined');
507+
assert.ok(backupShare, 'backupShare is not defined');
508508

509509
const bgTest = new BitGo({ env: 'test' });
510510
const userKeyShare = userShare.getKeyShare().toString('base64');
@@ -555,7 +555,7 @@ describe('External signer', () => {
555555
round1Result.signatureShareRound1,
556556
round1Result.userGpgPubKey
557557
);
558-
assert(
558+
assert.ok(
559559
txRequestRound1.transactions &&
560560
txRequestRound1.transactions.length === 1 &&
561561
txRequestRound1.transactions[0].signatureShares.length === 2,
@@ -589,7 +589,7 @@ describe('External signer', () => {
589589
round2Result.signatureShareRound2,
590590
round1Result.userGpgPubKey
591591
);
592-
assert(
592+
assert.ok(
593593
txRequestRound2.transactions &&
594594
txRequestRound2.transactions.length === 1 &&
595595
txRequestRound2.transactions[0].signatureShares.length === 4,
@@ -623,7 +623,7 @@ describe('External signer', () => {
623623
);
624624

625625
// signature generation and validation
626-
assert(userMsg4.data.msg4.signatureR === bitgoMsg4.signatureR, 'User and BitGo signaturesR do not match');
626+
assert.ok(userMsg4.data.msg4.signatureR === bitgoMsg4.signatureR, 'User and BitGo signaturesR do not match');
627627

628628
const deserializedBitgoMsg4 = DklsTypes.deserializeMessages({
629629
p2pMessages: [],
@@ -652,8 +652,8 @@ describe('External signer', () => {
652652
derivationPath,
653653
createKeccakHash('keccak256') as Hash
654654
);
655-
assert(convertedSignature, 'Signature is not valid');
656-
assert(convertedSignature.split(':').length === 4, 'Signature is not valid');
655+
assert.ok(convertedSignature, 'Signature is not valid');
656+
assert.ok(convertedSignature.split(':').length === 4, 'Signature is not valid');
657657
readFileStub.restore();
658658
envStub.restore();
659659
});
@@ -710,7 +710,7 @@ async function signBitgoMPCv2Round1(
710710
userShare: SignatureShareRecord,
711711
userGPGPubKey: string
712712
): Promise<TxRequest> {
713-
assert(
713+
assert.ok(
714714
txRequest.transactions && txRequest.transactions.length === 1,
715715
'txRequest.transactions is not an array of length 1'
716716
);
@@ -772,7 +772,7 @@ async function signBitgoMPCv2Round2(
772772
userShare: SignatureShareRecord,
773773
userGPGPubKey: string
774774
): Promise<{ txRequest: TxRequest; bitgoMsg4: DklsTypes.SerializedBroadcastMessage }> {
775-
assert(
775+
assert.ok(
776776
txRequest.transactions && txRequest.transactions.length === 1,
777777
'txRequest.transactions is not an array of length 1'
778778
);

modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
22
import { BitGo } from 'bitgo';
33
import { common } from '@bitgo/sdk-core';
4-
import * as nock from 'nock';
4+
import nock from 'nock';
55
import * as express from 'express';
66
import * as sinon from 'sinon';
77
import * as fs from 'fs';

0 commit comments

Comments
 (0)