Skip to content

Commit c4b2bdd

Browse files
add tests
1 parent 539892c commit c4b2bdd

File tree

3 files changed

+223
-10
lines changed

3 files changed

+223
-10
lines changed

packages/core/jest.config.tools.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
collectCoverage: true,
33
preset: 'ts-jest',
4-
setupFilesAfterEnv: ['<rootDir>/test/mockConsole.ts'],
4+
setupFilesAfterEnv: ['jest-extended/all', '<rootDir>/test/mockConsole.ts'],
55
globals: {
66
__DEV__: true,
77
},

packages/core/src/js/tools/sentryOptionsSerializer.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import * as path from 'path';
1+
import { logger } from '@sentry/core';
22
import * as fs from 'fs';
3-
import { MetroConfig, Module } from 'metro';
4-
import { createSet, MetroCustomSerializer, VirtualJSOutput } from './utils';
3+
import type { MetroConfig, Module } from 'metro';
54
// eslint-disable-next-line import/no-extraneous-dependencies
65
import * as countLines from 'metro/src/lib/countLines';
7-
import { logger } from '@sentry/core';
6+
import * as path from 'path';
7+
8+
import type { MetroCustomSerializer, VirtualJSOutput } from './utils';
9+
import { createSet } from './utils';
810

911
const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json';
1012

@@ -23,10 +25,12 @@ export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: stri
2325
return config;
2426
}
2527

26-
const optionsPath =
27-
typeof optionsFile === 'string'
28-
? path.join(projectRoot, optionsFile)
29-
: path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME);
28+
let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME);
29+
if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) {
30+
optionsPath = optionsFile;
31+
} else if (typeof optionsFile === 'string') {
32+
optionsPath = path.join(projectRoot, optionsFile);
33+
}
3034

3135
const originalSerializer = config.serializer?.customSerializer;
3236
if (!originalSerializer) {
@@ -78,7 +82,7 @@ function createSentryOptionsModule(filePath: string): Module<VirtualJSOutput> |
7882
}
7983

8084
const minifiedContent = JSON.stringify(parsedContent);
81-
let optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`;
85+
const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`;
8286

8387
logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`);
8488
return {
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { logger } from '@sentry/core';
2+
import * as fs from 'fs';
3+
import type { Graph, Module, SerializerOptions } from 'metro';
4+
5+
import { withSentryOptionsFromFile } from '../../src/js/tools/sentryOptionsSerializer';
6+
import { createSet } from '../../src/js/tools/utils';
7+
8+
jest.mock('fs', () => ({
9+
readFileSync: jest.fn(),
10+
}));
11+
12+
const consoleErrorSpy = jest.spyOn(console, 'error');
13+
const loggerDebugSpy = jest.spyOn(logger, 'debug');
14+
const loggerErrorSpy = jest.spyOn(logger, 'error');
15+
16+
const customSerializerMock = jest.fn();
17+
let mockedPreModules: Module[] = [];
18+
19+
describe('Sentry Options Serializer', () => {
20+
beforeEach(() => {
21+
jest.resetAllMocks();
22+
mockedPreModules = createMockedPreModules();
23+
});
24+
25+
afterAll(() => {
26+
jest.clearAllMocks();
27+
});
28+
29+
test('returns original config if optionsFile is false', () => {
30+
const config = () => ({
31+
projectRoot: '/test',
32+
serializer: {
33+
customSerializer: customSerializerMock,
34+
},
35+
});
36+
37+
const result = withSentryOptionsFromFile(config(), false);
38+
expect(result).toEqual(config());
39+
});
40+
41+
test('logs error and returns original config if projectRoot is missing', () => {
42+
const config = () => ({
43+
serializer: {
44+
customSerializer: customSerializerMock,
45+
},
46+
});
47+
48+
const result = withSentryOptionsFromFile(config(), true);
49+
50+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Project root is required'));
51+
expect(result).toEqual(config());
52+
});
53+
54+
test('logs error and returns original config if customSerializer is missing', () => {
55+
const config = () => ({
56+
projectRoot: '/test',
57+
serializer: {},
58+
});
59+
const consoleErrorSpy = jest.spyOn(console, 'error');
60+
61+
const result = withSentryOptionsFromFile(config(), true);
62+
63+
expect(consoleErrorSpy).toHaveBeenCalledWith(
64+
expect.stringContaining('`config.serializer.customSerializer` is required'),
65+
);
66+
expect(result).toEqual(config());
67+
});
68+
69+
test('adds sentry options module when file exists and is valid JSON', () => {
70+
const config = () => ({
71+
projectRoot: '/test',
72+
serializer: {
73+
customSerializer: customSerializerMock,
74+
},
75+
});
76+
77+
const mockOptions = { test: 'value' };
78+
(fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockOptions));
79+
80+
const actualConfig = withSentryOptionsFromFile(config(), true);
81+
actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null);
82+
83+
expect(mockedPreModules).toHaveLength(2);
84+
expect(mockedPreModules.at(-1)).toEqual(
85+
expect.objectContaining({
86+
getSource: expect.any(Function),
87+
path: '__sentry-options__',
88+
output: [
89+
{
90+
type: 'js/script/virtual',
91+
data: {
92+
code: 'var __SENTRY_OPTIONS__={"test":"value"};',
93+
lineCount: 1,
94+
map: [],
95+
},
96+
},
97+
],
98+
}),
99+
);
100+
expect(mockedPreModules.at(-1).getSource().toString()).toEqual(mockedPreModules.at(-1).output[0].data.code);
101+
expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options added to the bundle'));
102+
});
103+
104+
test('logs error and does not add module when file does not exist', () => {
105+
const config = () => ({
106+
projectRoot: '/test',
107+
serializer: {
108+
customSerializer: customSerializerMock,
109+
},
110+
});
111+
112+
(fs.readFileSync as jest.Mock).mockImplementation(() => {
113+
throw { code: 'ENOENT' };
114+
});
115+
116+
const actualConfig = withSentryOptionsFromFile(config(), true);
117+
actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null);
118+
119+
expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options file does not exist'));
120+
expect(mockedPreModules).toMatchObject(createMockedPreModules());
121+
});
122+
123+
test('logs error and does not add module when file contains invalid JSON', () => {
124+
const config = () => ({
125+
projectRoot: '/test',
126+
serializer: {
127+
customSerializer: customSerializerMock,
128+
},
129+
});
130+
131+
(fs.readFileSync as jest.Mock).mockReturnValue('invalid json');
132+
133+
const actualConfig = withSentryOptionsFromFile(config(), true);
134+
actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null);
135+
136+
expect(loggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse Sentry options file'));
137+
expect(mockedPreModules).toMatchObject(createMockedPreModules());
138+
});
139+
140+
test('calls original serializer with correct arguments and returns its result', () => {
141+
const mockedEntryPoint = 'entryPoint';
142+
const mockedGraph: Graph = jest.fn() as unknown as Graph;
143+
const mockedOptions: SerializerOptions = jest.fn() as unknown as SerializerOptions;
144+
const mockedResult = {};
145+
const originalSerializer = jest.fn().mockReturnValue(mockedResult);
146+
147+
const actualConfig = withSentryOptionsFromFile(
148+
{
149+
projectRoot: '/test',
150+
serializer: {
151+
customSerializer: originalSerializer,
152+
},
153+
},
154+
true,
155+
);
156+
const actualResult = actualConfig.serializer?.customSerializer(
157+
mockedEntryPoint,
158+
mockedPreModules,
159+
mockedGraph,
160+
mockedOptions,
161+
);
162+
163+
expect(originalSerializer).toHaveBeenCalledWith(mockedEntryPoint, mockedPreModules, mockedGraph, mockedOptions);
164+
expect(actualResult).toEqual(mockedResult);
165+
});
166+
167+
test('uses custom file path when optionsFile is a string', () => {
168+
const config = () => ({
169+
projectRoot: '/test',
170+
serializer: {
171+
customSerializer: customSerializerMock,
172+
},
173+
});
174+
175+
withSentryOptionsFromFile(config(), 'custom/path.json').serializer?.customSerializer(
176+
null,
177+
mockedPreModules,
178+
null,
179+
null,
180+
);
181+
withSentryOptionsFromFile(config(), '/absolute/path.json').serializer?.customSerializer(
182+
null,
183+
mockedPreModules,
184+
null,
185+
null,
186+
);
187+
188+
expect(fs.readFileSync).toHaveBeenCalledWith('/test/custom/path.json', expect.anything());
189+
expect(fs.readFileSync).toHaveBeenCalledWith('/absolute/path.json', expect.anything());
190+
});
191+
});
192+
193+
function createMockedPreModules(): Module[] {
194+
return [createMinimalModule()];
195+
}
196+
197+
function createMinimalModule(): Module {
198+
return {
199+
dependencies: new Map(),
200+
getSource: getEmptySource,
201+
inverseDependencies: createSet(),
202+
path: '__sentry-options__',
203+
output: [],
204+
};
205+
}
206+
207+
function getEmptySource(): Buffer {
208+
return Buffer.from('');
209+
}

0 commit comments

Comments
 (0)