@@ -277,6 +277,366 @@ describe('CoinSignTx codec tests', function () {
277277 assert . strictEqual ( coinStub . calledOnceWith ( coin ) , true ) ;
278278 assert . strictEqual ( mockCoin . signTransaction . calledOnce , true ) ;
279279 } ) ;
280+
281+ describe ( 'Error Cases' , function ( ) {
282+ it ( 'should handle invalid coin error' , async function ( ) {
283+ const invalidCoin = 'invalid_coin_xyz' ;
284+ const requestBody = {
285+ txPrebuild : {
286+ txHex :
287+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
288+ } ,
289+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
290+ } ;
291+
292+ // Stub coin() to throw error for invalid coin
293+ sinon . stub ( BitGo . prototype , 'coin' ) . throws ( new Error ( `Coin ${ invalidCoin } is not supported` ) ) ;
294+
295+ // Make the request to Express
296+ const result = await agent
297+ . post ( `/api/v2/${ invalidCoin } /signtx` )
298+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
299+ . set ( 'Content-Type' , 'application/json' )
300+ . send ( requestBody ) ;
301+
302+ // Verify error response
303+ assert . strictEqual ( result . status , 500 ) ;
304+ result . body . should . have . property ( 'error' ) ;
305+ } ) ;
306+
307+ it ( 'should handle signTransaction failure' , async function ( ) {
308+ const requestBody = {
309+ txPrebuild : {
310+ txHex :
311+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
312+ } ,
313+ prv : 'invalid_private_key' ,
314+ } ;
315+
316+ // Create mock coin where signTransaction fails
317+ const mockCoin = {
318+ signTransaction : sinon . stub ( ) . rejects ( new Error ( 'Invalid private key' ) ) ,
319+ } ;
320+
321+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
322+
323+ // Make the request to Express
324+ const result = await agent
325+ . post ( `/api/v2/${ coin } /signtx` )
326+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
327+ . set ( 'Content-Type' , 'application/json' )
328+ . send ( requestBody ) ;
329+
330+ // Verify error response
331+ assert . strictEqual ( result . status , 500 ) ;
332+ result . body . should . have . property ( 'error' ) ;
333+ } ) ;
334+
335+ it ( 'should handle missing transaction data error' , async function ( ) {
336+ const requestBody = {
337+ txPrebuild : { } ,
338+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
339+ } ;
340+
341+ // Create mock coin where signTransaction fails due to missing data
342+ const mockCoin = {
343+ signTransaction : sinon . stub ( ) . rejects ( new Error ( 'Missing transaction data' ) ) ,
344+ } ;
345+
346+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
347+
348+ // Make the request to Express
349+ const result = await agent
350+ . post ( `/api/v2/${ coin } /signtx` )
351+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
352+ . set ( 'Content-Type' , 'application/json' )
353+ . send ( requestBody ) ;
354+
355+ // Verify error response
356+ assert . strictEqual ( result . status , 500 ) ;
357+ result . body . should . have . property ( 'error' ) ;
358+ } ) ;
359+ } ) ;
360+
361+ describe ( 'Invalid Request Body' , function ( ) {
362+ it ( 'should reject request with empty body' , async function ( ) {
363+ // Make the request with empty body
364+ const result = await agent
365+ . post ( `/api/v2/${ coin } /signtx` )
366+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
367+ . set ( 'Content-Type' , 'application/json' )
368+ . send ( { } ) ;
369+
370+ // io-ts validation should fail or SDK should reject
371+ // Note: Depending on route config, this might be 400 or 500
372+ assert . ok ( result . status >= 400 ) ;
373+ } ) ;
374+
375+ it ( 'should reject request with invalid txPrebuild type' , async function ( ) {
376+ const requestBody = {
377+ txPrebuild : 'invalid_string_instead_of_object' , // Wrong type!
378+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
379+ } ;
380+
381+ // Make the request
382+ const result = await agent
383+ . post ( `/api/v2/${ coin } /signtx` )
384+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
385+ . set ( 'Content-Type' , 'application/json' )
386+ . send ( requestBody ) ;
387+
388+ // Should fail validation
389+ assert . ok ( result . status >= 400 ) ;
390+ } ) ;
391+
392+ it ( 'should reject request with invalid field types' , async function ( ) {
393+ const requestBody = {
394+ txPrebuild : {
395+ txHex :
396+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
397+ } ,
398+ prv : 12345 , // Number instead of string!
399+ isLastSignature : 'true' , // String instead of boolean!
400+ } ;
401+
402+ // Make the request
403+ const result = await agent
404+ . post ( `/api/v2/${ coin } /signtx` )
405+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
406+ . set ( 'Content-Type' , 'application/json' )
407+ . send ( requestBody ) ;
408+
409+ // Should fail validation
410+ assert . ok ( result . status >= 400 ) ;
411+ } ) ;
412+
413+ it ( 'should handle request with malformed JSON' , async function ( ) {
414+ // Make the request with malformed JSON
415+ const result = await agent
416+ . post ( `/api/v2/${ coin } /signtx` )
417+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
418+ . set ( 'Content-Type' , 'application/json' )
419+ . send ( '{ invalid json ]' ) ;
420+
421+ // Should fail parsing
422+ assert . ok ( result . status >= 400 ) ;
423+ } ) ;
424+ } ) ;
425+
426+ describe ( 'Edge Cases' , function ( ) {
427+ it ( 'should handle empty txPrebuild object' , async function ( ) {
428+ const requestBody = {
429+ txPrebuild : { } , // Empty object
430+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
431+ } ;
432+
433+ const mockCoin = {
434+ signTransaction : sinon . stub ( ) . rejects ( new Error ( 'Missing transaction data' ) ) ,
435+ } ;
436+
437+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
438+
439+ const result = await agent
440+ . post ( `/api/v2/${ coin } /signtx` )
441+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
442+ . set ( 'Content-Type' , 'application/json' )
443+ . send ( requestBody ) ;
444+
445+ // Should handle empty txPrebuild gracefully
446+ assert . ok ( result . status >= 400 ) ;
447+ } ) ;
448+
449+ it ( 'should handle very long private key' , async function ( ) {
450+ const requestBody = {
451+ txPrebuild : {
452+ txHex :
453+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
454+ } ,
455+ prv : 'x' . repeat ( 10000 ) , // Extremely long private key
456+ } ;
457+
458+ const mockCoin = {
459+ signTransaction : sinon . stub ( ) . rejects ( new Error ( 'Invalid private key format' ) ) ,
460+ } ;
461+
462+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
463+
464+ const result = await agent
465+ . post ( `/api/v2/${ coin } /signtx` )
466+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
467+ . set ( 'Content-Type' , 'application/json' )
468+ . send ( requestBody ) ;
469+
470+ // Should handle gracefully
471+ assert . ok ( result . status >= 400 ) ;
472+ } ) ;
473+
474+ it ( 'should handle missing prv for certain transaction types' , async function ( ) {
475+ const requestBody = {
476+ txPrebuild : {
477+ txHex :
478+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
479+ } ,
480+ // Missing prv - some transaction types might not require it
481+ } ;
482+
483+ const mockCoin = {
484+ signTransaction : sinon . stub ( ) . rejects ( new Error ( 'Private key required for signing' ) ) ,
485+ } ;
486+
487+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
488+
489+ const result = await agent
490+ . post ( `/api/v2/${ coin } /signtx` )
491+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
492+ . set ( 'Content-Type' , 'application/json' )
493+ . send ( requestBody ) ;
494+
495+ // Should fail if prv is required
496+ assert . ok ( result . status >= 400 ) ;
497+ } ) ;
498+
499+ it ( 'should handle coin parameter with special characters' , async function ( ) {
500+ const specialCoin = '../../../etc/passwd' ;
501+ const requestBody = {
502+ txPrebuild : {
503+ txHex :
504+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
505+ } ,
506+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
507+ } ;
508+
509+ sinon . stub ( BitGo . prototype , 'coin' ) . throws ( new Error ( 'Invalid coin identifier' ) ) ;
510+
511+ const result = await agent
512+ . post ( `/api/v2/${ encodeURIComponent ( specialCoin ) } /signtx` )
513+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
514+ . set ( 'Content-Type' , 'application/json' )
515+ . send ( requestBody ) ;
516+
517+ // Should handle special characters safely
518+ assert . ok ( result . status >= 400 ) ;
519+ } ) ;
520+
521+ it ( 'should handle request with both txHex and txBase64' , async function ( ) {
522+ const requestBody = {
523+ txPrebuild : {
524+ txHex :
525+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
526+ txBase64 :
527+ 'AQAAAAFz2JT3Xvjk8jKcYcMrKR8tPMRm5+/Q6J2sMgtz7QDpAAAAAAD+////AoCWmAAAAAAAGXapFJA29QPQaHHwR3Uriuhw2A6tHkPgiKwAAAAAAAEBH9cQ2QAAAAAAAXapFCf/zr8zPrMftHGIRsOt0Cf+wdOyiKwA' ,
528+ } ,
529+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
530+ } ;
531+
532+ const mockCoin = {
533+ signTransaction : sinon . stub ( ) . resolves ( mockFullySignedResponse ) ,
534+ } ;
535+
536+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
537+
538+ const result = await agent
539+ . post ( `/api/v2/${ coin } /signtx` )
540+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
541+ . set ( 'Content-Type' , 'application/json' )
542+ . send ( requestBody ) ;
543+
544+ // Should handle gracefully (accept or reject consistently)
545+ assert . ok ( result . status === 200 || result . status >= 400 ) ;
546+ } ) ;
547+
548+ it ( 'should handle request with invalid signingStep value' , async function ( ) {
549+ const requestBody = {
550+ txPrebuild : {
551+ txHex :
552+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
553+ } ,
554+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
555+ signingStep : 'invalidStep' , // Not one of: signerNonce, signerSignature, cosignerNonce
556+ } ;
557+
558+ const result = await agent
559+ . post ( `/api/v2/${ coin } /signtx` )
560+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
561+ . set ( 'Content-Type' , 'application/json' )
562+ . send ( requestBody ) ;
563+
564+ // Should fail validation
565+ assert . ok ( result . status >= 400 ) ;
566+ } ) ;
567+ } ) ;
568+
569+ describe ( 'Response Validation Edge Cases' , function ( ) {
570+ it ( 'should reject response with missing required field in FullySignedTransactionResponse' , async function ( ) {
571+ const requestBody = {
572+ txPrebuild : {
573+ txHex :
574+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
575+ } ,
576+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
577+ } ;
578+
579+ // Mock returns invalid response (missing txHex)
580+ const invalidResponse = { } ;
581+
582+ const mockCoin = {
583+ signTransaction : sinon . stub ( ) . resolves ( invalidResponse ) ,
584+ } ;
585+
586+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
587+
588+ const result = await agent
589+ . post ( `/api/v2/${ coin } /signtx` )
590+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
591+ . set ( 'Content-Type' , 'application/json' )
592+ . send ( requestBody ) ;
593+
594+ // Even if SDK returns 200, response should fail codec validation
595+ // This depends on where validation happens
596+ assert . ok ( result . status === 200 || result . status >= 400 ) ;
597+
598+ // If status is 200 but response is invalid, codec validation should catch it
599+ if ( result . status === 200 ) {
600+ assert . throws ( ( ) => {
601+ assertDecode ( FullySignedTransactionResponse , result . body ) ;
602+ } ) ;
603+ }
604+ } ) ;
605+
606+ it ( 'should reject response with wrong type in txHex field' , async function ( ) {
607+ const requestBody = {
608+ txPrebuild : {
609+ txHex :
610+ '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000' ,
611+ } ,
612+ prv : 'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2' ,
613+ } ;
614+
615+ // Mock returns invalid response (txHex is number instead of string)
616+ const invalidResponse = {
617+ txHex : 12345 , // Wrong type!
618+ } ;
619+
620+ const mockCoin = {
621+ signTransaction : sinon . stub ( ) . resolves ( invalidResponse ) ,
622+ } ;
623+
624+ sinon . stub ( BitGo . prototype , 'coin' ) . returns ( mockCoin as any ) ;
625+
626+ const result = await agent
627+ . post ( `/api/v2/${ coin } /signtx` )
628+ . set ( 'Authorization' , 'Bearer test_access_token_12345' )
629+ . set ( 'Content-Type' , 'application/json' )
630+ . send ( requestBody ) ;
631+
632+ // Response codec validation should catch type mismatch
633+ if ( result . status === 200 ) {
634+ assert . throws ( ( ) => {
635+ assertDecode ( FullySignedTransactionResponse , result . body ) ;
636+ } ) ;
637+ }
638+ } ) ;
639+ } ) ;
280640 } ) ;
281641
282642 describe ( 'CoinSignTxParams' , function ( ) {
0 commit comments