Skip to content

Commit a28d578

Browse files
authored
Merge pull request #20882 from mozilla/subplat_fix-341-3
fix(payments-ui): Fix circular dependency in assertCartOwnership decorator
2 parents bb1e0a0 + 4a5efab commit a28d578

2 files changed

Lines changed: 145 additions & 1 deletion

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
/* This Source Code Form is subject to the terms of the Mozilla Public
6+
* License, v. 2.0. If a copy of the MPL was not distributed with this
7+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8+
9+
import { CartUidMismatchError } from '@fxa/payments/cart';
10+
import { AssertCartOwnership } from './assertCartOwnership.decorator';
11+
12+
const MOCK_SESSION_UID = 'session-uid-abc-123';
13+
const MOCK_CART_ID = 'cart-id-def-456';
14+
const MOCK_OTHER_UID = 'other-uid-xyz-789';
15+
16+
const mockGetSessionUid = jest.fn<Promise<string | undefined>, []>();
17+
18+
jest.mock('@fxa/payments/ui-auth', () => ({
19+
__esModule: true,
20+
getSessionUid: (...args: unknown[]) => mockGetSessionUid(...(args as [])),
21+
}));
22+
23+
class TestService {
24+
cartManager = {
25+
fetchCartById: jest.fn(),
26+
};
27+
28+
originalMethodMock = jest.fn();
29+
30+
@AssertCartOwnership()
31+
async decoratedMethod(args: { cartId: string }) {
32+
return this.originalMethodMock(args);
33+
}
34+
}
35+
36+
describe('AssertCartOwnership', () => {
37+
let service: TestService;
38+
39+
beforeEach(() => {
40+
jest.clearAllMocks();
41+
service = new TestService();
42+
mockGetSessionUid.mockResolvedValue(MOCK_SESSION_UID);
43+
});
44+
45+
it('calls the original method when the cart has no uid', async () => {
46+
service.cartManager.fetchCartById.mockResolvedValue({
47+
id: MOCK_CART_ID,
48+
uid: null,
49+
});
50+
service.originalMethodMock.mockResolvedValue('result');
51+
52+
const result = await service.decoratedMethod({ cartId: MOCK_CART_ID });
53+
54+
expect(result).toBe('result');
55+
expect(service.cartManager.fetchCartById).toHaveBeenCalledWith(
56+
MOCK_CART_ID
57+
);
58+
expect(service.originalMethodMock).toHaveBeenCalledWith({
59+
cartId: MOCK_CART_ID,
60+
});
61+
});
62+
63+
it('calls the original method when cart uid matches the session uid', async () => {
64+
service.cartManager.fetchCartById.mockResolvedValue({
65+
id: MOCK_CART_ID,
66+
uid: MOCK_SESSION_UID,
67+
});
68+
service.originalMethodMock.mockResolvedValue('result');
69+
70+
const result = await service.decoratedMethod({ cartId: MOCK_CART_ID });
71+
72+
expect(result).toBe('result');
73+
expect(service.originalMethodMock).toHaveBeenCalledWith({
74+
cartId: MOCK_CART_ID,
75+
});
76+
});
77+
78+
it('throws CartUidMismatchError when cart uid does not match the session uid', async () => {
79+
service.cartManager.fetchCartById.mockResolvedValue({
80+
id: MOCK_CART_ID,
81+
uid: MOCK_OTHER_UID,
82+
});
83+
84+
await expect(
85+
service.decoratedMethod({ cartId: MOCK_CART_ID })
86+
).rejects.toThrow(CartUidMismatchError);
87+
expect(service.originalMethodMock).not.toHaveBeenCalled();
88+
});
89+
90+
it('throws CartUidMismatchError with the cart id and session uid', async () => {
91+
service.cartManager.fetchCartById.mockResolvedValue({
92+
id: MOCK_CART_ID,
93+
uid: MOCK_OTHER_UID,
94+
});
95+
96+
let thrownError: CartUidMismatchError | undefined;
97+
try {
98+
await service.decoratedMethod({ cartId: MOCK_CART_ID });
99+
} catch (err) {
100+
thrownError = err as CartUidMismatchError;
101+
}
102+
103+
expect(thrownError).toBeInstanceOf(CartUidMismatchError);
104+
expect(thrownError?.name).toBe('CartUidMismatchError');
105+
expect(thrownError?.message).toBe('Cart UID does not match session UID');
106+
});
107+
108+
it('allows access when the session uid is undefined and the cart has a uid', async () => {
109+
mockGetSessionUid.mockResolvedValue(undefined);
110+
service.cartManager.fetchCartById.mockResolvedValue({
111+
id: MOCK_CART_ID,
112+
uid: MOCK_OTHER_UID,
113+
});
114+
115+
// cart.uid ('other-uid') !== sessionUid (undefined) → should throw
116+
await expect(
117+
service.decoratedMethod({ cartId: MOCK_CART_ID })
118+
).rejects.toThrow(CartUidMismatchError);
119+
});
120+
121+
it('fetches the cart by the cartId from args', async () => {
122+
service.cartManager.fetchCartById.mockResolvedValue({
123+
id: MOCK_CART_ID,
124+
uid: null,
125+
});
126+
service.originalMethodMock.mockResolvedValue(undefined);
127+
128+
await service.decoratedMethod({ cartId: MOCK_CART_ID });
129+
130+
expect(service.cartManager.fetchCartById).toHaveBeenCalledWith(
131+
MOCK_CART_ID
132+
);
133+
});
134+
135+
it('propagates errors from fetchCartById', async () => {
136+
const dbError = new Error('ECONNREFUSED');
137+
service.cartManager.fetchCartById.mockRejectedValue(dbError);
138+
139+
await expect(
140+
service.decoratedMethod({ cartId: MOCK_CART_ID })
141+
).rejects.toThrow(dbError);
142+
expect(service.originalMethodMock).not.toHaveBeenCalled();
143+
});
144+
});

libs/payments/ui/src/lib/nestapp/assertCartOwnership.decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
import { getSessionUid } from '@fxa/payments/ui-auth';
65
import { CartUidMismatchError } from '@fxa/payments/cart';
76

87
/**
@@ -15,6 +14,7 @@ export function AssertCartOwnership() {
1514

1615
descriptor.value = async function (this: any, args: any) {
1716
const cartId = args?.cartId as string;
17+
const { getSessionUid } = await import('@fxa/payments/ui-auth');
1818
const sessionUid = await getSessionUid();
1919

2020
const cart = await this.cartManager.fetchCartById(cartId);

0 commit comments

Comments
 (0)