Skip to content

Commit e5f1d82

Browse files
committed
test(config): cover required defaults handling
1 parent cf12f8d commit e5f1d82

2 files changed

Lines changed: 111 additions & 14 deletions

File tree

src/config/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ type MissingRequiredTopLevelConfigKeys = Exclude<
6262
type AssertNever<T extends never> = T;
6363
type _RequiredTopLevelConfigKeysAreExhaustive = AssertNever<MissingRequiredTopLevelConfigKeys>;
6464

65-
function assertHasRequiredTopLevelConfig(
65+
export function assertHasRequiredTopLevelConfig(
6666
config: GitProxyConfig,
6767
): asserts config is FullGitProxyConfig {
6868
const missingKeys = REQUIRED_TOP_LEVEL_CONFIG_KEYS.filter((key) => config[key] === undefined);
@@ -375,7 +375,7 @@ const handleConfigUpdate = async (newConfig: Configuration) => {
375375
const validatedConfig = Convert.toGitProxyConfig(JSON.stringify(newConfig));
376376

377377
// 2. Get proxy module dynamically to avoid circular dependency
378-
const proxy = require('../proxy');
378+
const proxy = (await import('../proxy')) as any;
379379

380380
// 3. Stop existing services
381381
await proxy.stop();
@@ -392,7 +392,7 @@ const handleConfigUpdate = async (newConfig: Configuration) => {
392392
handleErrorAndLog(error, 'Failed to apply new configuration');
393393
// Attempt to restart with previous config
394394
try {
395-
const proxy = require('../proxy');
395+
const proxy = (await import('../proxy')) as any;
396396
await proxy.start();
397397
} catch (startError: unknown) {
398398
handleErrorAndLog(startError, 'Failed to restart services');

test/testConfig.test.ts

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -426,24 +426,29 @@ describe('Configuration Update Handling', () => {
426426
let tempUserFile: string;
427427
let oldEnv: NodeJS.ProcessEnv;
428428

429+
const waitForMockCall = async (mock: MockInstance, callCount = 1) => {
430+
for (let i = 0; i < 20; i += 1) {
431+
if (mock.mock.calls.length >= callCount) {
432+
return;
433+
}
434+
await new Promise((resolve) => setTimeout(resolve, 10));
435+
}
436+
};
437+
429438
beforeEach(() => {
430439
oldEnv = { ...process.env };
431440
tempDir = fs.mkdtempSync('gitproxy-test');
432441
tempUserFile = path.join(tempDir, 'test-settings.json');
442+
process.env.CONFIG_FILE = tempUserFile;
433443
configFile.setConfigFile(tempUserFile);
434444
});
435445

436446
it('should test ConfigLoader initialization', async () => {
437447
const configWithSources = {
448+
...defaultSettings,
438449
configurationSources: {
439450
enabled: true,
440-
sources: [
441-
{
442-
type: 'file',
443-
enabled: true,
444-
path: tempUserFile,
445-
},
446-
],
451+
sources: [],
447452
},
448453
};
449454

@@ -481,15 +486,98 @@ describe('Configuration Update Handling', () => {
481486
consoleErrorSpy.mockRestore();
482487
});
483488

484-
afterEach(() => {
485-
if (fs.existsSync(tempUserFile)) {
486-
fs.rmSync(tempUserFile, { force: true });
489+
it('should apply valid configuration updates from external sources', async () => {
490+
const updatedConfigFile = path.join(tempDir, 'updated-settings.json');
491+
const proxyStop = vi.fn().mockResolvedValue(undefined);
492+
const proxyStart = vi.fn().mockResolvedValue(undefined);
493+
vi.doMock('../src/proxy', () => ({
494+
stop: proxyStop,
495+
start: proxyStart,
496+
}));
497+
498+
const configWithSources = {
499+
configurationSources: {
500+
enabled: true,
501+
sources: [
502+
{
503+
type: 'file',
504+
enabled: true,
505+
path: updatedConfigFile,
506+
},
507+
],
508+
},
509+
};
510+
const updatedConfig = {
511+
...defaultSettings,
512+
configurationSources: configWithSources.configurationSources,
513+
sessionMaxAgeHours: 8,
514+
};
515+
516+
fs.writeFileSync(tempUserFile, JSON.stringify(configWithSources));
517+
fs.writeFileSync(updatedConfigFile, JSON.stringify(updatedConfig));
518+
519+
const config = await import('../src/config');
520+
521+
await waitForMockCall(proxyStart);
522+
523+
expect(proxyStop).toHaveBeenCalledTimes(1);
524+
expect(proxyStart).toHaveBeenCalledTimes(1);
525+
expect(config.getSessionMaxAgeHours()).toBe(updatedConfig.sessionMaxAgeHours);
526+
});
527+
528+
it('should restart the proxy with the previous config when updates fail', async () => {
529+
const updatedConfigFile = path.join(tempDir, 'updated-settings.json');
530+
const proxyStop = vi.fn().mockRejectedValue(new Error('stop failed'));
531+
const proxyStart = vi.fn().mockResolvedValue(undefined);
532+
vi.doMock('../src/proxy', () => ({
533+
stop: proxyStop,
534+
start: proxyStart,
535+
}));
536+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
537+
538+
const configWithSources = {
539+
configurationSources: {
540+
enabled: true,
541+
sources: [
542+
{
543+
type: 'file',
544+
enabled: true,
545+
path: updatedConfigFile,
546+
},
547+
],
548+
},
549+
};
550+
const updatedConfig = {
551+
...defaultSettings,
552+
configurationSources: configWithSources.configurationSources,
553+
sessionMaxAgeHours: 8,
554+
};
555+
556+
fs.writeFileSync(tempUserFile, JSON.stringify(configWithSources));
557+
fs.writeFileSync(updatedConfigFile, JSON.stringify(updatedConfig));
558+
559+
try {
560+
await import('../src/config');
561+
562+
await waitForMockCall(proxyStart);
563+
564+
expect(proxyStop).toHaveBeenCalledTimes(1);
565+
expect(proxyStart).toHaveBeenCalledTimes(1);
566+
expect(consoleErrorSpy).toHaveBeenCalledWith(
567+
'Failed to apply new configuration: stop failed',
568+
);
569+
} finally {
570+
consoleErrorSpy.mockRestore();
487571
}
572+
});
573+
574+
afterEach(() => {
488575
if (fs.existsSync(tempDir)) {
489-
fs.rmdirSync(tempDir);
576+
fs.rmSync(tempDir, { recursive: true, force: true });
490577
}
491578
process.env = oldEnv;
492579

580+
vi.doUnmock('../src/proxy');
493581
vi.resetModules();
494582
});
495583
});
@@ -591,5 +679,14 @@ describe('loadFullConfiguration', () => {
591679
'Invalid regular expression for commitConfig.author.email.local.block: [invalid(regex',
592680
);
593681
});
682+
683+
it('should throw error when merged defaults miss required top-level values', async () => {
684+
const config = await import('../src/config');
685+
const settingsWithoutSink = { ...defaultSettings, sink: undefined };
686+
687+
expect(() => config.assertHasRequiredTopLevelConfig(settingsWithoutSink)).toThrow(
688+
'Missing required top-level configuration values: sink',
689+
);
690+
});
594691
});
595692
});

0 commit comments

Comments
 (0)