Skip to content

Commit f832217

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 f832217

10 files changed

Lines changed: 401 additions & 9 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: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import 'should';
2+
import { startServices, IntegServices } from './helpers/setup';
3+
import { LOCALHOST } from './helpers/servers';
4+
import { SigningMode } from '../../shared/types';
5+
6+
describe('Generate wallet: LOCAL signing', () => {
7+
let services: IntegServices;
8+
9+
before(async () => {
10+
services = await startServices();
11+
});
12+
13+
after(async () => {
14+
await services.teardown();
15+
});
16+
17+
beforeEach(() => {
18+
services.keyProvider.calls.length = 0;
19+
services.bitgo.calls.length = 0;
20+
});
21+
22+
it('generates a tbtc onchain wallet end-to-end', async () => {
23+
const res = await fetch(
24+
`http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/generate`,
25+
{
26+
method: 'POST',
27+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
28+
body: JSON.stringify({
29+
enterprise: 'test-enterprise',
30+
label: 'test-wallet',
31+
multisigType: 'onchain',
32+
}),
33+
},
34+
);
35+
36+
res.status.should.equal(200);
37+
const body = (await res.json()) as any;
38+
body.should.have.property('wallet');
39+
body.wallet.should.have.property('id', 'test-wallet-id');
40+
41+
/** In local mode, AWM stores keys via POST /key — POST /key/generate must NOT be called */
42+
const keyProviderStoreCalls = services.keyProvider.calls.filter((c) => c.path === '/key');
43+
keyProviderStoreCalls.should.have.length(2);
44+
45+
const keyProviderGenerateCalls = services.keyProvider.calls.filter(
46+
(c) => c.path === '/key/generate',
47+
);
48+
keyProviderGenerateCalls.should.have.length(0);
49+
50+
/** BitGo received 3 keychain adds */
51+
const bitgoKeyCalls = services.bitgo.calls.filter(
52+
(c) => c.method === 'POST' && c.path.endsWith('/key'),
53+
);
54+
bitgoKeyCalls.should.have.length(3);
55+
56+
/** and 1 wallet add */
57+
const walletAddCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add'));
58+
walletAddCalls.should.have.length(1);
59+
});
60+
});
61+
62+
describe('Generate wallet: EXTERNAL signing', () => {
63+
let services: IntegServices;
64+
65+
before(async () => {
66+
services = await startServices({ signingMode: SigningMode.EXTERNAL });
67+
});
68+
69+
after(async () => {
70+
await services.teardown();
71+
});
72+
73+
beforeEach(() => {
74+
services.keyProvider.calls.length = 0;
75+
services.bitgo.calls.length = 0;
76+
});
77+
78+
it('generates a tbtc onchain wallet — key provider generates keys (external signing mode)', async () => {
79+
const res = await fetch(
80+
`http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/generate`,
81+
{
82+
method: 'POST',
83+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
84+
body: JSON.stringify({
85+
enterprise: 'test-enterprise',
86+
label: 'test-wallet',
87+
multisigType: 'onchain',
88+
}),
89+
},
90+
);
91+
92+
res.status.should.equal(200);
93+
const body = (await res.json()) as any;
94+
body.should.have.property('wallet');
95+
body.wallet.should.have.property('id', 'test-wallet-id');
96+
97+
/**
98+
* In external mode, AWM delegates key generation to the key provider.
99+
*/
100+
const keyProviderGenerateCalls = services.keyProvider.calls.filter(
101+
(c) => c.path === '/key/generate',
102+
);
103+
keyProviderGenerateCalls.should.have.length(2);
104+
105+
/** POST /key should NOT be called — AWM never generates keys locally in external mode */
106+
const keyProviderStoreCalls = services.keyProvider.calls.filter((c) => c.path === '/key');
107+
keyProviderStoreCalls.should.have.length(0);
108+
109+
/** BitGo receives 3 keychain adds */
110+
const bitgoKeyCalls = services.bitgo.calls.filter(
111+
(c) => c.method === 'POST' && c.path.endsWith('/key'),
112+
);
113+
bitgoKeyCalls.should.have.length(3);
114+
115+
/** and 1 wallet add */
116+
const walletAddCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add'));
117+
walletAddCalls.should.have.length(1);
118+
});
119+
});

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 — coin injected from request so the same fixture works for any coin */
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)