Skip to content

Commit cefdf83

Browse files
vdiezclaude
andcommitted
Refactor DI to use centralized dependency container and remove sinon
- Create src/deps.ts with centralized Dependencies interface and getDeps()/setDeps()/resetDeps() functions for dependency injection - Migrate all source files to use getDeps() instead of function parameters - Create test/unit/test-helpers.ts with reusable mock factories - Update all unit tests to use setDeps() in beforeEach and resetDeps() in afterEach instead of passing deps as function arguments - Remove sinon dependency, replacing all stubs with native node:test mocks - Fix TypeScript errors by using proper Mock<T> typing from node:test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a1f6fab commit cefdf83

20 files changed

+1355
-1279
lines changed

package-lock.json

Lines changed: 0 additions & 38 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,8 @@
3333
"@types/node-forge": "1.3.14",
3434
"@types/proxy-from-env": "1.0.4",
3535
"@types/semver": "7.7.1",
36-
"@types/sinon": "21.0.0",
3736
"@types/tar-stream": "3.1.4",
3837
"@typescript-eslint/parser": "8.52.0",
39-
"axios-mock-adapter": "2.1.0",
4038
"eslint": "9.39.2",
4139
"eslint-plugin-notice": "1.0.0",
4240
"husky": "9.1.7",
@@ -45,7 +43,6 @@
4543
"nyc": "17.1.0",
4644
"prettier": "3.7.4",
4745
"pretty-quick": "4.2.2",
48-
"sinon": "21.0.1",
4946
"toml": "3.0.0",
5047
"tsx": "4.21.0",
5148
"typescript": "5.9.3"

src/deps.ts

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* sonar-scanner-npm
3+
* Copyright (C) 2022-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import { exec, spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from 'node:child_process';
22+
import fs from 'node:fs';
23+
import util from 'node:util';
24+
import { download as defaultDownload, fetch as defaultFetch } from './request';
25+
import type { ScannerProperties, ScanOptions } from './types';
26+
27+
const execAsync = util.promisify(exec);
28+
29+
// Re-export spawn type (use typeof for compatibility with complex overloads)
30+
export type SpawnFn = typeof nodeSpawn;
31+
32+
/**
33+
* Exec async function type for executing shell commands
34+
*/
35+
export type ExecAsyncFn = (command: string) => Promise<{ stdout: string; stderr: string }>;
36+
37+
/**
38+
* High-level function types for scan orchestration
39+
*/
40+
export type ServerSupportsJREProvisioningFn = (properties: ScannerProperties) => Promise<boolean>;
41+
export type FetchJREFn = (properties: ScannerProperties) => Promise<string>;
42+
export type DownloadScannerCliFn = (properties: ScannerProperties) => Promise<string>;
43+
export type RunScannerCliFn = (
44+
scanOptions: ScanOptions,
45+
properties: ScannerProperties,
46+
binPath: string,
47+
) => Promise<void>;
48+
export type FetchScannerEngineFn = (properties: ScannerProperties) => Promise<string>;
49+
export type RunScannerEngineFn = (
50+
javaPath: string,
51+
scannerEnginePath: string,
52+
scanOptions: ScanOptions,
53+
properties: ScannerProperties,
54+
) => Promise<void>;
55+
export type LocateExecutableFromPathFn = (executable: string) => Promise<string | null>;
56+
57+
/**
58+
* File utility function type
59+
*/
60+
export type ExtractArchiveFn = (archivePath: string, destPath: string) => Promise<void>;
61+
62+
/**
63+
* Centralized dependency container for all injectable dependencies.
64+
* This allows for easy mocking in tests while keeping function signatures clean.
65+
*/
66+
export interface Dependencies {
67+
// File system operations
68+
fs: {
69+
existsSync: typeof fs.existsSync;
70+
readFileSync: typeof fs.readFileSync;
71+
readFile: typeof fs.readFile;
72+
mkdirSync: typeof fs.mkdirSync;
73+
createReadStream: typeof fs.createReadStream;
74+
createWriteStream: typeof fs.createWriteStream;
75+
remove: (path: string) => Promise<void>;
76+
writeFile: (path: string, data: string) => Promise<void>;
77+
exists: (path: string) => Promise<boolean>;
78+
ensureDir: (path: string) => Promise<void>;
79+
};
80+
// Process information
81+
process: {
82+
platform: NodeJS.Platform;
83+
arch: NodeJS.Architecture;
84+
env: NodeJS.ProcessEnv;
85+
cwd: () => string;
86+
};
87+
// HTTP operations
88+
http: {
89+
fetch: typeof defaultFetch;
90+
download: typeof defaultDownload;
91+
};
92+
// Spawning child processes
93+
spawn: SpawnFn;
94+
// Executing shell commands
95+
execAsync: ExecAsyncFn;
96+
// High-level scan orchestration functions (lazily initialized to avoid circular deps)
97+
scan: {
98+
serverSupportsJREProvisioning: ServerSupportsJREProvisioningFn;
99+
fetchJRE: FetchJREFn;
100+
downloadScannerCli: DownloadScannerCliFn;
101+
runScannerCli: RunScannerCliFn;
102+
fetchScannerEngine: FetchScannerEngineFn;
103+
runScannerEngine: RunScannerEngineFn;
104+
locateExecutableFromPath: LocateExecutableFromPathFn;
105+
};
106+
// File utilities
107+
file: {
108+
extractArchive: ExtractArchiveFn;
109+
};
110+
}
111+
112+
// Lazy-loaded modules to avoid circular dependencies
113+
let javaModule: typeof import('./java') | null = null;
114+
let scannerCliModule: typeof import('./scanner-cli') | null = null;
115+
let scannerEngineModule: typeof import('./scanner-engine') | null = null;
116+
let processModule: typeof import('./process') | null = null;
117+
let fileModule: typeof import('./file') | null = null;
118+
119+
async function getJavaModule() {
120+
if (!javaModule) {
121+
javaModule = await import('./java');
122+
}
123+
return javaModule;
124+
}
125+
126+
async function getScannerCliModule() {
127+
if (!scannerCliModule) {
128+
scannerCliModule = await import('./scanner-cli');
129+
}
130+
return scannerCliModule;
131+
}
132+
133+
async function getScannerEngineModule() {
134+
if (!scannerEngineModule) {
135+
scannerEngineModule = await import('./scanner-engine');
136+
}
137+
return scannerEngineModule;
138+
}
139+
140+
async function getProcessModule() {
141+
if (!processModule) {
142+
processModule = await import('./process');
143+
}
144+
return processModule;
145+
}
146+
147+
async function getFileModule() {
148+
if (!fileModule) {
149+
fileModule = await import('./file');
150+
}
151+
return fileModule;
152+
}
153+
154+
/**
155+
* Creates the default dependencies using real implementations.
156+
*/
157+
function createDefaultDeps(): Dependencies {
158+
return {
159+
fs: {
160+
existsSync: fs.existsSync,
161+
readFileSync: fs.readFileSync,
162+
readFile: fs.readFile,
163+
mkdirSync: fs.mkdirSync,
164+
createReadStream: fs.createReadStream,
165+
createWriteStream: fs.createWriteStream,
166+
remove: (filePath: string) => fs.promises.rm(filePath, { recursive: true, force: true }),
167+
writeFile: (filePath: string, data: string) => fs.promises.writeFile(filePath, data),
168+
exists: async (filePath: string) => {
169+
try {
170+
await fs.promises.access(filePath);
171+
return true;
172+
} catch {
173+
return false;
174+
}
175+
},
176+
ensureDir: (dirPath: string) =>
177+
fs.promises.mkdir(dirPath, { recursive: true }).then(() => {}),
178+
},
179+
process: {
180+
get platform() {
181+
return process.platform;
182+
},
183+
get arch() {
184+
return process.arch;
185+
},
186+
get env() {
187+
return process.env;
188+
},
189+
cwd: () => process.cwd(),
190+
},
191+
http: {
192+
fetch: defaultFetch,
193+
download: defaultDownload,
194+
},
195+
spawn: nodeSpawn,
196+
execAsync,
197+
scan: {
198+
serverSupportsJREProvisioning: async properties => {
199+
const mod = await getJavaModule();
200+
return mod.serverSupportsJREProvisioning(properties);
201+
},
202+
fetchJRE: async properties => {
203+
const mod = await getJavaModule();
204+
return mod.fetchJRE(properties);
205+
},
206+
downloadScannerCli: async properties => {
207+
const mod = await getScannerCliModule();
208+
return mod.downloadScannerCli(properties);
209+
},
210+
runScannerCli: async (scanOptions, properties, binPath) => {
211+
const mod = await getScannerCliModule();
212+
return mod.runScannerCli(scanOptions, properties, binPath);
213+
},
214+
fetchScannerEngine: async properties => {
215+
const mod = await getScannerEngineModule();
216+
return mod.fetchScannerEngine(properties);
217+
},
218+
runScannerEngine: async (javaPath, scannerEnginePath, scanOptions, properties) => {
219+
const mod = await getScannerEngineModule();
220+
return mod.runScannerEngine(javaPath, scannerEnginePath, scanOptions, properties);
221+
},
222+
locateExecutableFromPath: async executable => {
223+
const mod = await getProcessModule();
224+
return mod.locateExecutableFromPath(executable);
225+
},
226+
},
227+
file: {
228+
extractArchive: async (archivePath, destPath) => {
229+
const mod = await getFileModule();
230+
return mod.extractArchive(archivePath, destPath);
231+
},
232+
},
233+
};
234+
}
235+
236+
// Module-level dependency container
237+
let deps = createDefaultDeps();
238+
239+
/**
240+
* Get the current dependency container.
241+
* Internal functions use this to access dependencies.
242+
*/
243+
export function getDeps(): Dependencies {
244+
return deps;
245+
}
246+
247+
/**
248+
* Set/override dependencies. Used for testing.
249+
* Merges the provided partial dependencies with the defaults.
250+
*
251+
* @param newDeps - Partial dependencies to override
252+
*/
253+
export function setDeps(newDeps: Partial<Dependencies>): void {
254+
const defaults = createDefaultDeps();
255+
deps = {
256+
...defaults,
257+
...newDeps,
258+
// Deep merge nested objects
259+
fs: {
260+
...defaults.fs,
261+
...newDeps.fs,
262+
},
263+
process: {
264+
...defaults.process,
265+
...newDeps.process,
266+
},
267+
http: {
268+
...defaults.http,
269+
...newDeps.http,
270+
},
271+
scan: {
272+
...defaults.scan,
273+
...newDeps.scan,
274+
},
275+
file: {
276+
...defaults.file,
277+
...newDeps.file,
278+
},
279+
};
280+
}
281+
282+
/**
283+
* Reset dependencies to defaults. Should be called in afterEach() in tests.
284+
*/
285+
export function resetDeps(): void {
286+
deps = createDefaultDeps();
287+
}

0 commit comments

Comments
 (0)