Skip to content

Commit caa3f44

Browse files
committed
test(payments): add comprehensive test coverage for all payment paths
120 new tests across 7 files (6 new + 1 extended): - parsePaymentOutputs (23): output key mapping, missing fields, multi-manager - PaymentManagerPrimitive (20): add/remove/cascade/getRemovable/previewRemove - PaymentConnectorPrimitive (18): add/remove/composite-key/previewRemove - validate action.ts (9): all payment error paths in handleValidate - payment-env (7): dev-mode env var injection + AUTH_MODE - pre-deploy-payments (15): credential provider create/update/cleanup - wirePaymentCapability (17): template/BYO patching, idempotency Total suite: 4036 tests passing.
1 parent f863517 commit caa3f44

7 files changed

Lines changed: 2718 additions & 0 deletions

File tree

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import { parsePaymentOutputs } from '../outputs.js';
2+
import type { StackOutputs } from '../outputs.js';
3+
import { describe, expect, it } from 'vitest';
4+
5+
// ---------------------------------------------------------------------------
6+
// Helpers
7+
// ---------------------------------------------------------------------------
8+
9+
function makeOutputs(name: string, overrides: Record<string, string> = {}): StackOutputs {
10+
return {
11+
[`Payment${name}ManagerArn`]: `arn:aws:bedrock:us-east-1:123456789012:payment-manager/${name}`,
12+
[`Payment${name}ManagerId`]: `pm-${name.toLowerCase()}-001`,
13+
[`Payment${name}ProcessPaymentRoleArn`]: `arn:aws:iam::123456789012:role/${name}ProcessPaymentRole`,
14+
[`Payment${name}ResourceRetrievalRoleArn`]: `arn:aws:iam::123456789012:role/${name}ResourceRetrievalRole`,
15+
...overrides,
16+
};
17+
}
18+
19+
const COINBASE_CREDENTIAL_ARN = 'arn:aws:bedrock:us-east-1:123456789012:credential-provider/coinbase';
20+
21+
// ---------------------------------------------------------------------------
22+
// Tests
23+
// ---------------------------------------------------------------------------
24+
25+
describe('parsePaymentOutputs', () => {
26+
describe('happy path', () => {
27+
it('returns a complete PaymentDeployedState when all outputs are present', () => {
28+
const outputs: StackOutputs = {
29+
...makeOutputs('MyManager'),
30+
PaymentMyManagerCoinbaseConnectorId: 'conn-coinbase-001',
31+
};
32+
33+
const specs = [
34+
{
35+
name: 'MyManager',
36+
connectors: [
37+
{
38+
name: 'Coinbase',
39+
credentialProviderArn: COINBASE_CREDENTIAL_ARN,
40+
credentialProviderName: 'coinbase-cdp',
41+
},
42+
],
43+
},
44+
];
45+
46+
const result = parsePaymentOutputs(outputs, specs);
47+
48+
expect(result.MyManager).toBeDefined();
49+
expect(result.MyManager!.managerId).toBe('pm-mymanager-001');
50+
expect(result.MyManager!.managerArn).toBe('arn:aws:bedrock:us-east-1:123456789012:payment-manager/MyManager');
51+
expect(result.MyManager!.processPaymentRoleArn).toBe(
52+
'arn:aws:iam::123456789012:role/MyManagerProcessPaymentRole'
53+
);
54+
expect(result.MyManager!.resourceRetrievalRoleArn).toBe(
55+
'arn:aws:iam::123456789012:role/MyManagerResourceRetrievalRole'
56+
);
57+
expect(result.MyManager!.connectors.Coinbase).toEqual({
58+
connectorId: 'conn-coinbase-001',
59+
credentialProviderArn: COINBASE_CREDENTIAL_ARN,
60+
credentialProviderName: 'coinbase-cdp',
61+
});
62+
});
63+
});
64+
65+
describe('missing required manager fields', () => {
66+
it('skips a payment when managerArn is absent', () => {
67+
const outputs: StackOutputs = {
68+
PaymentMyManagerManagerId: 'pm-001',
69+
PaymentMyManagerProcessPaymentRoleArn: 'arn:aws:iam::123:role/ProcessPaymentRole',
70+
PaymentMyManagerResourceRetrievalRoleArn: 'arn:aws:iam::123:role/ResourceRetrievalRole',
71+
// managerArn intentionally omitted
72+
};
73+
74+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
75+
76+
expect(result.MyManager).toBeUndefined();
77+
expect(Object.keys(result)).toHaveLength(0);
78+
});
79+
80+
it('skips a payment when managerId is absent', () => {
81+
const outputs: StackOutputs = {
82+
PaymentMyManagerManagerArn: 'arn:aws:bedrock:us-east-1:123:payment-manager/MyManager',
83+
PaymentMyManagerProcessPaymentRoleArn: 'arn:aws:iam::123:role/ProcessPaymentRole',
84+
PaymentMyManagerResourceRetrievalRoleArn: 'arn:aws:iam::123:role/ResourceRetrievalRole',
85+
// managerId intentionally omitted
86+
};
87+
88+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
89+
90+
expect(result.MyManager).toBeUndefined();
91+
});
92+
93+
it('skips a payment when processPaymentRoleArn is absent', () => {
94+
const outputs: StackOutputs = {
95+
...makeOutputs('MyManager'),
96+
};
97+
delete outputs.PaymentMyManagerProcessPaymentRoleArn;
98+
99+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
100+
101+
expect(result.MyManager).toBeUndefined();
102+
});
103+
104+
it('skips a payment when resourceRetrievalRoleArn is absent', () => {
105+
const outputs: StackOutputs = {
106+
...makeOutputs('MyManager'),
107+
};
108+
delete outputs.PaymentMyManagerResourceRetrievalRoleArn;
109+
110+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
111+
112+
expect(result.MyManager).toBeUndefined();
113+
});
114+
});
115+
116+
describe('missing connector output', () => {
117+
it('includes the manager with an empty connectors map when connector output is absent', () => {
118+
const outputs: StackOutputs = makeOutputs('MyManager');
119+
// No connector output key present
120+
121+
const specs = [
122+
{
123+
name: 'MyManager',
124+
connectors: [
125+
{
126+
name: 'Coinbase',
127+
credentialProviderArn: COINBASE_CREDENTIAL_ARN,
128+
},
129+
],
130+
},
131+
];
132+
133+
const result = parsePaymentOutputs(outputs, specs);
134+
135+
expect(result.MyManager).toBeDefined();
136+
expect(result.MyManager!.connectors).toEqual({});
137+
});
138+
139+
it('includes a manager that has no connectors configured at all', () => {
140+
const outputs: StackOutputs = makeOutputs('MyManager');
141+
142+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
143+
144+
expect(result.MyManager).toBeDefined();
145+
expect(result.MyManager!.connectors).toEqual({});
146+
});
147+
});
148+
149+
describe('multiple managers', () => {
150+
it('parses both managers independently', () => {
151+
const outputs: StackOutputs = {
152+
...makeOutputs('Alpha'),
153+
...makeOutputs('Beta'),
154+
PaymentAlphaCoinbaseConnectorId: 'conn-alpha-coinbase',
155+
PaymentBetaStripeConnectorId: 'conn-beta-stripe',
156+
};
157+
158+
const specs = [
159+
{
160+
name: 'Alpha',
161+
connectors: [{ name: 'Coinbase', credentialProviderArn: 'arn:cred:alpha' }],
162+
},
163+
{
164+
name: 'Beta',
165+
connectors: [{ name: 'Stripe', credentialProviderArn: 'arn:cred:beta' }],
166+
},
167+
];
168+
169+
const result = parsePaymentOutputs(outputs, specs);
170+
171+
expect(Object.keys(result)).toHaveLength(2);
172+
173+
expect(result.Alpha!.managerId).toBe('pm-alpha-001');
174+
expect(result.Alpha!.connectors.Coinbase).toEqual({
175+
connectorId: 'conn-alpha-coinbase',
176+
credentialProviderArn: 'arn:cred:alpha',
177+
credentialProviderName: undefined,
178+
});
179+
180+
expect(result.Beta!.managerId).toBe('pm-beta-001');
181+
expect(result.Beta!.connectors.Stripe).toEqual({
182+
connectorId: 'conn-beta-stripe',
183+
credentialProviderArn: 'arn:cred:beta',
184+
credentialProviderName: undefined,
185+
});
186+
});
187+
188+
it('skips only the invalid manager when one of two is missing a required field', () => {
189+
const outputs: StackOutputs = {
190+
...makeOutputs('Good'),
191+
// Bad is missing resourceRetrievalRoleArn
192+
PaymentBadManagerArn: 'arn:aws:bedrock:us-east-1:123:payment-manager/Bad',
193+
PaymentBadManagerId: 'pm-bad-001',
194+
PaymentBadProcessPaymentRoleArn: 'arn:aws:iam::123:role/BadProcessPaymentRole',
195+
};
196+
197+
const result = parsePaymentOutputs(outputs, [
198+
{ name: 'Good', connectors: [] },
199+
{ name: 'Bad', connectors: [] },
200+
]);
201+
202+
expect(result.Good).toBeDefined();
203+
expect(result.Bad).toBeUndefined();
204+
});
205+
});
206+
207+
describe('authorizerType pass-through', () => {
208+
it('includes authorizerType AWS_IAM when set in spec', () => {
209+
const outputs: StackOutputs = makeOutputs('MyManager');
210+
211+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', authorizerType: 'AWS_IAM', connectors: [] }]);
212+
213+
expect(result.MyManager!.authorizerType).toBe('AWS_IAM');
214+
});
215+
216+
it('includes authorizerType CUSTOM_JWT when set in spec', () => {
217+
const outputs: StackOutputs = makeOutputs('MyManager');
218+
219+
const result = parsePaymentOutputs(outputs, [
220+
{ name: 'MyManager', authorizerType: 'CUSTOM_JWT', connectors: [] },
221+
]);
222+
223+
expect(result.MyManager!.authorizerType).toBe('CUSTOM_JWT');
224+
});
225+
226+
it('omits authorizerType when not set in spec', () => {
227+
const outputs: StackOutputs = makeOutputs('MyManager');
228+
229+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
230+
231+
expect(result.MyManager!.authorizerType).toBeUndefined();
232+
});
233+
});
234+
235+
describe('autoPayment / toolAllowlist / networkPreferences pass-through', () => {
236+
it('includes autoPayment: true when set in spec', () => {
237+
const outputs: StackOutputs = makeOutputs('MyManager');
238+
239+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', autoPayment: true, connectors: [] }]);
240+
241+
expect(result.MyManager!.autoPayment).toBe(true);
242+
});
243+
244+
it('includes autoPayment: false when explicitly set to false', () => {
245+
const outputs: StackOutputs = makeOutputs('MyManager');
246+
247+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', autoPayment: false, connectors: [] }]);
248+
249+
expect(result.MyManager!.autoPayment).toBe(false);
250+
});
251+
252+
it('omits autoPayment when not set in spec', () => {
253+
const outputs: StackOutputs = makeOutputs('MyManager');
254+
255+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
256+
257+
expect(result.MyManager!.autoPayment).toBeUndefined();
258+
});
259+
260+
it('includes paymentToolAllowlist when set in spec', () => {
261+
const outputs: StackOutputs = makeOutputs('MyManager');
262+
const allowlist = ['x402_pay', 'x402_check_balance'];
263+
264+
const result = parsePaymentOutputs(outputs, [
265+
{ name: 'MyManager', paymentToolAllowlist: allowlist, connectors: [] },
266+
]);
267+
268+
expect(result.MyManager!.paymentToolAllowlist).toEqual(allowlist);
269+
});
270+
271+
it('omits paymentToolAllowlist when not set in spec', () => {
272+
const outputs: StackOutputs = makeOutputs('MyManager');
273+
274+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
275+
276+
expect(result.MyManager!.paymentToolAllowlist).toBeUndefined();
277+
});
278+
279+
it('includes networkPreferences when set in spec', () => {
280+
const outputs: StackOutputs = makeOutputs('MyManager');
281+
const networks = ['eip155:84532', 'eip155:8453'];
282+
283+
const result = parsePaymentOutputs(outputs, [
284+
{ name: 'MyManager', networkPreferences: networks, connectors: [] },
285+
]);
286+
287+
expect(result.MyManager!.networkPreferences).toEqual(networks);
288+
});
289+
290+
it('omits networkPreferences when not set in spec', () => {
291+
const outputs: StackOutputs = makeOutputs('MyManager');
292+
293+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
294+
295+
expect(result.MyManager!.networkPreferences).toBeUndefined();
296+
});
297+
298+
it('passes all optional spec fields through together', () => {
299+
const outputs: StackOutputs = {
300+
...makeOutputs('MyManager'),
301+
PaymentMyManagerCoinbaseConnectorId: 'conn-001',
302+
};
303+
304+
const result = parsePaymentOutputs(outputs, [
305+
{
306+
name: 'MyManager',
307+
authorizerType: 'AWS_IAM',
308+
autoPayment: true,
309+
paymentToolAllowlist: ['x402_pay'],
310+
networkPreferences: ['eip155:84532'],
311+
connectors: [{ name: 'Coinbase', credentialProviderArn: COINBASE_CREDENTIAL_ARN }],
312+
},
313+
]);
314+
315+
expect(result.MyManager!.authorizerType).toBe('AWS_IAM');
316+
expect(result.MyManager!.autoPayment).toBe(true);
317+
expect(result.MyManager!.paymentToolAllowlist).toEqual(['x402_pay']);
318+
expect(result.MyManager!.networkPreferences).toEqual(['eip155:84532']);
319+
});
320+
});
321+
322+
describe('edge cases', () => {
323+
it('returns empty object when specs array is empty', () => {
324+
const outputs: StackOutputs = makeOutputs('MyManager');
325+
326+
const result = parsePaymentOutputs(outputs, []);
327+
328+
expect(result).toEqual({});
329+
});
330+
331+
it('returns empty object when outputs is empty', () => {
332+
const result = parsePaymentOutputs({}, [{ name: 'MyManager', connectors: [] }]);
333+
334+
expect(result).toEqual({});
335+
});
336+
337+
it('ignores unrelated stack outputs', () => {
338+
const outputs: StackOutputs = {
339+
...makeOutputs('MyManager'),
340+
SomeOtherOutputABC: 'unrelated-value',
341+
ApplicationAgentSomethingRuntimeIdOutput: 'rt-999',
342+
};
343+
344+
const result = parsePaymentOutputs(outputs, [{ name: 'MyManager', connectors: [] }]);
345+
346+
expect(Object.keys(result)).toHaveLength(1);
347+
expect(result.MyManager).toBeDefined();
348+
});
349+
});
350+
});

0 commit comments

Comments
 (0)