Skip to content

Commit fb979db

Browse files
committed
test: add integration test for generateWallet
This PR adds an integration test for the generateWallet endpoint, which is responsible for generating a new wallet with the provided parameters. The test verifies that the endpoint correctly generates keys using the key provider and creates a wallet on BitGo with the expected properties -- tests cover both local and external key generation modes. Ticket: WAL-1502
1 parent f46a4c6 commit fb979db

11 files changed

Lines changed: 413 additions & 16 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"id": "backup-key-id",
3+
"pub": "xpub_backup",
4+
"keyType": "independent",
5+
"source": "backup",
6+
"type": "independent"
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"id": "bitgo-key-id",
3+
"pub": "xpub_bitgo",
4+
"keyType": "independent",
5+
"source": "bitgo",
6+
"type": "independent",
7+
"isBitGo": true,
8+
"isTrust": false,
9+
"hsmType": "institutional"
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"id": "user-key-id",
3+
"pub": "xpub_user",
4+
"keyType": "independent",
5+
"source": "user",
6+
"type": "independent"
7+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"id": "test-wallet-id",
3+
"coin": "tbtc",
4+
"label": "test-wallet",
5+
"m": 2,
6+
"n": 3,
7+
"keys": ["user-key-id", "backup-key-id", "bitgo-key-id"],
8+
"keySignatures": {},
9+
"type": "advanced",
10+
"multisigType": "onchain",
11+
"isCold": true,
12+
"enterprise": "test-enterprise",
13+
"organization": "test-org",
14+
"bitgoOrg": "BitGo Inc",
15+
"tags": ["test-wallet-id", "test-enterprise"],
16+
"disableTransactionNotifications": false,
17+
"freeze": {},
18+
"deleted": false,
19+
"approvalsRequired": 1,
20+
"coinSpecific": {},
21+
"admin": {},
22+
"allowBackupKeySigning": false,
23+
"clientFlags": [],
24+
"recoverable": false,
25+
"startDate": "2025-01-01T00:00:00.000Z",
26+
"hasLargeNumberOfAddresses": false,
27+
"config": {},
28+
"balanceString": "0",
29+
"confirmedBalanceString": "0",
30+
"spendableBalanceString": "0",
31+
"pendingApprovals": [],
32+
"receiveAddress": {
33+
"id": "addr-id",
34+
"address": "0xexampleaddress",
35+
"chain": 0,
36+
"index": 0,
37+
"coin": "tbtc",
38+
"wallet": "test-wallet-id",
39+
"coinSpecific": {}
40+
},
41+
"users": [{ "user": "user-id", "permissions": ["admin", "spend", "view"] }]
42+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import 'should';
2+
import { startServices, IntegServices } from './helpers/setup';
3+
import { LOCALHOST } from './helpers/servers';
4+
import { SigningMode } from '../../shared/types';
5+
import type { GenerateWalletResponseBody } from '../../masterBitgoExpress/routers/generateWalletRoute';
6+
7+
describe('Generate wallet: LOCAL signing', () => {
8+
let services: IntegServices;
9+
10+
before(async () => {
11+
services = await startServices();
12+
});
13+
14+
after(async () => {
15+
await services.teardown();
16+
});
17+
18+
beforeEach(() => {
19+
services.keyProvider.calls.length = 0;
20+
services.bitgo.calls.length = 0;
21+
});
22+
23+
it('generates a tbtc onchain wallet end-to-end', async () => {
24+
const res = await fetch(
25+
`http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/generate`,
26+
{
27+
method: 'POST',
28+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
29+
body: JSON.stringify({
30+
enterprise: 'test-enterprise',
31+
label: 'test-wallet',
32+
multisigType: 'onchain',
33+
}),
34+
},
35+
);
36+
37+
res.status.should.equal(200);
38+
const body = (await res.json()) as GenerateWalletResponseBody;
39+
body.should.have.property('wallet');
40+
body.wallet.should.have.property('id', 'test-wallet-id');
41+
42+
/** In local mode, AWM stores keys via POST /key — POST /key/generate must NOT be called */
43+
const keyProviderStoreCalls = services.keyProvider.calls.filter((c) => c.path === '/key');
44+
keyProviderStoreCalls.should.have.length(2);
45+
46+
const keyProviderGenerateCalls = services.keyProvider.calls.filter(
47+
(c) => c.path === '/key/generate',
48+
);
49+
keyProviderGenerateCalls.should.have.length(0);
50+
51+
/** BitGo received 3 keychain adds */
52+
const bitgoKeyCalls = services.bitgo.calls.filter(
53+
(c) => c.method === 'POST' && c.path.endsWith('/key'),
54+
);
55+
bitgoKeyCalls.should.have.length(3);
56+
57+
/** and 1 wallet add */
58+
const walletAddCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add'));
59+
walletAddCalls.should.have.length(1);
60+
});
61+
});
62+
63+
describe('Generate wallet: EXTERNAL signing', () => {
64+
let services: IntegServices;
65+
66+
before(async () => {
67+
services = await startServices({ signingMode: SigningMode.EXTERNAL });
68+
});
69+
70+
after(async () => {
71+
await services.teardown();
72+
});
73+
74+
beforeEach(() => {
75+
services.keyProvider.calls.length = 0;
76+
services.bitgo.calls.length = 0;
77+
});
78+
79+
it('generates a tbtc onchain wallet — key provider generates keys (external signing mode)', async () => {
80+
const res = await fetch(
81+
`http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/generate`,
82+
{
83+
method: 'POST',
84+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
85+
body: JSON.stringify({
86+
enterprise: 'test-enterprise',
87+
label: 'test-wallet',
88+
multisigType: 'onchain',
89+
}),
90+
},
91+
);
92+
93+
res.status.should.equal(200);
94+
const body = (await res.json()) as GenerateWalletResponseBody;
95+
body.should.have.property('wallet');
96+
body.wallet.should.have.property('id', 'test-wallet-id');
97+
98+
/**
99+
* In external mode, AWM delegates key generation to the key provider.
100+
*/
101+
const keyProviderGenerateCalls = services.keyProvider.calls.filter(
102+
(c) => c.path === '/key/generate',
103+
);
104+
keyProviderGenerateCalls.should.have.length(2);
105+
106+
/** POST /key should NOT be called — AWM never generates keys locally in external mode */
107+
const keyProviderStoreCalls = services.keyProvider.calls.filter((c) => c.path === '/key');
108+
keyProviderStoreCalls.should.have.length(0);
109+
110+
/** BitGo receives 3 keychain adds */
111+
const bitgoKeyCalls = services.bitgo.calls.filter(
112+
(c) => c.method === 'POST' && c.path.endsWith('/key'),
113+
);
114+
bitgoKeyCalls.should.have.length(3);
115+
116+
/** and 1 wallet add */
117+
const walletAddCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add'));
118+
walletAddCalls.should.have.length(1);
119+
});
120+
});

src/__tests__/integration/health.integ.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import * as http from 'http';
33
import { app as awmApp } from '../../advancedWalletManagerApp';
44
import { app as mbeApp } from '../../masterBitGoExpressApp';
55
import { AppMode, TlsMode, SigningMode } from '../../shared/types';
6-
import { listen, close } from './helpers/servers';
6+
import { listen, close, LOCALHOST } from './helpers/servers';
77

8-
describe('integration — health checks', () => {
8+
describe('Integration Test — health checks', () => {
99
let awmServer: http.Server;
1010
let mbeServer: http.Server;
1111
let awmPort: number;
@@ -18,10 +18,10 @@ describe('integration — health checks', () => {
1818
tlsMode: TlsMode.DISABLED,
1919
signingMode: SigningMode.LOCAL,
2020
port: 0,
21-
bind: '127.0.0.1',
21+
bind: LOCALHOST,
2222
timeout: 30000,
2323
httpLoggerFile: '',
24-
keyProviderUrl: 'http://127.0.0.1:3082',
24+
keyProviderUrl: `http://${LOCALHOST}:3082`,
2525
}),
2626
);
2727
awmPort = await listen(awmServer);
@@ -31,12 +31,12 @@ describe('integration — health checks', () => {
3131
appMode: AppMode.MASTER_EXPRESS,
3232
tlsMode: TlsMode.DISABLED,
3333
port: 0,
34-
bind: '127.0.0.1',
34+
bind: LOCALHOST,
3535
timeout: 30000,
3636
httpLoggerFile: '',
3737
env: 'test',
3838
disableEnvCheck: true,
39-
advancedWalletManagerUrl: `http://127.0.0.1:${awmPort}`,
39+
advancedWalletManagerUrl: `http://${LOCALHOST}:${awmPort}`,
4040
awmServerCertAllowSelfSigned: true,
4141
}),
4242
);
@@ -49,12 +49,14 @@ describe('integration — health checks', () => {
4949
});
5050

5151
it('AWM /ping returns 200', async () => {
52-
const res = await fetch(`http://127.0.0.1:${awmPort}/ping`, { method: 'POST' });
52+
const res = await fetch(`http://${LOCALHOST}:${awmPort}/ping`, { method: 'POST' });
5353
res.status.should.equal(200);
5454
});
5555

5656
it('MBE /advancedwallet/ping returns 200', async () => {
57-
const res = await fetch(`http://127.0.0.1:${mbePort}/advancedwallet/ping`, { method: 'POST' });
57+
const res = await fetch(`http://${LOCALHOST}:${mbePort}/advancedwallet/ping`, {
58+
method: 'POST',
59+
});
5860
res.status.should.equal(200);
5961
});
6062
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as http from 'http';
2+
import * as path from 'path';
3+
import express from 'express';
4+
import { listen, close } from './servers';
5+
6+
export interface MockBitgoCall {
7+
method: string;
8+
path: string;
9+
body: unknown;
10+
}
11+
12+
export interface MockBitgoServer {
13+
port: number;
14+
calls: MockBitgoCall[];
15+
close(): Promise<void>;
16+
}
17+
18+
const FIXTURES_DIR = path.resolve(__dirname, '../fixtures/bitgo');
19+
20+
function loadFixture(name: string): unknown {
21+
return require(`${FIXTURES_DIR}/${name}.json`);
22+
}
23+
24+
export async function startMockBitgoServer(): Promise<MockBitgoServer> {
25+
const calls: MockBitgoCall[] = [];
26+
27+
const app = express();
28+
app.use(express.json());
29+
30+
app.use((req, _res, next) => {
31+
calls.push({ method: req.method, path: req.path, body: req.body });
32+
next();
33+
});
34+
35+
/** SDK calls this on every BitGo instance initialisation */
36+
app.get('/api/v1/client/constants', (_req, res) => {
37+
res.json({ ttl: 3600, constants: {} });
38+
});
39+
40+
/** Add keychain — source distinguishes user / backup / bitgo */
41+
app.post('/api/v2/:coin/key', (req, res) => {
42+
const { coin } = req.params;
43+
const source = req.body?.source;
44+
const fixtureName =
45+
source === 'user' ? 'addKey.user' : source === 'backup' ? 'addKey.backup' : 'addKey.bitgo';
46+
const fixture = loadFixture(fixtureName) as Record<string, unknown>;
47+
return res.json({ ...fixture, coin });
48+
});
49+
50+
/** Create wallet */
51+
app.post('/api/v2/:coin/wallet/add', (req, res) => {
52+
const { coin } = req.params;
53+
const fixture = loadFixture('createWallet') as Record<string, unknown>;
54+
res.json({ ...fixture, coin });
55+
});
56+
57+
const server = http.createServer(app);
58+
const port = await listen(server);
59+
60+
return { port, calls, close: () => close(server) };
61+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as http from 'http';
2+
import express from 'express';
3+
import { BitGoAPI } from '@bitgo-beta/sdk-api';
4+
import { Hteth } from '@bitgo-beta/sdk-coin-eth';
5+
import { Tbtc } from '@bitgo-beta/sdk-coin-btc';
6+
import { listen, close } from './servers';
7+
8+
export interface MockKeyProviderCall {
9+
method: string;
10+
path: string;
11+
body: unknown;
12+
}
13+
14+
export interface MockKeyProviderServer {
15+
port: number;
16+
calls: MockKeyProviderCall[];
17+
close(): Promise<void>;
18+
}
19+
20+
interface StoredKey {
21+
prv: string;
22+
source: string;
23+
type: string;
24+
}
25+
26+
/**
27+
* @returns BitGo Instance with coins registered
28+
*/
29+
function createBitgoInstance(): BitGoAPI {
30+
const instance = new BitGoAPI({ env: 'test' });
31+
instance.register('hteth', Hteth.createInstance);
32+
instance.register('tbtc', Tbtc.createInstance);
33+
return instance;
34+
}
35+
36+
function generateKeypair(coin: string): { pub: string; prv: string } {
37+
const keychain = createBitgoInstance().coin(coin).keychains().create();
38+
if (!keychain.pub || !keychain.prv)
39+
throw new Error(`Failed to generate keypair for coin: ${coin}`);
40+
return { pub: keychain.pub, prv: keychain.prv };
41+
}
42+
43+
export async function startMockKeyProviderServer(): Promise<MockKeyProviderServer> {
44+
const calls: MockKeyProviderCall[] = [];
45+
const keyStore = new Map<string, StoredKey>();
46+
47+
const app = express();
48+
app.use(express.json());
49+
50+
app.use((req, _res, next) => {
51+
calls.push({ method: req.method, path: req.path, body: req.body });
52+
next();
53+
});
54+
55+
/** External signing mode — key provider generates the key */
56+
app.post('/key/generate', (req, res) => {
57+
const { coin, source, type } = req.body;
58+
const { pub, prv } = generateKeypair(coin);
59+
keyStore.set(pub, { prv, source, type });
60+
res.json({ pub, coin, source, type });
61+
});
62+
63+
/** Local signing mode — AWM generates the key and sends it here for storage */
64+
app.post('/key', (req, res) => {
65+
const { pub, prv, coin, source, type } = req.body;
66+
keyStore.set(pub, { prv, source, type });
67+
res.json({ pub, coin, source, type });
68+
});
69+
70+
const server = http.createServer(app);
71+
const port = await listen(server);
72+
73+
return { port, calls, close: () => close(server) };
74+
}

src/__tests__/integration/helpers/servers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as http from 'http';
22
import * as net from 'net';
33

4+
export const LOCALHOST = '127.0.0.1';
5+
46
export function listen(server: http.Server): Promise<number> {
57
return new Promise((resolve, reject) => {
68
server.once('error', reject);
7-
server.listen(0, '127.0.0.1', () => {
9+
server.listen(0, LOCALHOST, () => {
810
resolve((server.address() as net.AddressInfo).port);
911
});
1012
});

0 commit comments

Comments
 (0)