Skip to content

Commit 419f5b7

Browse files
authored
Merge pull request #20238 from mozilla/fxa-12620
feat(auth-server): migrate auth server profile mocha tests to jest
2 parents f756eba + cf69a46 commit 419f5b7

5 files changed

Lines changed: 1433 additions & 0 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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 Sentry from '@sentry/node';
7+
import { default as Container } from 'typedi';
8+
9+
const sentryModule = require('./sentry');
10+
const { mockLog } = require('../test/mocks');
11+
const { GoogleMapsService } = require('./google-maps-services');
12+
const { AuthLogger, AppConfig } = require('./types');
13+
14+
function deepCopy(object: any) {
15+
return JSON.parse(JSON.stringify(object));
16+
}
17+
18+
const geocodeResultMany = {
19+
data: {
20+
results: [
21+
{
22+
address_components: [
23+
{
24+
long_name: 'Maryland',
25+
short_name: 'MD',
26+
types: ['administrative_area_level_1', 'political'],
27+
},
28+
{
29+
long_name: '11111',
30+
short_name: '11111',
31+
types: ['postal_code'],
32+
},
33+
],
34+
},
35+
{
36+
address_components: [
37+
{
38+
long_name: 'New York',
39+
short_name: 'NY',
40+
types: ['administrative_area_level_1', 'political'],
41+
},
42+
],
43+
},
44+
],
45+
status: 'OK',
46+
},
47+
};
48+
49+
const geocodeResultWithoutState = {
50+
data: {
51+
results: [
52+
{
53+
address_components: [
54+
{
55+
long_name: '20639',
56+
short_name: '20639',
57+
types: ['postal_code'],
58+
},
59+
{
60+
long_name: 'United States',
61+
short_name: 'US',
62+
types: ['country', 'political'],
63+
},
64+
],
65+
},
66+
],
67+
status: 'OK',
68+
},
69+
};
70+
71+
const geocodeResultDEZip = {
72+
data: {
73+
results: [
74+
{
75+
address_components: [
76+
{
77+
long_name: 'Saxony-Anhalt',
78+
short_name: 'SA',
79+
types: ['administrative_area_level_1', 'political'],
80+
},
81+
],
82+
},
83+
],
84+
status: 'OK',
85+
},
86+
};
87+
88+
const noResult = {
89+
data: {
90+
results: [],
91+
status: 'ZERO_RESULTS',
92+
},
93+
};
94+
95+
const noResultWithError = {
96+
data: {
97+
results: [],
98+
status: 'UNKNOWN_ERROR',
99+
error_message: 'An unknown error has occurred',
100+
},
101+
};
102+
103+
const mockConfig = {
104+
googleMapsApiKey: 'foo',
105+
};
106+
107+
let googleMapsServices: any;
108+
let googleClient: any;
109+
110+
describe('GoogleMapsServices', () => {
111+
let log: any;
112+
113+
beforeEach(() => {
114+
log = mockLog();
115+
Container.set(AuthLogger, log);
116+
Container.set(AppConfig, mockConfig);
117+
googleMapsServices = new GoogleMapsService();
118+
googleMapsServices.client = googleClient = {};
119+
});
120+
121+
afterEach(() => {
122+
sinon.restore();
123+
Container.reset();
124+
});
125+
126+
describe('getStateFromZip', () => {
127+
it('returns location for zip code and country', async () => {
128+
const expectedResult = 'SA';
129+
const expectedAddress = '06369, Germany';
130+
googleClient.geocode = sinon.stub().resolves(geocodeResultDEZip);
131+
132+
const actualResult = await googleMapsServices.getStateFromZip(
133+
'06369',
134+
'DE'
135+
);
136+
expect(actualResult).toBe(expectedResult);
137+
expect(googleClient.geocode.calledOnce).toBe(true);
138+
expect(
139+
googleClient.geocode.getCall(0).args[0].params.address
140+
).toBe(expectedAddress);
141+
});
142+
143+
it('returns location for zip code and country if more than 1 result is returned with matching states', async () => {
144+
const geocodeResultManyMatchingStates = deepCopy(geocodeResultMany);
145+
geocodeResultManyMatchingStates.data.results[1].address_components[0].short_name =
146+
'MD';
147+
const expectedResult = 'MD';
148+
const expectedAddress = '11111, United States of America';
149+
googleClient.geocode = sinon
150+
.stub()
151+
.resolves(geocodeResultManyMatchingStates);
152+
153+
const actualResult = await googleMapsServices.getStateFromZip(
154+
'11111',
155+
'US'
156+
);
157+
expect(actualResult).toBe(expectedResult);
158+
expect(googleClient.geocode.calledOnce).toBe(true);
159+
expect(
160+
googleClient.geocode.getCall(0).args[0].params.address
161+
).toBe(expectedAddress);
162+
});
163+
164+
it('Throws error if more than 1 result is returned with mismatching states', async () => {
165+
const expectedMessage = 'Could not find unique results. (22222, Germany)';
166+
googleClient.geocode = sinon.stub().resolves(geocodeResultMany);
167+
168+
try {
169+
await googleMapsServices.getStateFromZip('22222', 'DE');
170+
throw new Error('Expected error to be thrown');
171+
} catch (err) {
172+
expect(
173+
googleMapsServices.log.error.getCall(0).args[1].error.message
174+
).toBe(expectedMessage);
175+
}
176+
});
177+
178+
it('Throws error for invalid country code', async () => {
179+
const expectedMessage =
180+
'Invalid country (Germany). Only ISO 3166-1 alpha-2 country codes are supported.';
181+
182+
try {
183+
await googleMapsServices.getStateFromZip('11111', 'Germany');
184+
throw new Error('Expected error to be thrown');
185+
} catch (err) {
186+
expect(
187+
googleMapsServices.log.error.getCall(0).args[1].error.message
188+
).toBe(expectedMessage);
189+
}
190+
});
191+
192+
it('Throws error for zip code without state', async () => {
193+
const expectedMessage = 'State could not be found. (11111, Germany)';
194+
googleClient.geocode = sinon.stub().resolves(geocodeResultWithoutState);
195+
196+
try {
197+
await googleMapsServices.getStateFromZip('11111', 'DE');
198+
throw new Error('Expected error to be thrown');
199+
} catch (err) {
200+
expect(
201+
googleMapsServices.log.error.getCall(0).args[1].error.message
202+
).toBe(expectedMessage);
203+
}
204+
});
205+
206+
it('Throws error if no results were found', async () => {
207+
const expectedMessage =
208+
'Could not find any results for address. (11111, Germany)';
209+
googleClient.geocode = sinon.stub().resolves(noResult);
210+
211+
try {
212+
await googleMapsServices.getStateFromZip('11111', 'DE');
213+
throw new Error('Expected error to be thrown');
214+
} catch (err) {
215+
expect(
216+
googleMapsServices.log.error.getCall(0).args[1].error.message
217+
).toBe(expectedMessage);
218+
}
219+
});
220+
221+
it('Throws error for bad status code', async () => {
222+
const expectedMessage =
223+
'UNKNOWN_ERROR - An unknown error has occurred. (11111, Germany)';
224+
googleClient.geocode = sinon.stub().resolves(noResultWithError);
225+
226+
const scopeContextSpy = sinon.fake();
227+
const scopeSpy = {
228+
setContext: scopeContextSpy,
229+
};
230+
sinon.replace(Sentry, 'withScope', ((fn: any) => fn(scopeSpy)) as any);
231+
sinon.stub(sentryModule, 'reportSentryMessage').returns({});
232+
233+
try {
234+
await googleMapsServices.getStateFromZip('11111', 'DE');
235+
throw new Error('Expected error to be thrown');
236+
} catch (err) {
237+
expect(
238+
googleMapsServices.log.error.getCall(0).args[1].error.message
239+
).toBe(expectedMessage);
240+
expect(scopeContextSpy.calledOnce).toBe(true);
241+
expect(sentryModule.reportSentryMessage.calledOnce).toBe(true);
242+
}
243+
});
244+
245+
it('Throws error when GeocodeData fails', async () => {
246+
const expectedMessage = 'Geocode is not available';
247+
googleClient.geocode = sinon.stub().rejects(new Error(expectedMessage));
248+
249+
const scopeContextSpy = sinon.fake();
250+
const scopeSpy = {
251+
setContext: scopeContextSpy,
252+
};
253+
sinon.replace(Sentry, 'withScope', ((fn: any) => fn(scopeSpy)) as any);
254+
sinon.stub(sentryModule, 'reportSentryMessage').returns({});
255+
256+
try {
257+
await googleMapsServices.getStateFromZip('11111', 'DE');
258+
throw new Error('Expected error to be thrown');
259+
} catch (err) {
260+
expect(
261+
googleMapsServices.log.error.getCall(0).args[1].error.message
262+
).toBe(expectedMessage);
263+
expect(scopeContextSpy.calledOnce).toBe(true);
264+
expect(sentryModule.reportSentryMessage.calledOnce).toBe(true);
265+
}
266+
});
267+
});
268+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 { EventEmitter } from 'events';
7+
8+
const { mockDB, mockLog } = require('../../test/mocks');
9+
const profileUpdates = require('./updates');
10+
11+
const mockDeliveryQueue = new EventEmitter();
12+
(mockDeliveryQueue as any).start = function start() {};
13+
14+
function mockMessage(msg: any) {
15+
msg.del = sinon.spy();
16+
return msg;
17+
}
18+
19+
let pushShouldThrow = false;
20+
const mockPush = {
21+
notifyProfileUpdated: sinon.spy((uid: string) => {
22+
expect(typeof uid).toBe('string');
23+
if (pushShouldThrow) {
24+
throw new Error('oops');
25+
}
26+
return Promise.resolve();
27+
}),
28+
};
29+
30+
function mockProfileUpdates(log: any) {
31+
return profileUpdates(log)(mockDeliveryQueue, mockPush, mockDB());
32+
}
33+
34+
describe('profile updates', () => {
35+
beforeEach(() => {
36+
mockPush.notifyProfileUpdated.resetHistory();
37+
pushShouldThrow = false;
38+
});
39+
40+
it('should log errors', async () => {
41+
pushShouldThrow = true;
42+
const log = mockLog();
43+
await mockProfileUpdates(log).handleProfileUpdated(
44+
mockMessage({
45+
uid: 'bogusuid',
46+
})
47+
);
48+
expect(mockPush.notifyProfileUpdated.callCount).toBe(1);
49+
expect(log.error.callCount).toBe(1);
50+
});
51+
52+
it('should send notifications', async () => {
53+
const log = mockLog();
54+
const uid = '1e2122ba';
55+
const email = '[email protected]';
56+
const locale = 'en-US';
57+
const metricsEnabled = true;
58+
const totpEnabled = false;
59+
const accountDisabled = false;
60+
const accountLocked = false;
61+
62+
await mockProfileUpdates(log).handleProfileUpdated(
63+
mockMessage({
64+
uid,
65+
email,
66+
locale,
67+
metricsEnabled,
68+
totpEnabled,
69+
accountDisabled,
70+
accountLocked,
71+
})
72+
);
73+
74+
expect(log.error.callCount).toBe(0);
75+
expect(mockPush.notifyProfileUpdated.callCount).toBe(1);
76+
const args = mockPush.notifyProfileUpdated.getCall(0).args;
77+
expect(args[0]).toBe(uid);
78+
79+
expect(
80+
log.notifyAttachedServices.calledWithExactly(
81+
'profileDataChange',
82+
{},
83+
{ uid }
84+
)
85+
).toBe(true);
86+
});
87+
});

0 commit comments

Comments
 (0)