Skip to content

Commit 81037d7

Browse files
feat(express): typed router implementation for acceptShare
2 parents c437137 + 5bb04dd commit 81037d7

File tree

4 files changed

+210
-8
lines changed

4 files changed

+210
-8
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,9 @@ function handleShareWallet(req: express.Request) {
229229
* @deprecated
230230
* @param req
231231
*/
232-
function handleAcceptShare(req: express.Request) {
232+
function handleAcceptShare(req: ExpressApiRouteRequest<'express.v1.wallet.acceptShare', 'post'>) {
233233
const params = req.body || {};
234-
params.walletShareId = req.params.shareId;
234+
params.walletShareId = req.decoded.shareId;
235235
return req.bitgo.wallets().acceptShare(params);
236236
}
237237

@@ -1598,12 +1598,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
15981598
);
15991599

16001600
app.post('/api/v1/wallet/:id/simpleshare', parseBody, prepareBitGo(config), promiseWrapper(handleShareWallet));
1601-
app.post(
1602-
'/api/v1/walletshare/:shareId/acceptShare',
1603-
parseBody,
1604-
prepareBitGo(config),
1605-
promiseWrapper(handleAcceptShare)
1606-
);
1601+
router.post('express.v1.wallet.acceptShare', [prepareBitGo(config), typedPromiseWrapper(handleAcceptShare)]);
16071602

16081603
app.put(
16091604
'/api/v1/pendingapprovals/:id/express',
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 AcceptShareRequestParams = {
6+
shareId: t.string,
7+
};
8+
9+
export const AcceptShareRequestBody = {
10+
userPassword: optional(t.string),
11+
newWalletPassphrase: optional(t.string),
12+
overrideEncryptedXprv: optional(t.string),
13+
walletShareId: t.string,
14+
};
15+
16+
/**
17+
* Accept a wallet share
18+
*
19+
* @operationId express.v1.wallet.acceptShare
20+
*/
21+
export const PostAcceptShare = httpRoute({
22+
path: '/api/v1/walletshare/:shareId/acceptShare',
23+
method: 'POST',
24+
request: httpRequest({
25+
params: AcceptShareRequestParams,
26+
body: AcceptShareRequestBody,
27+
}),
28+
response: {
29+
200: t.UnknownRecord,
30+
400: BitgoExpressError,
31+
},
32+
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { GetPingExpress } from './common/pingExpress';
77
import { PostLogin } from './common/login';
88
import { PostDecrypt } from './common/decrypt';
99
import { PostVerifyAddress } from './common/verifyAddress';
10+
import { PostAcceptShare } from './common/acceptShare';
1011

1112
export const ExpressApi = apiSpec({
1213
'express.ping': {
@@ -24,6 +25,9 @@ export const ExpressApi = apiSpec({
2425
'express.verifyaddress': {
2526
post: PostVerifyAddress,
2627
},
28+
'express.v1.wallet.acceptShare': {
29+
post: PostAcceptShare,
30+
},
2731
});
2832

2933
export type ExpressApi = typeof ExpressApi;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as assert from 'assert';
2+
import * as t from 'io-ts';
3+
import {
4+
AcceptShareRequestParams,
5+
AcceptShareRequestBody,
6+
PostAcceptShare,
7+
} from '../../../src/typedRoutes/api/common/acceptShare';
8+
9+
/**
10+
* Helper function to test io-ts codec decoding
11+
*/
12+
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
13+
const result = codec.decode(input);
14+
if (result._tag === 'Left') {
15+
const errors = JSON.stringify(result.left, null, 2);
16+
assert.fail(`Decode failed with errors:\n${errors}`);
17+
}
18+
return result.right;
19+
}
20+
21+
describe('AcceptShare codec tests', function () {
22+
describe('AcceptShareRequestParams', function () {
23+
it('should validate valid params', function () {
24+
const validParams = {
25+
shareId: '123456789abcdef',
26+
};
27+
28+
const decoded = assertDecode(t.type(AcceptShareRequestParams), validParams);
29+
assert.strictEqual(decoded.shareId, validParams.shareId);
30+
});
31+
32+
it('should reject params with missing shareId', function () {
33+
const invalidParams = {};
34+
35+
assert.throws(() => {
36+
assertDecode(t.type(AcceptShareRequestParams), invalidParams);
37+
});
38+
});
39+
40+
it('should reject params with non-string shareId', function () {
41+
const invalidParams = {
42+
shareId: 12345, // number instead of string
43+
};
44+
45+
assert.throws(() => {
46+
assertDecode(t.type(AcceptShareRequestParams), invalidParams);
47+
});
48+
});
49+
});
50+
51+
describe('AcceptShareRequestBody', function () {
52+
it('should validate body with all fields', function () {
53+
const validBody = {
54+
userPassword: 'mySecurePassword',
55+
newWalletPassphrase: 'myNewPassphrase',
56+
overrideEncryptedXprv: 'encryptedXprvString',
57+
walletShareId: 'walletShare123',
58+
};
59+
60+
const decoded = assertDecode(t.type(AcceptShareRequestBody), validBody);
61+
assert.strictEqual(decoded.userPassword, validBody.userPassword);
62+
assert.strictEqual(decoded.newWalletPassphrase, validBody.newWalletPassphrase);
63+
assert.strictEqual(decoded.overrideEncryptedXprv, validBody.overrideEncryptedXprv);
64+
assert.strictEqual(decoded.walletShareId, validBody.walletShareId);
65+
});
66+
67+
it('should validate body with only required fields', function () {
68+
const validBody = {
69+
walletShareId: 'walletShare123',
70+
};
71+
72+
const decoded = assertDecode(t.type(AcceptShareRequestBody), validBody);
73+
assert.strictEqual(decoded.walletShareId, validBody.walletShareId);
74+
assert.strictEqual(decoded.userPassword, undefined);
75+
assert.strictEqual(decoded.newWalletPassphrase, undefined);
76+
assert.strictEqual(decoded.overrideEncryptedXprv, undefined);
77+
});
78+
79+
it('should reject body with missing walletShareId', function () {
80+
const invalidBody = {
81+
userPassword: 'mySecurePassword',
82+
newWalletPassphrase: 'myNewPassphrase',
83+
};
84+
85+
assert.throws(() => {
86+
assertDecode(t.type(AcceptShareRequestBody), invalidBody);
87+
});
88+
});
89+
90+
it('should reject body with non-string walletShareId', function () {
91+
const invalidBody = {
92+
walletShareId: 12345, // number instead of string
93+
};
94+
95+
assert.throws(() => {
96+
assertDecode(t.type(AcceptShareRequestBody), invalidBody);
97+
});
98+
});
99+
100+
it('should reject body with non-string optional fields', function () {
101+
const invalidBody = {
102+
userPassword: 12345, // number instead of string
103+
walletShareId: 'walletShare123',
104+
};
105+
106+
assert.throws(() => {
107+
assertDecode(t.type(AcceptShareRequestBody), invalidBody);
108+
});
109+
});
110+
});
111+
112+
describe('Edge cases', function () {
113+
it('should handle empty strings for required fields', function () {
114+
const body = {
115+
walletShareId: '', // empty string
116+
};
117+
118+
// Empty strings are still valid strings
119+
const decoded = assertDecode(t.type(AcceptShareRequestBody), body);
120+
assert.strictEqual(decoded.walletShareId, '');
121+
});
122+
123+
describe('PostAcceptShare route definition', function () {
124+
it('should have the correct path', function () {
125+
assert.strictEqual(PostAcceptShare.path, '/api/v1/walletshare/:shareId/acceptShare');
126+
});
127+
128+
it('should have the correct HTTP method', function () {
129+
assert.strictEqual(PostAcceptShare.method, 'POST');
130+
});
131+
132+
it('should have the correct request configuration', function () {
133+
// Verify the route is configured with a request property
134+
assert.ok(PostAcceptShare.request);
135+
});
136+
137+
it('should have the correct response types', function () {
138+
// Check that the response object has the expected status codes
139+
assert.ok(PostAcceptShare.response[200]);
140+
assert.ok(PostAcceptShare.response[400]);
141+
});
142+
});
143+
144+
it('should handle empty strings for optional fields', function () {
145+
const body = {
146+
userPassword: '',
147+
newWalletPassphrase: '',
148+
overrideEncryptedXprv: '',
149+
walletShareId: 'walletShare123',
150+
};
151+
152+
const decoded = assertDecode(t.type(AcceptShareRequestBody), body);
153+
assert.strictEqual(decoded.userPassword, '');
154+
assert.strictEqual(decoded.newWalletPassphrase, '');
155+
assert.strictEqual(decoded.overrideEncryptedXprv, '');
156+
});
157+
158+
it('should handle additional unknown properties', function () {
159+
const body = {
160+
walletShareId: 'walletShare123',
161+
unknownProperty: 'some value',
162+
};
163+
164+
// io-ts with t.exact() strips out additional properties
165+
const decoded = assertDecode(t.exact(t.type(AcceptShareRequestBody)), body);
166+
assert.strictEqual(decoded.walletShareId, 'walletShare123');
167+
// @ts-expect-error - unknownProperty doesn't exist on the type
168+
assert.strictEqual(decoded.unknownProperty, undefined);
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)