Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 5 additions & 3 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ function readConfigFile() {
}

return raw;
} catch {
} catch (error) {
console.error(chalk.yellow(`⚠ Failed to parse config file at ${CONFIG_FILE}: ${error.message}`));
console.error(chalk.yellow(' Run "confluence init" to recreate it.'));
return null;
}
}
Expand All @@ -259,7 +261,7 @@ const validateCliOptions = (options) => {
errors.push('--domain cannot be empty');
}

if (options.token !== undefined && !options.token.trim()) {
if (options.token !== undefined && (typeof options.token !== 'string' || !options.token.trim())) {
errors.push('--token cannot be empty');
}

Expand Down Expand Up @@ -304,7 +306,7 @@ const validateCliOptions = (options) => {
}
}

if (normAuthType === 'cookie' && options.cookie !== undefined && !options.cookie.trim()) {
if (normAuthType === 'cookie' && options.cookie !== undefined && (typeof options.cookie !== 'string' || !options.cookie.trim())) {
errors.push('--cookie cannot be empty when using cookie authentication');
}

Expand Down
70 changes: 69 additions & 1 deletion tests/config.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { getConfig } = require('../lib/config');
const { getConfig, initConfig } = require('../lib/config');

// Save and restore all relevant env vars around each test
const ENV_KEYS = [
Expand Down Expand Up @@ -282,3 +282,71 @@ describe('getConfig env var aliases', () => {
}
});
});

describe('initConfig CLI option validation', () => {
let exitSpy;
let errorSpy;
let logSpy;

beforeEach(() => {
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
exitSpy.mockRestore();
errorSpy.mockRestore();
logSpy.mockRestore();
});

test('null --token surfaces a validation error instead of crashing', async () => {
await expect(initConfig({
domain: 'example.com',
token: null,
authType: 'bearer',
})).rejects.toThrow('process.exit called');

expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/--token cannot be empty/)
);
});

test('non-string --token surfaces a validation error instead of crashing', async () => {
await expect(initConfig({
domain: 'example.com',
token: 12345,
authType: 'bearer',
})).rejects.toThrow('process.exit called');

expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/--token cannot be empty/)
);
});

test('null --cookie with cookie auth surfaces a validation error instead of crashing', async () => {
await expect(initConfig({
domain: 'example.com',
authType: 'cookie',
cookie: null,
})).rejects.toThrow('process.exit called');

expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/--cookie cannot be empty/)
);
});

test('whitespace-only --token still flagged as empty (regression guard)', async () => {
await expect(initConfig({
domain: 'example.com',
token: ' ',
authType: 'bearer',
})).rejects.toThrow('process.exit called');

expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/--token cannot be empty/)
);
});
});
33 changes: 33 additions & 0 deletions tests/profile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,37 @@ describe('Profile management', () => {
expect(fileChmod.mode).toBe(0o600);
});
});

describe('readConfigFile error reporting', () => {
test('logs a warning when the config file contains invalid JSON', () => {
fs.existsSync.mockImplementation((filePath) => {
if (filePath === CONFIG_FILE) return true;
return jest.requireActual('fs').existsSync(filePath);
});
fs.readFileSync.mockImplementation((filePath, encoding) => {
if (filePath === CONFIG_FILE) return '{ not valid json';
return jest.requireActual('fs').readFileSync(filePath, encoding);
});

const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});

try {
expect(() => getConfig()).toThrow('process.exit called');
expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/Failed to parse config file/)
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/Run "confluence init" to recreate it/)
);
} finally {
errorSpy.mockRestore();
logSpy.mockRestore();
exitSpy.mockRestore();
}
});
});
});
Loading