Skip to content

Commit 8eda0c6

Browse files
committed
test(auth-server): migrate subscription route unit tests from Mocha to Jest (FXA-12619)
1 parent 4ce4abe commit 8eda0c6

8 files changed

Lines changed: 7954 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import sinon from 'sinon';
6+
import { Container } from 'typedi';
7+
8+
const mocks = require('../../../test/mocks');
9+
const { AppleIapHandler } = require('./apple');
10+
const {
11+
PurchaseUpdateError,
12+
} = require('../../payments/iap/apple-app-store/types/errors');
13+
const { AppError: error } = require('@fxa/accounts/errors');
14+
const { AuthLogger } = require('../../types');
15+
const { AppleIAP } = require('../../payments/iap/apple-app-store/apple-iap');
16+
const { IAPConfig } = require('../../payments/iap/iap-config');
17+
const { OAUTH_SCOPE_SUBSCRIPTIONS_IAP } = require('fxa-shared/oauth/constants');
18+
const { CapabilityService } = require('../../payments/capability');
19+
const {
20+
CertificateValidationError,
21+
} = require('app-store-server-api/dist/cjs/Errors');
22+
23+
const MOCK_SCOPES = [OAUTH_SCOPE_SUBSCRIPTIONS_IAP];
24+
const VALID_REQUEST = {
25+
auth: {
26+
credentials: {
27+
scope: MOCK_SCOPES,
28+
user: `uid1234`,
29+
30+
},
31+
},
32+
};
33+
34+
describe('AppleIapHandler', () => {
35+
let iapConfig: any;
36+
let appleIap: any;
37+
let log: any;
38+
let appleIapHandler: any;
39+
let mockCapabilityService: any;
40+
41+
beforeEach(() => {
42+
log = mocks.mockLog();
43+
appleIap = {};
44+
Container.set(AuthLogger, log);
45+
iapConfig = {};
46+
Container.set(IAPConfig, iapConfig);
47+
Container.set(AppleIAP, appleIap);
48+
mockCapabilityService = {};
49+
mockCapabilityService.iapUpdate = sinon.fake.resolves({});
50+
Container.set(CapabilityService, mockCapabilityService);
51+
appleIapHandler = new AppleIapHandler();
52+
});
53+
54+
afterEach(() => {
55+
Container.reset();
56+
sinon.restore();
57+
});
58+
59+
describe('registerOriginalTransactionId', () => {
60+
const request = {
61+
...VALID_REQUEST,
62+
params: { appName: 'test' },
63+
payload: { originalTransactionId: 'testTransactionId' },
64+
};
65+
66+
it('returns valid with new products', async () => {
67+
appleIap.purchaseManager = {
68+
registerToUserAccount: sinon.fake.resolves({}),
69+
};
70+
iapConfig.getBundleId = sinon.fake.resolves('testPackage');
71+
const result =
72+
await appleIapHandler.registerOriginalTransactionId(request);
73+
sinon.assert.calledOnce(appleIap.purchaseManager.registerToUserAccount);
74+
sinon.assert.calledOnce(iapConfig.getBundleId);
75+
sinon.assert.calledOnce(mockCapabilityService.iapUpdate);
76+
expect(result).toEqual({ transactionIdValid: true });
77+
});
78+
79+
it('throws on invalid package', async () => {
80+
appleIap.purchaseManager = {
81+
registerToUserAccount: sinon.fake.resolves({}),
82+
};
83+
iapConfig.getBundleId = sinon.fake.resolves(undefined);
84+
try {
85+
await appleIapHandler.registerOriginalTransactionId(request);
86+
throw new Error('Expected failure');
87+
} catch (err: any) {
88+
sinon.assert.calledOnce(iapConfig.getBundleId);
89+
expect(err.errno).toBe(error.ERRNO.IAP_UNKNOWN_APPNAME);
90+
}
91+
});
92+
93+
it('throws on invalid transaction id', async () => {
94+
const libraryError = new Error('Purchase is not registerable');
95+
libraryError.name = PurchaseUpdateError.INVALID_ORIGINAL_TRANSACTION_ID;
96+
97+
appleIap.purchaseManager = {
98+
registerToUserAccount: sinon.fake.rejects(libraryError),
99+
};
100+
iapConfig.getBundleId = sinon.fake.resolves('testPackage');
101+
try {
102+
await appleIapHandler.registerOriginalTransactionId(request);
103+
throw new Error('Expected failure');
104+
} catch (err: any) {
105+
expect(err.errno).toBe(error.ERRNO.IAP_INVALID_TOKEN);
106+
sinon.assert.calledOnce(
107+
appleIap.purchaseManager.registerToUserAccount
108+
);
109+
sinon.assert.calledOnce(iapConfig.getBundleId);
110+
}
111+
});
112+
113+
it('throws on conflict', async () => {
114+
const libraryError = new Error('Purchase is not registerable');
115+
libraryError.name = PurchaseUpdateError.CONFLICT;
116+
117+
appleIap.purchaseManager = {
118+
registerToUserAccount: sinon.fake.rejects(libraryError),
119+
};
120+
iapConfig.getBundleId = sinon.fake.resolves('testPackage');
121+
try {
122+
await appleIapHandler.registerOriginalTransactionId(request);
123+
throw new Error('Expected failure');
124+
} catch (err: any) {
125+
expect(err.errno).toBe(error.ERRNO.IAP_PURCHASE_ALREADY_REGISTERED);
126+
sinon.assert.calledOnce(
127+
appleIap.purchaseManager.registerToUserAccount
128+
);
129+
sinon.assert.calledOnce(iapConfig.getBundleId);
130+
}
131+
});
132+
133+
it('throws on unknown errors', async () => {
134+
appleIap.purchaseManager = {
135+
registerToUserAccount: sinon.fake.rejects(new Error('Unknown error')),
136+
};
137+
iapConfig.getBundleId = sinon.fake.resolves('testPackage');
138+
try {
139+
await appleIapHandler.registerOriginalTransactionId(request);
140+
throw new Error('Expected failure');
141+
} catch (err: any) {
142+
expect(err.errno).toBe(error.ERRNO.BACKEND_SERVICE_FAILURE);
143+
sinon.assert.calledOnce(
144+
appleIap.purchaseManager.registerToUserAccount
145+
);
146+
sinon.assert.calledOnce(iapConfig.getBundleId);
147+
}
148+
});
149+
});
150+
151+
describe('processNotification', () => {
152+
const mockBundleId = 'testPackage';
153+
const mockOriginalTransactionId = '123';
154+
155+
let mockDecodedNotificationPayload: any;
156+
let mockPurchase: any;
157+
let mockRequest: any;
158+
159+
beforeEach(() => {
160+
mockDecodedNotificationPayload = {
161+
notificationType: 'WOW',
162+
subtype: 'IMPRESS',
163+
};
164+
mockPurchase = {
165+
userId: 'test1234',
166+
};
167+
mockRequest = {
168+
payload: {
169+
signedPayload: 'base64 encoded string',
170+
},
171+
};
172+
appleIap.purchaseManager = {
173+
decodeNotificationPayload: sinon.fake.resolves({
174+
bundleId: mockBundleId,
175+
originalTransactionId: mockOriginalTransactionId,
176+
decodedPayload: mockDecodedNotificationPayload,
177+
}),
178+
getSubscriptionPurchase: sinon.fake.resolves(mockPurchase),
179+
processNotification: sinon.fake.resolves({}),
180+
};
181+
});
182+
183+
it('handles a notification that requires profile updating', async () => {
184+
const result = await appleIapHandler.processNotification(mockRequest);
185+
expect(result).toEqual({});
186+
sinon.assert.calledOnceWithExactly(
187+
appleIap.purchaseManager.decodeNotificationPayload,
188+
mockRequest.payload.signedPayload
189+
);
190+
sinon.assert.calledOnce(
191+
appleIap.purchaseManager.getSubscriptionPurchase
192+
);
193+
sinon.assert.calledOnce(appleIap.purchaseManager.processNotification);
194+
sinon.assert.calledOnce(mockCapabilityService.iapUpdate);
195+
sinon.assert.calledOnceWithExactly(
196+
log.debug,
197+
'appleIap.processNotification.decodedPayload',
198+
{
199+
bundleId: mockBundleId,
200+
originalTransactionId: mockOriginalTransactionId,
201+
notificationType: mockDecodedNotificationPayload.notificationType,
202+
notificationSubtype: mockDecodedNotificationPayload.subtype,
203+
}
204+
);
205+
});
206+
207+
it("doesn't log a notificationSubtype when omitted from the notification", async () => {
208+
delete mockDecodedNotificationPayload.subtype;
209+
const result = await appleIapHandler.processNotification(mockRequest);
210+
expect(result).toEqual({});
211+
sinon.assert.calledOnceWithExactly(
212+
log.debug,
213+
'appleIap.processNotification.decodedPayload',
214+
{
215+
bundleId: mockBundleId,
216+
originalTransactionId: mockOriginalTransactionId,
217+
notificationType: mockDecodedNotificationPayload.notificationType,
218+
}
219+
);
220+
});
221+
222+
it('throws an unauthorized error on certificate validation failure', async () => {
223+
appleIap.purchaseManager.decodeNotificationPayload = sinon.fake.rejects(
224+
new CertificateValidationError()
225+
);
226+
try {
227+
await appleIapHandler.processNotification(mockRequest);
228+
throw new Error('Should have thrown.');
229+
} catch (err: any) {
230+
expect(err.output.statusCode).toBe(401);
231+
expect(err.message).toBe('Unauthorized for route');
232+
}
233+
});
234+
235+
it('rethrows any other type of error if decoding the notification fails', async () => {
236+
appleIap.purchaseManager.decodeNotificationPayload = sinon.fake.rejects(
237+
new Error('Yikes')
238+
);
239+
try {
240+
await appleIapHandler.processNotification(mockRequest);
241+
throw new Error('Should have thrown.');
242+
} catch (err: any) {
243+
expect(err.message).toBe('Yikes');
244+
}
245+
});
246+
247+
it('Still processes the notification if the purchase is not found in Firestore', async () => {
248+
appleIap.purchaseManager.getSubscriptionPurchase =
249+
sinon.fake.resolves(null);
250+
const result = await appleIapHandler.processNotification(mockRequest);
251+
expect(result).toEqual({});
252+
sinon.assert.calledOnce(appleIap.purchaseManager.processNotification);
253+
});
254+
255+
it('Still processes the notification if there is no user id but does not broadcast', async () => {
256+
mockPurchase.userId = null;
257+
const result = await appleIapHandler.processNotification(mockRequest);
258+
expect(result).toEqual({});
259+
sinon.assert.calledOnce(appleIap.purchaseManager.processNotification);
260+
sinon.assert.notCalled(mockCapabilityService.iapUpdate);
261+
});
262+
});
263+
});

0 commit comments

Comments
 (0)