Skip to content

Commit cf94c97

Browse files
authored
Merge pull request #20243 from mozilla/fxa-12562
feat(auth): migrate metrics/logging/monitoring tests from Mocha to Jest
2 parents 7e670e8 + 524bce3 commit cf94c97

8 files changed

Lines changed: 5590 additions & 0 deletions

File tree

packages/fxa-auth-server/lib/log.spec.ts

Lines changed: 863 additions & 0 deletions
Large diffs are not rendered by default.

packages/fxa-auth-server/lib/metrics/amplitude.spec.ts

Lines changed: 1032 additions & 0 deletions
Large diffs are not rendered by default.

packages/fxa-auth-server/lib/metrics/context.spec.ts

Lines changed: 1061 additions & 0 deletions
Large diffs are not rendered by default.

packages/fxa-auth-server/lib/metrics/events.spec.ts

Lines changed: 1162 additions & 0 deletions
Large diffs are not rendered by default.

packages/fxa-auth-server/lib/metrics/glean/index.spec.ts

Lines changed: 921 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
6+
import { MetricsRedis, MetricsRedisConfig } from './metricsCache';
7+
8+
// Mock typedi so Container.has() always returns false, preventing DI lookups.
9+
jest.mock('typedi', () => ({
10+
Container: { has: jest.fn().mockReturnValue(false), get: jest.fn() },
11+
Token: jest.fn(),
12+
}));
13+
14+
// Mock RedisShared so the constructor does not create a real ioredis connection.
15+
// We replace `this.redis` in beforeEach anyway, but this prevents side effects.
16+
jest.mock('fxa-shared/db/redis', () => {
17+
class RedisShared {
18+
redis: any = {};
19+
constructor() {
20+
// no-op
21+
}
22+
}
23+
return { RedisShared, Config: {} };
24+
});
25+
26+
describe('MetricsRedis', () => {
27+
let metricsRedis: MetricsRedis;
28+
let redisStub: {
29+
exists: jest.Mock;
30+
set: jest.Mock;
31+
get: jest.Mock;
32+
del: jest.Mock;
33+
};
34+
35+
const KEY = `testKey${Date.now()}`;
36+
const metricsContext = {
37+
deviceId: 'eb3a368713e94801b0f3a67df6d059e0',
38+
entrypoint: 'preferences',
39+
flowBeginTime: 1711393758313,
40+
flowId:
41+
'4083592c5736512a96cea28ad68ae9c7ecc8f96298d394fe3430687531e703d2',
42+
flowCompleteSignal: 'account.signed',
43+
flowType: 'login',
44+
service: 'sync',
45+
};
46+
47+
const enabledConfig: MetricsRedisConfig = {
48+
redis: {
49+
metrics: {
50+
enabled: true,
51+
prefix: 'metrics:',
52+
lifetime: 60,
53+
} as any,
54+
},
55+
};
56+
57+
beforeEach(() => {
58+
redisStub = {
59+
exists: jest.fn(),
60+
set: jest.fn(),
61+
get: jest.fn(),
62+
del: jest.fn(),
63+
};
64+
65+
metricsRedis = new MetricsRedis(enabledConfig);
66+
// Replace the internal redis client with our stub.
67+
(metricsRedis as any).redis = redisStub;
68+
});
69+
70+
afterEach(() => {
71+
jest.restoreAllMocks();
72+
});
73+
74+
describe('#add', () => {
75+
it('should add data to the cache', async () => {
76+
redisStub.exists.mockResolvedValue(0);
77+
redisStub.set.mockResolvedValue('OK');
78+
79+
await metricsRedis.add(KEY, metricsContext);
80+
81+
expect(redisStub.exists).toHaveBeenCalledWith(KEY);
82+
expect(redisStub.set).toHaveBeenCalledWith(
83+
KEY,
84+
JSON.stringify(metricsContext),
85+
'EX',
86+
60
87+
);
88+
});
89+
90+
it('should throw an error if key already exists', async () => {
91+
redisStub.exists.mockResolvedValue(1);
92+
93+
await expect(metricsRedis.add(KEY, metricsContext)).rejects.toThrow(
94+
'Key already exists'
95+
);
96+
});
97+
98+
it('should fail silently if the cache is not enabled', async () => {
99+
redisStub.exists.mockResolvedValue(0);
100+
(metricsRedis as any).enabled = false;
101+
102+
await metricsRedis.add(KEY, metricsContext);
103+
104+
expect(redisStub.set).not.toHaveBeenCalled();
105+
});
106+
});
107+
108+
describe('#del', () => {
109+
it('should delete data from the cache', async () => {
110+
redisStub.del.mockResolvedValue(1);
111+
112+
await metricsRedis.del(KEY);
113+
114+
expect(redisStub.del).toHaveBeenCalledWith(KEY);
115+
});
116+
117+
it('should fail silently if the cache is not enabled', async () => {
118+
(metricsRedis as any).enabled = false;
119+
120+
await metricsRedis.del(KEY);
121+
122+
expect(redisStub.del).not.toHaveBeenCalled();
123+
});
124+
});
125+
126+
describe('#get', () => {
127+
it('should fetch data from the cache', async () => {
128+
redisStub.get.mockResolvedValue(JSON.stringify(metricsContext));
129+
130+
const result = await metricsRedis.get(KEY);
131+
132+
expect(redisStub.get).toHaveBeenCalledWith(KEY);
133+
expect(result).toEqual(metricsContext);
134+
});
135+
136+
it('should return an empty object if an error occurs', async () => {
137+
redisStub.get.mockRejectedValue(new Error('Test error'));
138+
139+
const result = await metricsRedis.get(KEY);
140+
141+
expect(redisStub.get).toHaveBeenCalledWith(KEY);
142+
expect(result).toEqual({});
143+
});
144+
145+
it('should return an empty object if not enabled', async () => {
146+
(metricsRedis as any).enabled = false;
147+
148+
const result = await metricsRedis.get(KEY);
149+
150+
expect(redisStub.get).not.toHaveBeenCalled();
151+
expect(result).toEqual({});
152+
});
153+
});
154+
});
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
6+
describe('monitoring', () => {
7+
let mockInitMonitoring: jest.Mock;
8+
let mockIgnoreErrors: jest.Mock;
9+
let mockLog: jest.Mock;
10+
let mockLogger: Record<string, jest.Mock>;
11+
let mockHapiIntegration: jest.Mock;
12+
let mockLinkedErrorsIntegration: jest.Mock;
13+
14+
beforeEach(() => {
15+
jest.resetModules();
16+
17+
mockInitMonitoring = jest.fn();
18+
mockIgnoreErrors = jest.fn();
19+
mockLogger = {
20+
info: jest.fn(),
21+
error: jest.fn(),
22+
warn: jest.fn(),
23+
debug: jest.fn(),
24+
trace: jest.fn(),
25+
};
26+
mockLog = jest.fn().mockReturnValue(mockLogger);
27+
mockHapiIntegration = jest.fn().mockReturnValue({ name: 'Hapi' });
28+
mockLinkedErrorsIntegration = jest
29+
.fn()
30+
.mockReturnValue({ name: 'LinkedErrors' });
31+
32+
jest.doMock('fxa-shared/monitoring', () => ({
33+
initMonitoring: mockInitMonitoring,
34+
}));
35+
jest.doMock('../config', () => ({
36+
config: {
37+
getProperties: jest.fn().mockReturnValue({
38+
log: { level: 'debug' },
39+
sentry: { dsn: 'https://[email protected]/123' },
40+
}),
41+
},
42+
}));
43+
jest.doMock('./log', () => mockLog);
44+
jest.doMock('../package.json', () => ({ version: '1.234.0' }), {
45+
virtual: true,
46+
});
47+
jest.doMock('@fxa/accounts/errors', () => ({
48+
ignoreErrors: mockIgnoreErrors,
49+
}));
50+
jest.doMock('@sentry/node', () => ({
51+
hapiIntegration: mockHapiIntegration,
52+
linkedErrorsIntegration: mockLinkedErrorsIntegration,
53+
}));
54+
55+
// Requiring the module triggers the top-level initMonitoring() call.
56+
require('./monitoring');
57+
});
58+
59+
afterEach(() => {
60+
jest.restoreAllMocks();
61+
});
62+
63+
it('calls initMonitoring on module load', () => {
64+
expect(mockInitMonitoring).toHaveBeenCalledTimes(1);
65+
66+
const callArg = mockInitMonitoring.mock.calls[0][0];
67+
68+
// Logger is the return value of require('./log')(level, name)
69+
expect(callArg.logger).toBe(mockLogger);
70+
expect(mockLog).toHaveBeenCalledWith('debug', 'configure-sentry');
71+
72+
// Config includes spread properties, release, eventFilters, integrations
73+
expect(callArg.config).toEqual(
74+
expect.objectContaining({
75+
log: { level: 'debug' },
76+
sentry: { dsn: 'https://[email protected]/123' },
77+
release: '1.234.0',
78+
})
79+
);
80+
expect(callArg.config.eventFilters).toHaveLength(1);
81+
expect(typeof callArg.config.eventFilters[0]).toBe('function');
82+
expect(callArg.config.integrations).toHaveLength(2);
83+
});
84+
85+
it('passes Sentry integrations with correct configuration', () => {
86+
expect(mockHapiIntegration).toHaveBeenCalledTimes(1);
87+
expect(mockLinkedErrorsIntegration).toHaveBeenCalledWith({
88+
key: 'jse_cause',
89+
});
90+
});
91+
92+
describe('filterSentryEvent', () => {
93+
let filterSentryEvent: (event: any, hint?: any) => any;
94+
95+
beforeEach(() => {
96+
// Extract the filterSentryEvent function from the initMonitoring call
97+
const callArg = mockInitMonitoring.mock.calls[0][0];
98+
filterSentryEvent = callArg.config.eventFilters[0];
99+
});
100+
101+
it('returns null when ignoreErrors returns true', () => {
102+
mockIgnoreErrors.mockReturnValue(true);
103+
104+
const event = { event_id: 'abc123' };
105+
const hint = { originalException: new Error('ignored error') };
106+
107+
const result = filterSentryEvent(event, hint);
108+
109+
expect(result).toBeNull();
110+
expect(mockIgnoreErrors).toHaveBeenCalledWith(hint.originalException);
111+
});
112+
113+
it('returns the event when ignoreErrors returns false', () => {
114+
mockIgnoreErrors.mockReturnValue(false);
115+
116+
const event = { event_id: 'def456' };
117+
const hint = { originalException: new Error('real error') };
118+
119+
const result = filterSentryEvent(event, hint);
120+
121+
expect(result).toBe(event);
122+
expect(mockIgnoreErrors).toHaveBeenCalledWith(hint.originalException);
123+
});
124+
125+
it('returns the event when hint.originalException is null', () => {
126+
const event = { event_id: 'ghi789' };
127+
const hint = { originalException: null };
128+
129+
const result = filterSentryEvent(event, hint);
130+
131+
expect(result).toBe(event);
132+
expect(mockIgnoreErrors).not.toHaveBeenCalled();
133+
});
134+
135+
it('returns the event when hint.originalException is undefined', () => {
136+
const event = { event_id: 'jkl012' };
137+
const hint = { originalException: undefined };
138+
139+
const result = filterSentryEvent(event, hint);
140+
141+
expect(result).toBe(event);
142+
expect(mockIgnoreErrors).not.toHaveBeenCalled();
143+
});
144+
145+
it('returns the event when hint is undefined', () => {
146+
const event = { event_id: 'mno345' };
147+
148+
const result = filterSentryEvent(event, undefined);
149+
150+
expect(result).toBe(event);
151+
expect(mockIgnoreErrors).not.toHaveBeenCalled();
152+
});
153+
154+
it('returns the event when hint is null', () => {
155+
const event = { event_id: 'pqr678' };
156+
157+
const result = filterSentryEvent(event, null);
158+
159+
expect(result).toBe(event);
160+
expect(mockIgnoreErrors).not.toHaveBeenCalled();
161+
});
162+
});
163+
});

0 commit comments

Comments
 (0)