2121// Integration tests for `sonar integrate git`
2222
2323import { afterEach , beforeEach , describe , expect , it } from 'bun:test' ;
24- import { mkdirSync , writeFileSync } from 'node:fs' ;
24+ import { mkdirSync , symlinkSync , writeFileSync } from 'node:fs' ;
2525import { join } from 'node:path' ;
2626import { TestHarness } from '../../harness' ;
27+ import { BINARY_PATH } from '../../harness/cli-runner.js' ;
28+
29+ const PATH_DELIM = process . platform === 'win32' ? ';' : ':' ;
30+ function pathWithoutNodeModules ( envPath : string | undefined ) : string {
31+ return ( envPath ?? '' )
32+ . split ( PATH_DELIM )
33+ . filter ( ( p ) => ! p . includes ( 'node_modules' ) )
34+ . join ( PATH_DELIM ) ;
35+ }
36+
37+ // Hardcoded test token — intentional fixture for secret detection in pre-commit hook test
38+ // sonar-ignore-next-line S6769
39+ const GITHUB_TEST_TOKEN = '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' ;
40+
41+ function buildHookEnv ( sonarBinDir : string ) : Record < string , string > {
42+ return {
43+ ...process . env ,
44+ PATH : `${ sonarBinDir } ${ PATH_DELIM } ${ pathWithoutNodeModules ( process . env . PATH ) } ` ,
45+ } ;
46+ }
47+
48+ function setupSonarBinDir ( harness : TestHarness ) : {
49+ sonarBinDir : string ;
50+ hookEnv : Record < string , string > ;
51+ } {
52+ const sonarBinDir = join ( harness . cwd . path , 'sonar-bin' ) ;
53+ mkdirSync ( sonarBinDir , { recursive : true } ) ;
54+ symlinkSync ( BINARY_PATH , join ( sonarBinDir , 'sonar' ) ) ;
55+ return { sonarBinDir, hookEnv : buildHookEnv ( sonarBinDir ) } ;
56+ }
57+
58+ function setupGitUser ( cwd : string ) : void {
59+ Bun . spawnSync ( [ 'git' , 'config' , 'user.email' , 'test@example.com' ] , { cwd } ) ;
60+ Bun . spawnSync ( [ 'git' , 'config' , 'user.name' , 'Test User' ] , { cwd } ) ;
61+ }
62+
63+ function addBareRemote ( cwd : string ) : void {
64+ const remotePath = join ( cwd , '..' , 'remote.git' ) ;
65+ mkdirSync ( remotePath , { recursive : true } ) ;
66+ Bun . spawnSync ( [ 'git' , 'init' , '--bare' ] , { cwd : remotePath } ) ;
67+ Bun . spawnSync ( [ 'git' , 'remote' , 'add' , 'origin' , remotePath ] , { cwd } ) ;
68+ Bun . spawnSync ( [ 'git' , 'branch' , '-M' , 'main' ] , { cwd } ) ;
69+ }
70+
71+ function gitCommit (
72+ cwd : string ,
73+ env : Record < string , string > ,
74+ message : string ,
75+ ) : ReturnType < typeof Bun . spawnSync > {
76+ return Bun . spawnSync ( [ 'git' , 'commit' , '-m' , message ] , {
77+ cwd,
78+ env,
79+ stdout : 'pipe' ,
80+ stderr : 'pipe' ,
81+ } ) ;
82+ }
83+
84+ function gitPush (
85+ cwd : string ,
86+ env : Record < string , string > ,
87+ setUpstream : boolean ,
88+ ) : ReturnType < typeof Bun . spawnSync > {
89+ const args = setUpstream
90+ ? [ 'git' , 'push' , '-u' , 'origin' , 'main' ]
91+ : [ 'git' , 'push' , 'origin' , 'main' ] ;
92+ return Bun . spawnSync ( args , { cwd, env, stdout : 'pipe' , stderr : 'pipe' } ) ;
93+ }
94+
95+ const INTEGRATION_TEST_TOKEN = 'test-token' ;
96+
97+ type SetupAuthOptions = { withSecretsBinary ?: boolean } ;
98+
99+ async function setupAuthenticated (
100+ harness : TestHarness ,
101+ options : SetupAuthOptions = { } ,
102+ ) : Promise < void > {
103+ const server = await harness . newFakeServer ( ) . withAuthToken ( INTEGRATION_TEST_TOKEN ) . start ( ) ;
104+ const chain = harness
105+ . state ( )
106+ . withActiveConnection ( server . baseUrl ( ) )
107+ . withKeychainToken ( server . baseUrl ( ) , INTEGRATION_TEST_TOKEN ) ;
108+ if ( options . withSecretsBinary ) {
109+ chain . withSecretsBinaryInstalled ( ) ;
110+ }
111+ }
112+
113+ function initGitRepo ( harness : TestHarness ) : void {
114+ mkdirSync ( harness . cwd . path , { recursive : true } ) ;
115+ Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
116+ }
117+
118+ function initGitRepoWithHusky ( harness : TestHarness ) : void {
119+ initGitRepo ( harness ) ;
120+ Bun . spawnSync ( [ 'git' , 'config' , 'core.hooksPath' , '.husky' ] , { cwd : harness . cwd . path } ) ;
121+ mkdirSync ( join ( harness . cwd . path , '.husky' ) , { recursive : true } ) ;
122+ }
123+
124+ function initGitRepoWithPreCommitConfig ( harness : TestHarness ) : void {
125+ initGitRepo ( harness ) ;
126+ harness . cwd . writeFile ( '.pre-commit-config.yaml' , 'repos: []\n' ) ;
127+ }
27128
28129describe ( 'integrate git (native hooks)' , ( ) => {
29130 let harness : TestHarness ;
@@ -39,11 +140,7 @@ describe('integrate git (native hooks)', () => {
39140 it (
40141 'exits with error when user cancels the hook-type selection' ,
41142 async ( ) => {
42- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
43- harness
44- . state ( )
45- . withActiveConnection ( server . baseUrl ( ) )
46- . withKeychainToken ( server . baseUrl ( ) , 'test-token' ) ;
143+ await setupAuthenticated ( harness ) ;
47144
48145 // Minimal git repo: findGitRoot() detects the .git directory
49146 harness . cwd . writeFile ( '.git/.keep' , '' ) ;
@@ -72,11 +169,7 @@ describe('integrate git (native hooks)', () => {
72169 it (
73170 'exits with error when run outside a git repository' ,
74171 async ( ) => {
75- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
76- harness
77- . state ( )
78- . withActiveConnection ( server . baseUrl ( ) )
79- . withKeychainToken ( server . baseUrl ( ) , 'test-token' ) ;
172+ await setupAuthenticated ( harness ) ;
80173
81174 // No .git directory — discoverProject() sets isGitRepo: false
82175 const result = await harness . run ( 'integrate git --non-interactive' ) ;
@@ -88,42 +181,70 @@ describe('integrate git (native hooks)', () => {
88181 ) ;
89182
90183 it (
91- 'installs pre-commit hook when user selects pre-commit (--non-interactive) ' ,
184+ 'pre-commit hook blocks commit when staged file contains a secret ' ,
92185 async ( ) => {
93- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
94- harness
95- . state ( )
96- . withActiveConnection ( server . baseUrl ( ) )
97- . withKeychainToken ( server . baseUrl ( ) , 'test-token' ) ;
186+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
187+ initGitRepo ( harness ) ;
98188
99- // Minimal git repo
100- harness . cwd . writeFile ( '.git/.keep' , '' ) ;
189+ const result = await harness . run ( 'integrate git --hook pre-commit --non-interactive' ) ;
190+ expect ( result . exitCode ) . toBe ( 0 ) ;
191+ expect ( harness . cwd . exists ( '.git' , 'hooks' , 'pre-commit' ) ) . toBe ( true ) ;
101192
102- // Fake binaries server so that ensureSonarSecrets() can download sonar-secrets
103- await harness . newFakeBinariesServer ( ) . start ( ) ;
193+ const { hookEnv } = setupSonarBinDir ( harness ) ;
194+ harness . cwd . writeFile ( 'secret.js' , `const token = "${ GITHUB_TEST_TOKEN } ";` ) ;
195+ Bun . spawnSync ( [ 'git' , 'add' , 'secret.js' ] , { cwd : harness . cwd . path } ) ;
196+ setupGitUser ( harness . cwd . path ) ;
104197
105- const result = await harness . run ( 'integrate git --hook pre-commit --non-interactive' ) ;
198+ const commit = gitCommit ( harness . cwd . path , hookEnv , 'wip' ) ;
199+ expect ( commit . exitCode ) . not . toBe ( 0 ) ;
200+ const output = ( commit . stdout ?. toString ( ) ?? '' ) + ( commit . stderr ?. toString ( ) ?? '' ) ;
201+ expect ( output ) . toContain ( 'Secrets found' ) ;
202+ } ,
203+ { timeout : 30000 } ,
204+ ) ;
205+
206+ it (
207+ 'pre-push hook blocks push when commit contains a secret' ,
208+ async ( ) => {
209+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
210+ initGitRepo ( harness ) ;
106211
212+ const result = await harness . run ( 'integrate git --hook pre-push --non-interactive' ) ;
107213 expect ( result . exitCode ) . toBe ( 0 ) ;
108- expect ( harness . cwd . exists ( '.git' , 'hooks' , 'pre-commit' ) ) . toBe ( true ) ;
214+ expect ( harness . cwd . exists ( '.git' , 'hooks' , 'pre-push' ) ) . toBe ( true ) ;
215+
216+ const { hookEnv } = setupSonarBinDir ( harness ) ;
217+ setupGitUser ( harness . cwd . path ) ;
218+
219+ // First commit + push: clean file, should succeed
220+ harness . cwd . writeFile ( 'clean.js' , 'const x = 1;\n' ) ;
221+ Bun . spawnSync ( [ 'git' , 'add' , 'clean.js' ] , { cwd : harness . cwd . path } ) ;
222+ gitCommit ( harness . cwd . path , hookEnv , 'initial' ) ;
223+ addBareRemote ( harness . cwd . path ) ;
224+ const firstPush = gitPush ( harness . cwd . path , hookEnv , true ) ;
225+ expect ( firstPush . exitCode ) . toBe ( 0 ) ;
226+
227+ // Second commit + push: file with secret, should be blocked by pre-push hook
228+ harness . cwd . writeFile ( 'secret.js' , `const token = "${ GITHUB_TEST_TOKEN } ";` ) ;
229+ Bun . spawnSync ( [ 'git' , 'add' , 'secret.js' ] , { cwd : harness . cwd . path } ) ;
230+ gitCommit ( harness . cwd . path , hookEnv , 'wip' ) ;
231+ const secondPush = gitPush ( harness . cwd . path , hookEnv , false ) ;
232+
233+ expect ( secondPush . exitCode ) . not . toBe ( 0 ) ;
234+ const output = ( secondPush . stdout ?. toString ( ) ?? '' ) + ( secondPush . stderr ?. toString ( ) ?? '' ) ;
235+ expect ( output ) . toContain ( 'Secrets found' ) ;
109236 } ,
110237 { timeout : 30000 } ,
111238 ) ;
112239
113240 it (
114241 'installs native pre-commit hook via interactive prompts when secrets is already installed' ,
115242 async ( ) => {
116- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
117- harness
118- . state ( )
119- . withActiveConnection ( server . baseUrl ( ) )
120- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
121- . withSecretsBinaryInstalled ( ) ;
243+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
122244
123245 // Real git repo so that git commands (e.g. git config core.hooksPath) behave correctly
124246 // and resolveGitHooksDir() resolves to .git/hooks as expected
125- mkdirSync ( harness . cwd . path , { recursive : true } ) ;
126- Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
247+ initGitRepo ( harness ) ;
127248
128249 // Two separate stdin chunks with a delay between them so readline doesn't buffer
129250 // both at once: 'y' confirms 'Install here?', then '\r' selects pre-commit
@@ -139,15 +260,8 @@ describe('integrate git (native hooks)', () => {
139260 it (
140261 'installs native pre-push hook via interactive prompts when secrets is already installed' ,
141262 async ( ) => {
142- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
143- harness
144- . state ( )
145- . withActiveConnection ( server . baseUrl ( ) )
146- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
147- . withSecretsBinaryInstalled ( ) ;
148-
149- mkdirSync ( harness . cwd . path , { recursive : true } ) ;
150- Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
263+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
264+ initGitRepo ( harness ) ;
151265
152266 // 'y' confirms 'Install here?'; '\x1b[B' moves the selection down to pre-push; '\r' submits
153267 const result = await harness . run ( 'integrate git' , {
@@ -164,12 +278,7 @@ describe('integrate git (native hooks)', () => {
164278 it (
165279 'installs native global pre-commit hook via interactive prompts when secrets is already installed' ,
166280 async ( ) => {
167- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
168- harness
169- . state ( )
170- . withActiveConnection ( server . baseUrl ( ) )
171- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
172- . withSecretsBinaryInstalled ( ) ;
281+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
173282
174283 // Two separate stdin chunks with a delay between them so readline doesn't buffer
175284 // both at once: 'y' confirms global hook warning, then '\r' selects pre-commit
@@ -185,12 +294,7 @@ describe('integrate git (native hooks)', () => {
185294 it (
186295 'installs native global pre-push hook via interactive prompts when secrets is already installed' ,
187296 async ( ) => {
188- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
189- harness
190- . state ( )
191- . withActiveConnection ( server . baseUrl ( ) )
192- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
193- . withSecretsBinaryInstalled ( ) ;
297+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
194298
195299 // Two separate stdin chunks with a delay between them so readline doesn't buffer
196300 // both at once: 'y' confirms global hook warning, then '\r' selects pre-push
@@ -220,17 +324,8 @@ describe('integrate git (husky)', () => {
220324 it (
221325 'installs pre-commit hook via husky when core.hooksPath is .husky' ,
222326 async ( ) => {
223- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
224- harness
225- . state ( )
226- . withActiveConnection ( server . baseUrl ( ) )
227- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
228- . withSecretsBinaryInstalled ( ) ;
229-
230- mkdirSync ( harness . cwd . path , { recursive : true } ) ;
231- Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
232- Bun . spawnSync ( [ 'git' , 'config' , 'core.hooksPath' , '.husky' ] , { cwd : harness . cwd . path } ) ;
233- mkdirSync ( join ( harness . cwd . path , '.husky' ) , { recursive : true } ) ;
327+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
328+ initGitRepoWithHusky ( harness ) ;
234329
235330 const result = await harness . run ( 'integrate git --hook pre-commit --non-interactive' ) ;
236331
@@ -246,17 +341,8 @@ describe('integrate git (husky)', () => {
246341 it (
247342 'installs pre-push hook via husky when core.hooksPath is .husky' ,
248343 async ( ) => {
249- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
250- harness
251- . state ( )
252- . withActiveConnection ( server . baseUrl ( ) )
253- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
254- . withSecretsBinaryInstalled ( ) ;
255-
256- mkdirSync ( harness . cwd . path , { recursive : true } ) ;
257- Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
258- Bun . spawnSync ( [ 'git' , 'config' , 'core.hooksPath' , '.husky' ] , { cwd : harness . cwd . path } ) ;
259- mkdirSync ( join ( harness . cwd . path , '.husky' ) , { recursive : true } ) ;
344+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
345+ initGitRepoWithHusky ( harness ) ;
260346
261347 const result = await harness . run ( 'integrate git --hook pre-push --non-interactive' ) ;
262348
@@ -293,16 +379,8 @@ describe('integrate git (pre-commit framework)', () => {
293379 it (
294380 'installs pre-commit hook via pre-commit framework when .pre-commit-config.yaml exists' ,
295381 async ( ) => {
296- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
297- harness
298- . state ( )
299- . withActiveConnection ( server . baseUrl ( ) )
300- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
301- . withSecretsBinaryInstalled ( ) ;
302-
303- mkdirSync ( harness . cwd . path , { recursive : true } ) ;
304- Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
305- harness . cwd . writeFile ( '.pre-commit-config.yaml' , 'repos: []\n' ) ;
382+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
383+ initGitRepoWithPreCommitConfig ( harness ) ;
306384
307385 const result = await harness . run ( 'integrate git --hook pre-commit --non-interactive' , {
308386 extraEnv : { PATH : setupFakePreCommit ( ) } ,
@@ -319,16 +397,8 @@ describe('integrate git (pre-commit framework)', () => {
319397 it (
320398 'installs pre-push hook via pre-commit framework when .pre-commit-config.yaml exists' ,
321399 async ( ) => {
322- const server = await harness . newFakeServer ( ) . withAuthToken ( 'test-token' ) . start ( ) ;
323- harness
324- . state ( )
325- . withActiveConnection ( server . baseUrl ( ) )
326- . withKeychainToken ( server . baseUrl ( ) , 'test-token' )
327- . withSecretsBinaryInstalled ( ) ;
328-
329- mkdirSync ( harness . cwd . path , { recursive : true } ) ;
330- Bun . spawnSync ( [ 'git' , 'init' ] , { cwd : harness . cwd . path } ) ;
331- harness . cwd . writeFile ( '.pre-commit-config.yaml' , 'repos: []\n' ) ;
400+ await setupAuthenticated ( harness , { withSecretsBinary : true } ) ;
401+ initGitRepoWithPreCommitConfig ( harness ) ;
332402
333403 const result = await harness . run ( 'integrate git --hook pre-push --non-interactive' , {
334404 extraEnv : { PATH : setupFakePreCommit ( ) } ,
0 commit comments