Skip to content

Commit ad50479

Browse files
committed
lint
1 parent bda2918 commit ad50479

2 files changed

Lines changed: 141 additions & 104 deletions

File tree

dev-packages/test-utils/src/mock-sentry-server.ts

Lines changed: 135 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import http from 'node:http';
1+
import { execFileSync } from 'node:child_process';
22
import fs from 'node:fs';
3-
import zlib from 'node:zlib';
3+
import http from 'node:http';
44
import path from 'node:path';
5-
import { execFileSync } from 'node:child_process';
6-
import type { RequestRecord } from './sourcemap-upload-assertions';
5+
import zlib from 'node:zlib';
6+
import type { ChunkFileRecord, RequestRecord } from './sourcemap-upload-assertions';
77

88
export interface MockSentryServerOptions {
99
port?: number;
@@ -32,16 +32,16 @@ function parseMultipartParts(body: Buffer, boundary: string): { headers: string;
3232
if (idx === -1) break;
3333

3434
const afterBoundary = idx + boundaryBuf.length;
35-
if (body.slice(afterBoundary, afterBoundary + 2).toString() === '--') break;
35+
if (body.subarray(afterBoundary, afterBoundary + 2).toString() === '--') break;
3636

3737
const headerEnd = body.indexOf('\r\n\r\n', afterBoundary);
3838
if (headerEnd === -1) break;
3939

40-
const headerStr = body.slice(afterBoundary, headerEnd).toString();
40+
const headerStr = body.subarray(afterBoundary, headerEnd).toString();
4141

4242
const nextBoundary = body.indexOf(boundaryBuf, headerEnd + 4);
4343
const contentEnd = nextBoundary !== -1 ? nextBoundary - 2 : body.length;
44-
const content = body.slice(headerEnd + 4, contentEnd);
44+
const content = body.subarray(headerEnd + 4, contentEnd);
4545

4646
parts.push({ headers: headerStr, content });
4747
start = nextBoundary !== -1 ? nextBoundary : body.length;
@@ -50,6 +50,130 @@ function parseMultipartParts(body: Buffer, boundary: string): { headers: string;
5050
return parts;
5151
}
5252

53+
/**
54+
* Extract and inspect a single multipart chunk: decompress, unzip, read manifest.
55+
*/
56+
function extractChunkPart(partContent: Buffer, outputDir: string, chunkIndex: number, partIndex: number): ChunkFileRecord {
57+
const bundleDir = path.join(outputDir, `bundle_${chunkIndex}_${partIndex}`);
58+
59+
// Try to decompress (sentry-cli gzips chunks)
60+
let zipBuffer: Buffer;
61+
try {
62+
zipBuffer = zlib.gunzipSync(partContent);
63+
} catch {
64+
zipBuffer = partContent;
65+
}
66+
67+
const zipFile = `${bundleDir}.zip`;
68+
fs.writeFileSync(zipFile, zipBuffer);
69+
70+
// Extract the zip to inspect contents
71+
try {
72+
fs.mkdirSync(bundleDir, { recursive: true });
73+
execFileSync('unzip', ['-q', '-o', zipFile, '-d', bundleDir], { stdio: 'ignore' });
74+
75+
// Read manifest.json if present
76+
const manifestPath = path.join(bundleDir, 'manifest.json');
77+
if (fs.existsSync(manifestPath)) {
78+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { files?: Record<string, unknown> };
79+
return {
80+
bundleDir,
81+
manifest: manifest as ChunkFileRecord['manifest'],
82+
fileCount: Object.keys(manifest.files || {}).length,
83+
};
84+
}
85+
return { bundleDir, note: 'no manifest.json found' };
86+
} catch (err: unknown) {
87+
return {
88+
zipFile,
89+
note: `extraction failed: ${err instanceof Error ? err.message : String(err)}`,
90+
};
91+
}
92+
}
93+
94+
/**
95+
* Process a chunk upload POST request: parse multipart body, extract each part.
96+
*/
97+
function processChunkUpload(
98+
record: RequestRecord,
99+
body: Buffer,
100+
contentType: string,
101+
outputDir: string,
102+
chunkIndex: number,
103+
): number {
104+
record.hasBody = true;
105+
record.chunkFiles = [];
106+
107+
const boundaryMatch = contentType.match(/boundary=(.+)/);
108+
if (!boundaryMatch) {
109+
return chunkIndex;
110+
}
111+
112+
// boundaryMatch[1] is guaranteed to exist since the regex matched
113+
const parts = parseMultipartParts(body, boundaryMatch[1] as string);
114+
let nextChunkIndex = chunkIndex;
115+
for (let i = 0; i < parts.length; i++) {
116+
// parts[i] is guaranteed to exist within the loop bounds
117+
const part = parts[i] as { headers: string; content: Buffer };
118+
record.chunkFiles.push(extractChunkPart(part.content, outputDir, nextChunkIndex, i));
119+
nextChunkIndex++;
120+
}
121+
122+
return nextChunkIndex;
123+
}
124+
125+
/**
126+
* Send the appropriate mock response based on the request URL.
127+
*/
128+
function sendResponse(
129+
req: http.IncomingMessage,
130+
res: http.ServerResponse,
131+
port: number,
132+
org: string,
133+
): void {
134+
const url = req.url || '';
135+
136+
if (url.includes('/artifactbundle/assemble/')) {
137+
res.writeHead(200, { 'Content-Type': 'application/json' });
138+
res.end(JSON.stringify({ state: 'created', missingChunks: [] }));
139+
} else if (url.includes('/chunk-upload/')) {
140+
if (req.method === 'GET') {
141+
res.writeHead(200, { 'Content-Type': 'application/json' });
142+
res.end(
143+
JSON.stringify({
144+
url: `http://localhost:${port}/api/0/organizations/${org}/chunk-upload/`,
145+
chunkSize: 8388608,
146+
chunksPerRequest: 64,
147+
maxFileSize: 2147483648,
148+
maxRequestSize: 33554432,
149+
concurrency: 1,
150+
hashAlgorithm: 'sha1',
151+
compression: ['gzip'],
152+
accept: [
153+
'debug_files',
154+
'release_files',
155+
'pdbs',
156+
'sources',
157+
'bcsymbolmaps',
158+
'il2cpp',
159+
'portablepdbs',
160+
'artifact_bundles',
161+
],
162+
}),
163+
);
164+
} else {
165+
res.writeHead(200, { 'Content-Type': 'application/json' });
166+
res.end(JSON.stringify({}));
167+
}
168+
} else if (url.includes('/releases/')) {
169+
res.writeHead(201, { 'Content-Type': 'application/json' });
170+
res.end(JSON.stringify({ version: 'test-release', dateCreated: new Date().toISOString() }));
171+
} else {
172+
res.writeHead(200, { 'Content-Type': 'application/json' });
173+
res.end(JSON.stringify({ ok: true }));
174+
}
175+
}
176+
53177
/**
54178
* Starts a mock Sentry server that captures sourcemap upload requests.
55179
*
@@ -91,54 +215,7 @@ export function startMockSentryServer(options: MockSentryServerOptions = {}): Mo
91215

92216
// For chunk upload POSTs, save and extract artifact bundles
93217
if (req.url?.includes('chunk-upload') && req.method === 'POST' && body.length > 0) {
94-
record.hasBody = true;
95-
record.chunkFiles = [];
96-
97-
const boundaryMatch = contentType.match(/boundary=(.+)/);
98-
if (boundaryMatch) {
99-
const parts = parseMultipartParts(body, boundaryMatch[1]!);
100-
for (let i = 0; i < parts.length; i++) {
101-
const part = parts[i]!;
102-
const bundleDir = path.join(outputDir, `bundle_${chunkIndex}_${i}`);
103-
104-
// Try to decompress (sentry-cli gzips chunks)
105-
let zipBuffer: Buffer;
106-
try {
107-
zipBuffer = zlib.gunzipSync(part.content);
108-
} catch {
109-
zipBuffer = part.content;
110-
}
111-
112-
const zipFile = `${bundleDir}.zip`;
113-
fs.writeFileSync(zipFile, zipBuffer);
114-
115-
// Extract the zip to inspect contents
116-
try {
117-
fs.mkdirSync(bundleDir, { recursive: true });
118-
execFileSync('unzip', ['-q', '-o', zipFile, '-d', bundleDir], { stdio: 'ignore' });
119-
120-
// Read manifest.json if present
121-
const manifestPath = path.join(bundleDir, 'manifest.json');
122-
if (fs.existsSync(manifestPath)) {
123-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
124-
record.chunkFiles.push({
125-
bundleDir,
126-
manifest,
127-
fileCount: Object.keys(manifest.files || {}).length,
128-
});
129-
} else {
130-
record.chunkFiles.push({ bundleDir, note: 'no manifest.json found' });
131-
}
132-
} catch (err: unknown) {
133-
record.chunkFiles.push({
134-
zipFile,
135-
note: `extraction failed: ${err instanceof Error ? err.message : String(err)}`,
136-
});
137-
}
138-
139-
chunkIndex++;
140-
}
141-
}
218+
chunkIndex = processChunkUpload(record, body, contentType, outputDir, chunkIndex);
142219
}
143220

144221
// For artifact bundle assemble, capture the request body
@@ -155,54 +232,12 @@ export function startMockSentryServer(options: MockSentryServerOptions = {}): Mo
155232
// Write all collected requests to the output file after each request
156233
fs.writeFileSync(outputFile, JSON.stringify(requests, null, 2));
157234

158-
// Route responses — order matters: most specific first
159-
const url = req.url || '';
160-
161-
if (url.includes('/artifactbundle/assemble/')) {
162-
res.writeHead(200, { 'Content-Type': 'application/json' });
163-
res.end(JSON.stringify({ state: 'created', missingChunks: [] }));
164-
} else if (url.includes('/chunk-upload/')) {
165-
if (req.method === 'GET') {
166-
res.writeHead(200, { 'Content-Type': 'application/json' });
167-
res.end(
168-
JSON.stringify({
169-
url: `http://localhost:${port}/api/0/organizations/${org}/chunk-upload/`,
170-
chunkSize: 8388608,
171-
chunksPerRequest: 64,
172-
maxFileSize: 2147483648,
173-
maxRequestSize: 33554432,
174-
concurrency: 1,
175-
hashAlgorithm: 'sha1',
176-
compression: ['gzip'],
177-
accept: [
178-
'debug_files',
179-
'release_files',
180-
'pdbs',
181-
'sources',
182-
'bcsymbolmaps',
183-
'il2cpp',
184-
'portablepdbs',
185-
'artifact_bundles',
186-
],
187-
}),
188-
);
189-
} else {
190-
res.writeHead(200, { 'Content-Type': 'application/json' });
191-
res.end(JSON.stringify({}));
192-
}
193-
} else if (url.includes('/releases/')) {
194-
res.writeHead(201, { 'Content-Type': 'application/json' });
195-
res.end(JSON.stringify({ version: 'test-release', dateCreated: new Date().toISOString() }));
196-
} else {
197-
res.writeHead(200, { 'Content-Type': 'application/json' });
198-
res.end(JSON.stringify({ ok: true }));
199-
}
235+
sendResponse(req, res, port, org);
200236
});
201237
});
202238

203-
server.listen(port, () => {
204-
console.log(`Mock Sentry server listening on port ${port}`);
205-
});
239+
// eslint-disable-next-line no-console
240+
server.listen(port, () => console.log(`Mock Sentry server listening on port ${port}`));
206241

207242
return {
208243
port,

dev-packages/test-utils/src/sourcemap-upload-assertions.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import * as assert from 'assert/strict';
12
import * as fs from 'fs';
23
import * as path from 'path';
3-
import * as assert from 'assert/strict';
44

55
export interface ManifestFile {
66
type: 'minified_source' | 'source_map';
@@ -133,7 +133,7 @@ export function assertDebugIdPairs(manifests: ArtifactBundleData[]): DebugIdPair
133133
'Expected at least one JS/sourcemap pair with matching debug IDs in the uploaded artifact bundles',
134134
);
135135

136-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
136+
const uuidRegex = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
137137
for (const pair of debugIdPairs) {
138138
assert.match(pair.debugId, uuidRegex, `Expected debug ID to be a valid UUID, got: ${pair.debugId}`);
139139
}
@@ -207,16 +207,18 @@ export function assertSourcemapMappings(manifests: ArtifactBundleData[]): void {
207207
/**
208208
* Assert a sourcemap references source files matching a pattern.
209209
*/
210-
export function assertSourcemapSources(manifests: ArtifactBundleData[], sourcePattern: string | RegExp): void {
211-
const regex = typeof sourcePattern === 'string' ? new RegExp(sourcePattern) : sourcePattern;
210+
export function assertSourcemapSources(manifests: ArtifactBundleData[], sourcePattern: RegExp): void {
211+
const regex = sourcePattern;
212212
let found = false;
213213

214214
forEachSourcemap(manifests, ({ url, sourcemap }) => {
215215
if (sourcemap.sources?.some(s => regex.test(s))) {
216216
found = true;
217217

218+
// eslint-disable-next-line no-console
218219
console.log(`Sourcemap ${url} references app sources:`);
219220
for (const src of sourcemap.sources.filter(s => regex.test(s))) {
221+
// eslint-disable-next-line no-console
220222
console.log(` - ${src}`);
221223
}
222224

0 commit comments

Comments
 (0)