Skip to content

Commit ca19584

Browse files
feat(express): typed-router for pendingApprovals
2 parents 1c89217 + 31504f0 commit ca19584

File tree

5 files changed

+260
-8
lines changed

5 files changed

+260
-8
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ function handleAcceptShare(req: ExpressApiRouteRequest<'express.v1.wallet.accept
239239
* @deprecated
240240
* @param req
241241
*/
242-
function handleApproveTransaction(req: express.Request) {
242+
function handleApproveTransaction(req: ExpressApiRouteRequest<'express.v1.pendingapprovals', 'put'>) {
243243
const params = req.body || {};
244244
return req.bitgo
245245
.pendingApprovals()
@@ -1598,12 +1598,8 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
15981598
app.post('/api/v1/wallet/:id/simpleshare', parseBody, prepareBitGo(config), promiseWrapper(handleShareWallet));
15991599
router.post('express.v1.wallet.acceptShare', [prepareBitGo(config), typedPromiseWrapper(handleAcceptShare)]);
16001600

1601-
app.put(
1602-
'/api/v1/pendingapprovals/:id/express',
1603-
parseBody,
1604-
prepareBitGo(config),
1605-
promiseWrapper(handleApproveTransaction)
1606-
);
1601+
router.put('express.v1.pendingapprovals', [prepareBitGo(config), typedPromiseWrapper(handleApproveTransaction)]);
1602+
16071603
app.put(
16081604
'/api/v1/pendingapprovals/:id/constructTx',
16091605
parseBody,

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { PostDecrypt } from './common/decrypt';
99
import { PostVerifyAddress } from './common/verifyAddress';
1010
import { PostAcceptShare } from './common/acceptShare';
1111
import { PostSimpleCreate } from './v1/simpleCreate';
12-
12+
import { PutPendingApproval } from './v1/pendingApproval';
1313
export const ExpressApi = apiSpec({
1414
'express.ping': {
1515
get: GetPing,
@@ -32,6 +32,9 @@ export const ExpressApi = apiSpec({
3232
'express.v1.wallet.simplecreate': {
3333
post: PostSimpleCreate,
3434
},
35+
'express.v1.pendingapprovals': {
36+
put: PutPendingApproval,
37+
},
3538
});
3639

3740
export type ExpressApi = typeof ExpressApi;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
export const pendingApprovalRequestParams = {
6+
id: t.string,
7+
};
8+
9+
export const pendingApprovalRequestBody = {
10+
walletPassphrase: optional(t.string),
11+
otp: optional(t.string),
12+
tx: optional(t.string),
13+
xprv: optional(t.string),
14+
previewPendingTxs: optional(t.boolean),
15+
pendingApprovalId: optional(t.string),
16+
};
17+
18+
/**
19+
* Pending approval request
20+
*
21+
* @operationId express.v1.pendingapprovals
22+
*/
23+
24+
export const PutPendingApproval = httpRoute({
25+
path: '/api/v1/pendingapprovals/:id/express',
26+
method: 'PUT',
27+
request: httpRequest({
28+
params: pendingApprovalRequestParams,
29+
body: pendingApprovalRequestBody,
30+
}),
31+
response: {
32+
200: t.UnknownRecord,
33+
400: BitgoExpressError,
34+
},
35+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as assert from 'assert';
2+
import * as t from 'io-ts';
3+
4+
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
5+
const result = codec.decode(input);
6+
if (result._tag === 'Left') {
7+
const errors = JSON.stringify(result.left, null, 2);
8+
assert.fail(`Decode failed with errors:\n${errors}`);
9+
}
10+
return result.right;
11+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import * as assert from 'assert';
2+
import * as t from 'io-ts';
3+
import {
4+
pendingApprovalRequestParams,
5+
pendingApprovalRequestBody,
6+
PutPendingApproval,
7+
} from '../../../src/typedRoutes/api/v1/pendingApproval';
8+
import { assertDecode } from './common';
9+
/**
10+
* Helper function to test io-ts codec decoding
11+
*/
12+
13+
describe('PendingApproval codec tests', function () {
14+
describe('pendingApprovalRequestParams', function () {
15+
it('should validate valid params', function () {
16+
const validParams = {
17+
id: '123456789abcdef',
18+
};
19+
20+
const decoded = assertDecode(t.type(pendingApprovalRequestParams), validParams);
21+
assert.strictEqual(decoded.id, validParams.id);
22+
});
23+
24+
it('should reject params with missing id', function () {
25+
const invalidParams = {};
26+
27+
assert.throws(() => {
28+
assertDecode(t.type(pendingApprovalRequestParams), invalidParams);
29+
});
30+
});
31+
32+
it('should reject params with non-string id', function () {
33+
const invalidParams = {
34+
id: 12345, // number instead of string
35+
};
36+
37+
assert.throws(() => {
38+
assertDecode(t.type(pendingApprovalRequestParams), invalidParams);
39+
});
40+
});
41+
});
42+
43+
describe('pendingApprovalRequestBody', function () {
44+
it('should validate body with all fields', function () {
45+
const validBody = {
46+
walletPassphrase: 'mySecurePassword',
47+
otp: '123456',
48+
tx: 'transactionHexString',
49+
xprv: 'xprvString',
50+
previewPendingTxs: true,
51+
pendingApprovalId: 'pendingApproval123',
52+
};
53+
54+
const decoded = assertDecode(t.type(pendingApprovalRequestBody), validBody);
55+
assert.strictEqual(decoded.walletPassphrase, validBody.walletPassphrase);
56+
assert.strictEqual(decoded.otp, validBody.otp);
57+
assert.strictEqual(decoded.tx, validBody.tx);
58+
assert.strictEqual(decoded.xprv, validBody.xprv);
59+
assert.strictEqual(decoded.previewPendingTxs, validBody.previewPendingTxs);
60+
assert.strictEqual(decoded.pendingApprovalId, validBody.pendingApprovalId);
61+
});
62+
63+
it('should validate body with no fields (all optional)', function () {
64+
const validBody = {};
65+
66+
const decoded = assertDecode(t.type(pendingApprovalRequestBody), validBody);
67+
assert.strictEqual(decoded.walletPassphrase, undefined);
68+
assert.strictEqual(decoded.otp, undefined);
69+
assert.strictEqual(decoded.tx, undefined);
70+
assert.strictEqual(decoded.xprv, undefined);
71+
assert.strictEqual(decoded.previewPendingTxs, undefined);
72+
assert.strictEqual(decoded.pendingApprovalId, undefined);
73+
});
74+
75+
it('should validate body with some fields', function () {
76+
const validBody = {
77+
walletPassphrase: 'mySecurePassword',
78+
otp: '123456',
79+
};
80+
81+
const decoded = assertDecode(t.type(pendingApprovalRequestBody), validBody);
82+
assert.strictEqual(decoded.walletPassphrase, validBody.walletPassphrase);
83+
assert.strictEqual(decoded.otp, validBody.otp);
84+
assert.strictEqual(decoded.tx, undefined);
85+
assert.strictEqual(decoded.xprv, undefined);
86+
assert.strictEqual(decoded.previewPendingTxs, undefined);
87+
assert.strictEqual(decoded.pendingApprovalId, undefined);
88+
});
89+
90+
it('should reject body with non-string walletPassphrase', function () {
91+
const invalidBody = {
92+
walletPassphrase: 12345, // number instead of string
93+
};
94+
95+
assert.throws(() => {
96+
assertDecode(t.type(pendingApprovalRequestBody), invalidBody);
97+
});
98+
});
99+
100+
it('should reject body with non-string otp', function () {
101+
const invalidBody = {
102+
otp: 123456, // number instead of string
103+
};
104+
105+
assert.throws(() => {
106+
assertDecode(t.type(pendingApprovalRequestBody), invalidBody);
107+
});
108+
});
109+
110+
it('should reject body with non-string tx', function () {
111+
const invalidBody = {
112+
tx: 12345, // number instead of string
113+
};
114+
115+
assert.throws(() => {
116+
assertDecode(t.type(pendingApprovalRequestBody), invalidBody);
117+
});
118+
});
119+
120+
it('should reject body with non-string xprv', function () {
121+
const invalidBody = {
122+
xprv: 12345, // number instead of string
123+
};
124+
125+
assert.throws(() => {
126+
assertDecode(t.type(pendingApprovalRequestBody), invalidBody);
127+
});
128+
});
129+
130+
it('should reject body with non-boolean previewPendingTxs', function () {
131+
const invalidBody = {
132+
previewPendingTxs: 'true', // string instead of boolean
133+
};
134+
135+
assert.throws(() => {
136+
assertDecode(t.type(pendingApprovalRequestBody), invalidBody);
137+
});
138+
});
139+
140+
it('should reject body with non-string pendingApprovalId', function () {
141+
const invalidBody = {
142+
pendingApprovalId: 12345, // number instead of string
143+
};
144+
145+
assert.throws(() => {
146+
assertDecode(t.type(pendingApprovalRequestBody), invalidBody);
147+
});
148+
});
149+
});
150+
151+
describe('Edge cases', function () {
152+
it('should handle empty strings for string fields', function () {
153+
const body = {
154+
walletPassphrase: '',
155+
otp: '',
156+
tx: '',
157+
xprv: '',
158+
pendingApprovalId: '',
159+
};
160+
161+
const decoded = assertDecode(t.type(pendingApprovalRequestBody), body);
162+
assert.strictEqual(decoded.walletPassphrase, '');
163+
assert.strictEqual(decoded.otp, '');
164+
assert.strictEqual(decoded.tx, '');
165+
assert.strictEqual(decoded.xprv, '');
166+
assert.strictEqual(decoded.pendingApprovalId, '');
167+
});
168+
169+
it('should handle additional unknown properties', function () {
170+
const body = {
171+
walletPassphrase: 'mySecurePassword',
172+
unknownProperty: 'some value',
173+
};
174+
175+
// io-ts with t.exact() strips out additional properties
176+
const decoded = assertDecode(t.exact(t.type(pendingApprovalRequestBody)), body);
177+
assert.strictEqual(decoded.walletPassphrase, 'mySecurePassword');
178+
// @ts-expect-error - unknownProperty doesn't exist on the type
179+
assert.strictEqual(decoded.unknownProperty, undefined);
180+
});
181+
});
182+
183+
describe('PutPendingApproval route definition', function () {
184+
it('should have the correct path', function () {
185+
assert.strictEqual(PutPendingApproval.path, '/api/v1/pendingapprovals/:id/express');
186+
});
187+
188+
it('should have the correct HTTP method', function () {
189+
assert.strictEqual(PutPendingApproval.method, 'PUT');
190+
});
191+
192+
it('should have the correct request configuration', function () {
193+
// Verify the route is configured with a request property
194+
assert.ok(PutPendingApproval.request);
195+
196+
// The request is created using httpRequest which takes params and body
197+
// We can't directly access these properties in the test, but we can verify
198+
// the request exists
199+
});
200+
201+
it('should have the correct response types', function () {
202+
// Check that the response object has the expected status codes
203+
assert.ok(PutPendingApproval.response[200]);
204+
assert.ok(PutPendingApproval.response[400]);
205+
});
206+
});
207+
});

0 commit comments

Comments
 (0)