Skip to content

Commit 0a2ef3c

Browse files
authored
test(express): added error and edge case tests
2 parents 642e500 + c079bae commit 0a2ef3c

File tree

1 file changed

+360
-0
lines changed

1 file changed

+360
-0
lines changed

modules/express/test/unit/typedRoutes/coinSignTx.ts

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)