Skip to content

Commit 0939154

Browse files
authored
test: add chown traversal and writeConfigs OpenSSL guard tests
- ssl-bump-chown.test.ts: new file with jest.mock('fs') to verify chownRecursive (called by initSslDb) walks subdirectories and chowns every entry when chownSync succeeds — addresses the regression gap noted in the PR review - config-writer.test.ts: new file testing the isOpenSslAvailable guard in writeConfigs; verifies the SSL Bump initialization failed message and that generateSessionCa is not called when OpenSSL is absent
1 parent 07df497 commit 0939154

2 files changed

Lines changed: 220 additions & 0 deletions

File tree

src/config-writer.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
5+
// jest.mock() calls are hoisted before imports — keep them at the top.
6+
7+
// fs.chownSync is non-configurable and cannot be overridden with jest.spyOn.
8+
// Use a module-level mock that replaces only chownSync, keeping all other
9+
// fs functions real so directory/file creation in writeConfigs works normally.
10+
jest.mock('fs', () => {
11+
const actual = jest.requireActual<typeof import('fs')>('fs');
12+
return { ...actual, chownSync: jest.fn() };
13+
});
14+
15+
jest.mock('./ssl-bump', () => ({
16+
isOpenSslAvailable: jest.fn(),
17+
generateSessionCa: jest.fn(),
18+
initSslDb: jest.fn(),
19+
parseUrlPatterns: jest.fn().mockReturnValue([]),
20+
}));
21+
22+
jest.mock('./host-env', () => ({
23+
SQUID_PORT: 3128,
24+
getSafeHostUid: jest.fn().mockReturnValue('1000'),
25+
getSafeHostGid: jest.fn().mockReturnValue('1000'),
26+
getRealUserHome: jest.fn(),
27+
}));
28+
29+
jest.mock('./squid-config', () => ({
30+
generateSquidConfig: jest.fn().mockReturnValue('# mock squid config'),
31+
generatePolicyManifest: jest.fn().mockReturnValue({}),
32+
}));
33+
34+
jest.mock('./compose-generator', () => ({
35+
generateDockerCompose: jest.fn().mockReturnValue({ services: {}, version: '3' }),
36+
redactDockerComposeSecrets: jest.fn().mockReturnValue({ services: {}, version: '3' }),
37+
}));
38+
39+
import { writeConfigs } from './config-writer';
40+
import { isOpenSslAvailable } from './ssl-bump';
41+
import { getRealUserHome } from './host-env';
42+
43+
describe('writeConfigs', () => {
44+
let tempDir: string;
45+
46+
beforeEach(() => {
47+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-writer-test-'));
48+
jest.clearAllMocks();
49+
// getRealUserHome is used to locate host home subdirectories; point it at
50+
// tempDir so mkdirSync calls stay within the temp tree.
51+
(getRealUserHome as jest.Mock).mockReturnValue(tempDir);
52+
});
53+
54+
afterEach(() => {
55+
// Clean up tempDir and the chroot-home sibling directory that writeConfigs creates.
56+
fs.rmSync(tempDir, { recursive: true, force: true });
57+
fs.rmSync(`${tempDir}-chroot-home`, { recursive: true, force: true });
58+
});
59+
60+
describe('SSL Bump preflight guard', () => {
61+
it('should throw when sslBump is enabled and OpenSSL is unavailable', async () => {
62+
(isOpenSslAvailable as jest.Mock).mockResolvedValue(false);
63+
64+
await expect(
65+
writeConfigs({
66+
workDir: tempDir,
67+
sslBump: true,
68+
allowedDomains: [],
69+
agentCommand: 'echo test',
70+
logLevel: 'info',
71+
keepContainers: false,
72+
buildLocal: false,
73+
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
74+
imageTag: 'latest',
75+
})
76+
).rejects.toThrow('SSL Bump initialization failed: openssl is not available on this system');
77+
});
78+
79+
it('should check OpenSSL availability before calling generateSessionCa', async () => {
80+
(isOpenSslAvailable as jest.Mock).mockResolvedValue(false);
81+
const { generateSessionCa } = jest.requireMock('./ssl-bump');
82+
83+
await expect(
84+
writeConfigs({
85+
workDir: tempDir,
86+
sslBump: true,
87+
allowedDomains: [],
88+
agentCommand: 'echo test',
89+
logLevel: 'info',
90+
keepContainers: false,
91+
buildLocal: false,
92+
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
93+
imageTag: 'latest',
94+
})
95+
).rejects.toThrow();
96+
97+
expect(isOpenSslAvailable).toHaveBeenCalledTimes(1);
98+
expect(generateSessionCa).not.toHaveBeenCalled();
99+
});
100+
101+
it('should not check OpenSSL availability when sslBump is not enabled', async () => {
102+
await writeConfigs({
103+
workDir: tempDir,
104+
sslBump: false,
105+
allowedDomains: [],
106+
agentCommand: 'echo test',
107+
logLevel: 'info',
108+
keepContainers: false,
109+
buildLocal: false,
110+
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
111+
imageTag: 'latest',
112+
});
113+
114+
expect(isOpenSslAvailable).not.toHaveBeenCalled();
115+
});
116+
});
117+
});

src/ssl-bump-chown.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Tests for the chown traversal inside initSslDb.
3+
*
4+
* fs.chownSync is a non-configurable property and cannot be replaced with
5+
* jest.spyOn. This file uses a module-level jest.mock('fs', ...) to intercept
6+
* chownSync calls so the successful (non-EPERM) recursion path can be verified.
7+
* It is separate from ssl-bump.test.ts which relies on real fs operations.
8+
*/
9+
10+
import * as path from 'path';
11+
import * as os from 'os';
12+
13+
// Mock chownSync and conditionally intercept readdirSync (withFileTypes calls only).
14+
const mockChownSync = jest.fn();
15+
const mockReaddirSync = jest.fn();
16+
17+
jest.mock('fs', () => {
18+
const actual = jest.requireActual<typeof import('fs')>('fs');
19+
return {
20+
...actual,
21+
chownSync: (...args: unknown[]) => mockChownSync(...args),
22+
readdirSync: (...args: unknown[]) => {
23+
// Only intercept calls that pass { withFileTypes: true } — those come from
24+
// chownRecursive. All other readdirSync calls (e.g. from mkdirSync internals)
25+
// use the real implementation.
26+
if (args[1] && typeof args[1] === 'object' && 'withFileTypes' in (args[1] as object)) {
27+
return mockReaddirSync(...args);
28+
}
29+
return actual.readdirSync(...args as Parameters<typeof actual.readdirSync>);
30+
},
31+
};
32+
});
33+
34+
// Mock execa (imported transitively by ssl-bump.ts).
35+
jest.mock('execa');
36+
37+
import * as fs from 'fs';
38+
import { initSslDb } from './ssl-bump';
39+
40+
describe('initSslDb chown traversal', () => {
41+
let tempDir: string;
42+
43+
beforeEach(() => {
44+
mockChownSync.mockReset();
45+
mockReaddirSync.mockReset();
46+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ssl-db-chown-test-'));
47+
});
48+
49+
afterEach(() => {
50+
fs.rmSync(tempDir, { recursive: true, force: true });
51+
});
52+
53+
it('should chown ssl_db root, certs subdir, and all created files', async () => {
54+
// Let chownSync succeed (no EPERM).
55+
mockChownSync.mockImplementation(() => {});
56+
57+
// Return the real ssl_db structure that initSslDb creates:
58+
// first readdirSync call is on ssl_db root → index.txt, size, certs (dir)
59+
// second readdirSync call is on certs → empty
60+
mockReaddirSync
61+
.mockReturnValueOnce([
62+
{ name: 'index.txt', isDirectory: () => false },
63+
{ name: 'size', isDirectory: () => false },
64+
{ name: 'certs', isDirectory: () => true },
65+
])
66+
.mockReturnValueOnce([]);
67+
68+
const sslDbPath = await initSslDb(tempDir);
69+
70+
// Root ssl_db directory
71+
expect(mockChownSync).toHaveBeenCalledWith(sslDbPath, 13, 13);
72+
// Files in ssl_db root
73+
expect(mockChownSync).toHaveBeenCalledWith(path.join(sslDbPath, 'index.txt'), 13, 13);
74+
expect(mockChownSync).toHaveBeenCalledWith(path.join(sslDbPath, 'size'), 13, 13);
75+
// Subdirectory (recursed into)
76+
expect(mockChownSync).toHaveBeenCalledWith(path.join(sslDbPath, 'certs'), 13, 13);
77+
expect(mockChownSync).toHaveBeenCalledTimes(4);
78+
});
79+
80+
it('should recurse into nested subdirectories', async () => {
81+
mockChownSync.mockImplementation(() => {});
82+
83+
// ssl_db root contains one subdirectory with a file inside it.
84+
mockReaddirSync
85+
.mockReturnValueOnce([
86+
{ name: 'certs', isDirectory: () => true },
87+
])
88+
.mockReturnValueOnce([
89+
{ name: '1234ABCD.pem', isDirectory: () => false },
90+
]);
91+
92+
const sslDbPath = await initSslDb(tempDir);
93+
94+
expect(mockChownSync).toHaveBeenCalledWith(sslDbPath, 13, 13);
95+
expect(mockChownSync).toHaveBeenCalledWith(path.join(sslDbPath, 'certs'), 13, 13);
96+
expect(mockChownSync).toHaveBeenCalledWith(
97+
path.join(sslDbPath, 'certs', '1234ABCD.pem'),
98+
13,
99+
13
100+
);
101+
expect(mockChownSync).toHaveBeenCalledTimes(3);
102+
});
103+
});

0 commit comments

Comments
 (0)