|
21 | 21 | // Integration tests for `sonar integrate git` |
22 | 22 |
|
23 | 23 | import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; |
24 | | -import { mkdirSync, writeFileSync } from 'node:fs'; |
| 24 | +import { mkdirSync, symlinkSync, writeFileSync } from 'node:fs'; |
25 | 25 | import { join } from 'node:path'; |
26 | 26 | import { 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(harness: TestHarness, sonarBinDir: string): Record<string, string> { |
| 42 | + return { |
| 43 | + ...process.env, |
| 44 | + HOME: harness.userHome.path, |
| 45 | + SONAR_CLI_KEYCHAIN_FILE: harness.keychainJsonFile.path, |
| 46 | + PATH: `${sonarBinDir}${PATH_DELIM}${pathWithoutNodeModules(process.env.PATH)}`, |
| 47 | + }; |
| 48 | +} |
| 49 | + |
| 50 | +function setupSonarBinDir(harness: TestHarness): { |
| 51 | + sonarBinDir: string; |
| 52 | + hookEnv: Record<string, string>; |
| 53 | +} { |
| 54 | + const sonarBinDir = join(harness.cwd.path, 'sonar-bin'); |
| 55 | + mkdirSync(sonarBinDir, { recursive: true }); |
| 56 | + symlinkSync(BINARY_PATH, join(sonarBinDir, 'sonar')); |
| 57 | + return { sonarBinDir, hookEnv: buildHookEnv(harness, sonarBinDir) }; |
| 58 | +} |
| 59 | + |
| 60 | +function setupGitUser(cwd: string): void { |
| 61 | + Bun.spawnSync(['git', 'config', 'user.email', 'test@example.com'], { cwd }); |
| 62 | + Bun.spawnSync(['git', 'config', 'user.name', 'Test User'], { cwd }); |
| 63 | +} |
| 64 | + |
| 65 | +function addBareRemote(cwd: string): void { |
| 66 | + const remotePath = join(cwd, '..', 'remote.git'); |
| 67 | + mkdirSync(remotePath, { recursive: true }); |
| 68 | + Bun.spawnSync(['git', 'init', '--bare'], { cwd: remotePath }); |
| 69 | + Bun.spawnSync(['git', 'remote', 'add', 'origin', remotePath], { cwd }); |
| 70 | + Bun.spawnSync(['git', 'branch', '-M', 'main'], { cwd }); |
| 71 | +} |
| 72 | + |
| 73 | +function gitCommit( |
| 74 | + cwd: string, |
| 75 | + env: Record<string, string>, |
| 76 | + message: string, |
| 77 | +): ReturnType<typeof Bun.spawnSync> { |
| 78 | + return Bun.spawnSync(['git', 'commit', '-m', message], { |
| 79 | + cwd, |
| 80 | + env, |
| 81 | + stdout: 'pipe', |
| 82 | + stderr: 'pipe', |
| 83 | + }); |
| 84 | +} |
| 85 | + |
| 86 | +function gitPush( |
| 87 | + cwd: string, |
| 88 | + env: Record<string, string>, |
| 89 | + setUpstream: boolean, |
| 90 | +): ReturnType<typeof Bun.spawnSync> { |
| 91 | + const args = setUpstream |
| 92 | + ? ['git', 'push', '-u', 'origin', 'main'] |
| 93 | + : ['git', 'push', 'origin', 'main']; |
| 94 | + return Bun.spawnSync(args, { cwd, env, stdout: 'pipe', stderr: 'pipe' }); |
| 95 | +} |
27 | 96 |
|
28 | 97 | describe('integrate git (native hooks)', () => { |
29 | 98 | let harness: TestHarness; |
@@ -88,24 +157,72 @@ describe('integrate git (native hooks)', () => { |
88 | 157 | ); |
89 | 158 |
|
90 | 159 | it( |
91 | | - 'installs pre-commit hook when user selects pre-commit (--non-interactive)', |
| 160 | + 'pre-commit hook blocks commit when staged file contains a secret', |
92 | 161 | async () => { |
93 | 162 | const server = await harness.newFakeServer().withAuthToken('test-token').start(); |
94 | 163 | harness |
95 | 164 | .state() |
96 | 165 | .withActiveConnection(server.baseUrl()) |
97 | | - .withKeychainToken(server.baseUrl(), 'test-token'); |
98 | | - |
99 | | - // Minimal git repo |
100 | | - harness.cwd.writeFile('.git/.keep', ''); |
| 166 | + .withKeychainToken(server.baseUrl(), 'test-token') |
| 167 | + .withSecretsBinaryInstalled(); |
101 | 168 |
|
102 | | - // Fake binaries server so that ensureSonarSecrets() can download sonar-secrets |
103 | | - await harness.newFakeBinariesServer().start(); |
| 169 | + mkdirSync(harness.cwd.path, { recursive: true }); |
| 170 | + Bun.spawnSync(['git', 'init'], { cwd: harness.cwd.path }); |
104 | 171 |
|
105 | 172 | const result = await harness.run('integrate git --hook pre-commit --non-interactive'); |
106 | | - |
107 | 173 | expect(result.exitCode).toBe(0); |
108 | 174 | expect(harness.cwd.exists('.git', 'hooks', 'pre-commit')).toBe(true); |
| 175 | + |
| 176 | + const { hookEnv } = setupSonarBinDir(harness); |
| 177 | + harness.cwd.writeFile('secret.js', `const token = "${GITHUB_TEST_TOKEN}";`); |
| 178 | + Bun.spawnSync(['git', 'add', 'secret.js'], { cwd: harness.cwd.path }); |
| 179 | + setupGitUser(harness.cwd.path); |
| 180 | + |
| 181 | + const commit = gitCommit(harness.cwd.path, hookEnv, 'wip'); |
| 182 | + expect(commit.exitCode).not.toBe(0); |
| 183 | + const output = (commit.stdout?.toString() ?? '') + (commit.stderr?.toString() ?? ''); |
| 184 | + expect(output).toContain('Secrets found'); |
| 185 | + }, |
| 186 | + { timeout: 30000 }, |
| 187 | + ); |
| 188 | + |
| 189 | + it( |
| 190 | + 'pre-push hook blocks push when commit contains a secret', |
| 191 | + async () => { |
| 192 | + const server = await harness.newFakeServer().withAuthToken('test-token').start(); |
| 193 | + harness |
| 194 | + .state() |
| 195 | + .withActiveConnection(server.baseUrl()) |
| 196 | + .withKeychainToken(server.baseUrl(), 'test-token') |
| 197 | + .withSecretsBinaryInstalled(); |
| 198 | + |
| 199 | + mkdirSync(harness.cwd.path, { recursive: true }); |
| 200 | + Bun.spawnSync(['git', 'init'], { cwd: harness.cwd.path }); |
| 201 | + |
| 202 | + const result = await harness.run('integrate git --hook pre-push --non-interactive'); |
| 203 | + expect(result.exitCode).toBe(0); |
| 204 | + expect(harness.cwd.exists('.git', 'hooks', 'pre-push')).toBe(true); |
| 205 | + |
| 206 | + const { hookEnv } = setupSonarBinDir(harness); |
| 207 | + setupGitUser(harness.cwd.path); |
| 208 | + |
| 209 | + // First commit + push: clean file, should succeed |
| 210 | + harness.cwd.writeFile('clean.js', 'const x = 1;\n'); |
| 211 | + Bun.spawnSync(['git', 'add', 'clean.js'], { cwd: harness.cwd.path }); |
| 212 | + gitCommit(harness.cwd.path, hookEnv, 'initial'); |
| 213 | + addBareRemote(harness.cwd.path); |
| 214 | + const firstPush = gitPush(harness.cwd.path, hookEnv, true); |
| 215 | + expect(firstPush.exitCode).toBe(0); |
| 216 | + |
| 217 | + // Second commit + push: file with secret, should be blocked by pre-push hook |
| 218 | + harness.cwd.writeFile('secret.js', `const token = "${GITHUB_TEST_TOKEN}";`); |
| 219 | + Bun.spawnSync(['git', 'add', 'secret.js'], { cwd: harness.cwd.path }); |
| 220 | + gitCommit(harness.cwd.path, hookEnv, 'wip'); |
| 221 | + const secondPush = gitPush(harness.cwd.path, hookEnv, false); |
| 222 | + |
| 223 | + expect(secondPush.exitCode).not.toBe(0); |
| 224 | + const output = (secondPush.stdout?.toString() ?? '') + (secondPush.stderr?.toString() ?? ''); |
| 225 | + expect(output).toContain('Secrets found'); |
109 | 226 | }, |
110 | 227 | { timeout: 30000 }, |
111 | 228 | ); |
|
0 commit comments