Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 304 additions & 0 deletions src/config-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,309 @@ describe('writeConfigs', () => {
const mcpLogsDirMode = fs.statSync(mcpLogsDir).mode & 0o777;
expect(mcpLogsDirMode).toBe(0o777);
});

it('throws when workDir path exists but is not a directory', async () => {
const filePath = path.join(tempDir, 'not-a-directory');
fs.writeFileSync(filePath, 'content');

await expect(
writeConfigs({
workDir: filePath,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
})
).rejects.toThrow(/EEXIST|ENOTDIR/);
});

it('creates chroot home directory when it does not exist', async () => {
const emptyHomeDir = `${tempDir}-chroot-home`;
expect(fs.existsSync(emptyHomeDir)).toBe(false);

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
});

expect(fs.existsSync(emptyHomeDir)).toBe(true);
expect(fs.statSync(emptyHomeDir).isDirectory()).toBe(true);
});

it('uses existing chroot home directory if already present', async () => {
const emptyHomeDir = `${tempDir}-chroot-home`;
fs.mkdirSync(emptyHomeDir, { recursive: true });
const statBefore = fs.statSync(emptyHomeDir);

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
});

const statAfter = fs.statSync(emptyHomeDir);
expect(statAfter.ino).toBe(statBefore.ino); // Same directory
});

it('creates missing home subdirectories with correct ownership', async () => {
const homeDir = tempDir;
(getRealUserHome as jest.Mock).mockReturnValue(homeDir);

// Delete .copilot if it exists
const copilotDir = path.join(homeDir, '.copilot');
if (fs.existsSync(copilotDir)) {
fs.rmSync(copilotDir, { recursive: true, force: true });
}

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
});

expect(fs.existsSync(copilotDir)).toBe(true);
expect(fs.chownSync).toHaveBeenCalledWith(copilotDir, 1000, 1000);
});

it('creates .gemini directory when geminiApiKey is provided', async () => {
const homeDir = tempDir;
(getRealUserHome as jest.Mock).mockReturnValue(homeDir);

const geminiDir = path.join(homeDir, '.gemini');
if (fs.existsSync(geminiDir)) {
fs.rmSync(geminiDir, { recursive: true, force: true });
}

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
geminiApiKey: 'test-key',
});

expect(fs.existsSync(geminiDir)).toBe(true);
});

it('does not create .gemini directory when geminiApiKey is not provided', async () => {
const homeDir = tempDir;
(getRealUserHome as jest.Mock).mockReturnValue(homeDir);

const geminiDir = path.join(homeDir, '.gemini');
if (fs.existsSync(geminiDir)) {
fs.rmSync(geminiDir, { recursive: true, force: true });
}

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
});

expect(fs.existsSync(geminiDir)).toBe(false);
});

it('creates audit directory when it does not exist', async () => {
const auditDir = path.join(tempDir, 'custom-audit');

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
auditDir,
});

expect(fs.existsSync(auditDir)).toBe(true);
expect(fs.existsSync(path.join(auditDir, 'squid.conf'))).toBe(true);
expect(fs.existsSync(path.join(auditDir, 'docker-compose.redacted.yml'))).toBe(true);
expect(fs.existsSync(path.join(auditDir, 'policy-manifest.json'))).toBe(true);
});
});

describe('seccomp profile', () => {
it('throws error when seccomp profile is not found', async () => {
const originalExistsSync = fs.existsSync;
const existsSyncSpy = jest.spyOn(fs, 'existsSync').mockImplementation((filePath: fs.PathLike) => {
const normalizedPath =
typeof filePath === 'string' ? filePath : filePath.toString();

if (
normalizedPath === 'seccomp-profile.json' ||
normalizedPath.endsWith(`${path.sep}seccomp-profile.json`)
) {
return false;
}

return originalExistsSync(filePath);
});

try {
await expect(
writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
})
).rejects.toThrow(/Seccomp profile not found/);
} finally {
existsSyncSpy.mockRestore();
}
});
});

describe('URL patterns and API proxy', () => {
beforeEach(() => {
const { parseUrlPatterns } = jest.requireMock('./ssl-bump');
parseUrlPatterns.mockReturnValue(['https://example\\.com/.*']);
});

it('parses URL patterns when allowedUrls is provided', async () => {
const { parseUrlPatterns } = jest.requireMock('./ssl-bump');
const { generateSquidConfig } = jest.requireMock('./squid-config');

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: ['example.com'],
allowedUrls: ['https://example.com/api/*'],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
});

expect(parseUrlPatterns).toHaveBeenCalledWith(['https://example.com/api/*']);
expect(generateSquidConfig).toHaveBeenCalledWith(
expect.objectContaining({
urlPatterns: ['https://example\\.com/.*'],
})
);
});

it('does not parse URL patterns when allowedUrls is empty', async () => {
const { parseUrlPatterns } = jest.requireMock('./ssl-bump');
const { generateSquidConfig } = jest.requireMock('./squid-config');

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: ['example.com'],
allowedUrls: [],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
});

expect(parseUrlPatterns).not.toHaveBeenCalled();
expect(generateSquidConfig).toHaveBeenCalledWith(
expect.objectContaining({
urlPatterns: undefined,
})
);
});

it('includes API proxy configuration when enableApiProxy is true', async () => {
const { generateSquidConfig } = jest.requireMock('./squid-config');
const { generatePolicyManifest } = jest.requireMock('./squid-config');

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: ['example.com'],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
enableApiProxy: true,
});

expect(generateSquidConfig).toHaveBeenCalledWith(
expect.objectContaining({
apiProxyIp: '172.30.0.30',
apiProxyPorts: expect.arrayContaining([10000, 10001, 10002, 10003]),
})
);
expect(generatePolicyManifest).toHaveBeenCalledWith(
expect.objectContaining({
apiProxyIp: '172.30.0.30',
})
);
});

it('does not include API proxy configuration when enableApiProxy is false', async () => {
const { generateSquidConfig } = jest.requireMock('./squid-config');

await writeConfigs({
workDir: tempDir,
sslBump: false,
allowedDomains: ['example.com'],
agentCommand: 'echo test',
logLevel: 'info',
keepContainers: false,
buildLocal: false,
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
imageTag: 'latest',
enableApiProxy: false,
});

expect(generateSquidConfig).toHaveBeenCalledWith(
expect.not.objectContaining({
apiProxyIp: expect.anything(),
})
);
});
});
});
Loading