1+ import 'should' ;
2+ import sinon from 'sinon' ;
3+ import * as request from 'supertest' ;
4+ import nock from 'nock' ;
5+ import { app as expressApp } from '../../../masterExpressApp' ;
6+ import { AppMode , MasterExpressConfig , TlsMode } from '../../../shared/types' ;
7+ import { Environments , Wallet } from '@bitgo/sdk-core' ;
8+
9+ describe ( 'POST /api/:coin/wallet/:walletId/consolidateunspents' , ( ) => {
10+ let agent : request . SuperAgentTest ;
11+ const coin = 'btc' ;
12+ const walletId = 'test-wallet-id' ;
13+ const accessToken = 'test-access-token' ;
14+ const bitgoApiUrl = Environments . test . uri ;
15+ const enclavedExpressUrl = 'https://test-enclaved-express.com' ;
16+
17+ before ( ( ) => {
18+ nock . disableNetConnect ( ) ;
19+ nock . enableNetConnect ( '127.0.0.1' ) ;
20+
21+ const config : MasterExpressConfig = {
22+ appMode : AppMode . MASTER_EXPRESS ,
23+ port : 0 ,
24+ bind : 'localhost' ,
25+ timeout : 30000 ,
26+ logFile : '' ,
27+ env : 'test' ,
28+ disableEnvCheck : true ,
29+ authVersion : 2 ,
30+ enclavedExpressUrl : enclavedExpressUrl ,
31+ enclavedExpressCert : 'test-cert' ,
32+ tlsMode : TlsMode . DISABLED ,
33+ mtlsRequestCert : false ,
34+ allowSelfSigned : true ,
35+ } ;
36+
37+ const app = expressApp ( config ) ;
38+ agent = request . agent ( app ) ;
39+ } ) ;
40+
41+ afterEach ( ( ) => {
42+ nock . cleanAll ( ) ;
43+ sinon . restore ( ) ;
44+ } ) ;
45+
46+ it ( 'should return transfer, txid, tx, and status on success' , async ( ) => {
47+ const walletGetNock = nock ( bitgoApiUrl )
48+ . get ( `/api/v2/${ coin } /wallet/${ walletId } ` )
49+ . matchHeader ( 'any' , ( ) => true )
50+ . reply ( 200 , {
51+ id : walletId ,
52+ type : 'cold' ,
53+ subType : 'onPrem' ,
54+ keys : [ 'user-key-id' , 'backup-key-id' , 'bitgo-key-id' ] ,
55+ } ) ;
56+
57+ const keychainGetNock = nock ( bitgoApiUrl )
58+ . get ( `/api/v2/${ coin } /key/user-key-id` )
59+ . matchHeader ( 'any' , ( ) => true )
60+ . reply ( 200 , {
61+ id : 'user-key-id' ,
62+ pub : 'xpub_user' ,
63+ } ) ;
64+
65+ const mockResult = {
66+ transfer : {
67+ entries : [
68+ { address : 'tb1qu...' , value : - 4000 } ,
69+ { address : 'tb1qle...' , value : - 4000 } ,
70+ { address : 'tb1qtw...' , value : 2714 , isChange : true } ,
71+ ] ,
72+ id : '685ac2f3c2f8a2a5d9cc18d3593f1751' ,
73+ coin : 'tbtc' ,
74+ wallet : '685abbf19ca95b79f88e0b41d9337109' ,
75+ txid : '239d143cdfc6d6c83a935da4f3d610b2364a956c7b6dcdc165eb706f62c4432a' ,
76+ status : 'signed' ,
77+ } ,
78+ txid : '239d143cdfc6d6c83a935da4f3d610b2364a956c7b6dcdc165eb706f62c4432a' ,
79+ tx : '01000000000102580b...' ,
80+ status : 'signed' ,
81+ } ;
82+
83+ const consolidateUnspentsStub = sinon
84+ . stub ( Wallet . prototype , 'consolidateUnspents' )
85+ . resolves ( mockResult ) ;
86+
87+ const response = await agent
88+ . post ( `/api/${ coin } /wallet/${ walletId } /consolidateunspents` )
89+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
90+ . send ( {
91+ source : 'user' ,
92+ pubkey : 'xpub_user' ,
93+ feeRate : 1000 ,
94+ } ) ;
95+
96+ response . status . should . equal ( 200 ) ;
97+ response . body . should . have . property ( 'transfer' ) ;
98+ response . body . should . have . property ( 'txid' , mockResult . txid ) ;
99+ response . body . should . have . property ( 'tx' , mockResult . tx ) ;
100+ response . body . should . have . property ( 'status' , mockResult . status ) ;
101+ response . body . transfer . should . have . property ( 'txid' , mockResult . transfer . txid ) ;
102+ response . body . transfer . should . have . property ( 'status' , mockResult . transfer . status ) ;
103+ response . body . transfer . should . have . property ( 'entries' ) . which . is . Array ( ) ;
104+
105+ walletGetNock . done ( ) ;
106+ keychainGetNock . done ( ) ;
107+ sinon . assert . calledOnce ( consolidateUnspentsStub ) ;
108+ } ) ;
109+
110+ it ( 'should return error, name, and details on failure' , async ( ) => {
111+ const walletGetNock = nock ( bitgoApiUrl )
112+ . get ( `/api/v2/${ coin } /wallet/${ walletId } ` )
113+ . matchHeader ( 'any' , ( ) => true )
114+ . reply ( 200 , {
115+ id : walletId ,
116+ type : 'cold' ,
117+ subType : 'onPrem' ,
118+ keys : [ 'user-key-id' , 'backup-key-id' , 'bitgo-key-id' ] ,
119+ } ) ;
120+
121+ const keychainGetNock = nock ( bitgoApiUrl )
122+ . get ( `/api/v2/${ coin } /key/user-key-id` )
123+ . matchHeader ( 'any' , ( ) => true )
124+ . reply ( 200 , {
125+ id : 'user-key-id' ,
126+ pub : 'xpub_user' ,
127+ } ) ;
128+
129+ const mockError = {
130+ error : 'Internal Server Error' ,
131+ name : 'ApiResponseError' ,
132+ details : 'There are too few unspents that meet the given parameters to consolidate (1 available).' ,
133+ } ;
134+
135+ const consolidateUnspentsStub = sinon
136+ . stub ( Wallet . prototype , 'consolidateUnspents' )
137+ . throws ( Object . assign ( new Error ( mockError . details ) , mockError ) ) ;
138+
139+ const response = await agent
140+ . post ( `/api/${ coin } /wallet/${ walletId } /consolidateunspents` )
141+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
142+ . send ( {
143+ source : 'user' ,
144+ pubkey : 'xpub_user' ,
145+ feeRate : 1000 ,
146+ } ) ;
147+
148+ response . status . should . equal ( 500 ) ;
149+ response . body . should . have . property ( 'error' , mockError . error ) ;
150+ response . body . should . have . property ( 'name' , mockError . name ) ;
151+ response . body . should . have . property ( 'details' , mockError . details ) ;
152+
153+ walletGetNock . done ( ) ;
154+ keychainGetNock . done ( ) ;
155+ sinon . assert . calledOnce ( consolidateUnspentsStub ) ;
156+ } ) ;
157+
158+ it ( 'should throw error when provided pubkey does not match wallet keychain' , async ( ) => {
159+ const walletGetNock = nock ( bitgoApiUrl )
160+ . get ( `/api/v2/${ coin } /wallet/${ walletId } ` )
161+ . matchHeader ( 'any' , ( ) => true )
162+ . reply ( 200 , {
163+ id : walletId ,
164+ type : 'cold' ,
165+ subType : 'onPrem' ,
166+ keys : [ 'user-key-id' , 'backup-key-id' , 'bitgo-key-id' ] ,
167+ } ) ;
168+
169+ const keychainGetNock = nock ( bitgoApiUrl )
170+ . get ( `/api/v2/${ coin } /key/user-key-id` )
171+ . matchHeader ( 'any' , ( ) => true )
172+ . reply ( 200 , {
173+ id : 'user-key-id' ,
174+ pub : 'xpub_user' ,
175+ } ) ;
176+
177+ const response = await agent
178+ . post ( `/api/${ coin } /wallet/${ walletId } /consolidateunspents` )
179+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
180+ . send ( {
181+ source : 'user' ,
182+ pubkey : 'wrong_pubkey' ,
183+ feeRate : 1000 ,
184+ } ) ;
185+
186+ response . status . should . equal ( 500 ) ;
187+
188+ walletGetNock . done ( ) ;
189+ keychainGetNock . done ( ) ;
190+ } ) ;
191+ } ) ;
0 commit comments