Skip to content

test: improve config test coverage #1032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 5, 2025
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
44 changes: 15 additions & 29 deletions src/config/ConfigLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,8 @@
try {
console.log(`Loading configuration from ${source.type} source`);
return await this.loadFromSource(source);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`Error loading from ${source.type} source:`, error.message);
}
} catch (error: any) {
console.error(`Error loading from ${source.type} source:`, error.message);
return null;
}
}),
Expand Down Expand Up @@ -234,7 +232,7 @@
} else {
console.log('Configuration has not changed, no update needed');
}
} catch (error: unknown) {
} catch (error: any) {
console.error('Error reloading configuration:', error);
this.emit('configurationError', error);
} finally {
Expand Down Expand Up @@ -330,24 +328,18 @@
try {
await execFileAsync('git', ['clone', source.repository, repoDir], execOptions);
console.log('Repository cloned successfully');
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Failed to clone repository:', error.message);
throw new Error(`Failed to clone repository: ${error.message}`);
}
throw error;
} catch (error: any) {
console.error('Failed to clone repository:', error.message);
throw new Error(`Failed to clone repository: ${error.message}`);
}
} else {
console.log(`Pulling latest changes from ${source.repository}`);
try {
await execFileAsync('git', ['pull'], { cwd: repoDir });
console.log('Repository pulled successfully');
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Failed to pull repository:', error.message);
throw new Error(`Failed to pull repository: ${error.message}`);
}
throw error;
} catch (error: any) {
console.error('Failed to pull repository:', error.message);
throw new Error(`Failed to pull repository: ${error.message}`);

Check warning on line 342 in src/config/ConfigLoader.ts

View check run for this annotation

Codecov / codecov/patch

src/config/ConfigLoader.ts#L341-L342

Added lines #L341 - L342 were not covered by tests
}
}

Expand All @@ -357,12 +349,9 @@
try {
await execFileAsync('git', ['checkout', source.branch], { cwd: repoDir });
console.log(`Branch ${source.branch} checked out successfully`);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`Failed to checkout branch ${source.branch}:`, error.message);
throw new Error(`Failed to checkout branch ${source.branch}: ${error.message}`);
}
throw error;
} catch (error: any) {
console.error(`Failed to checkout branch ${source.branch}:`, error.message);
throw new Error(`Failed to checkout branch ${source.branch}: ${error.message}`);
}
}

Expand All @@ -382,12 +371,9 @@
const config = JSON.parse(content);
console.log('Configuration loaded successfully from Git');
return config;
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Failed to read or parse configuration file:', error.message);
throw new Error(`Failed to read or parse configuration file: ${error.message}`);
}
throw error;
} catch (error: any) {
console.error('Failed to read or parse configuration file:', error.message);
throw new Error(`Failed to read or parse configuration file: ${error.message}`);
}
}

Expand Down
250 changes: 246 additions & 4 deletions test/ConfigLoader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ describe('ConfigLoader', () => {

expect(spy.calledOnce).to.be.true; // Should only emit once
});

it('should not emit event if configurationSources is disabled', async () => {
const config = {
configurationSources: {
enabled: false,
},
};

configLoader = new ConfigLoader(config);
const spy = sinon.spy();
configLoader.on('configurationChanged', spy);

await configLoader.reloadConfiguration();

expect(spy.called).to.be.false;
});
});

describe('initialize', () => {
Expand Down Expand Up @@ -182,6 +198,98 @@ describe('ConfigLoader', () => {
});
});

describe('start', () => {
it('should perform initial load on start if configurationSources is enabled', async () => {
const mockConfig = {
configurationSources: {
enabled: true,
sources: [
{
type: 'file',
enabled: true,
path: tempConfigFile,
},
],
reloadIntervalSeconds: 30,
},
};

const configLoader = new ConfigLoader(mockConfig);
const spy = sinon.spy(configLoader, 'reloadConfiguration');
await configLoader.start();

expect(spy.calledOnce).to.be.true;
});

it('should clear an existing reload interval if it exists', async () => {
const mockConfig = {
configurationSources: {
enabled: true,
sources: [
{
type: 'file',
enabled: true,
path: tempConfigFile,
},
],
},
};

const configLoader = new ConfigLoader(mockConfig);
configLoader.reloadTimer = setInterval(() => {}, 1000);
await configLoader.start();

expect(configLoader.reloadTimer).to.be.null;
});

it('should run reloadConfiguration multiple times on short reload interval', async () => {
const mockConfig = {
configurationSources: {
enabled: true,
sources: [
{
type: 'file',
enabled: true,
path: tempConfigFile,
},
],
reloadIntervalSeconds: 0.01,
},
};

const configLoader = new ConfigLoader(mockConfig);
const spy = sinon.spy(configLoader, 'reloadConfiguration');
await configLoader.start();

// Make sure the reload interval is triggered
await new Promise((resolve) => setTimeout(resolve, 50));

expect(spy.callCount).to.greaterThan(1);
});

it('should clear the interval when stop is called', async () => {
const mockConfig = {
configurationSources: {
enabled: true,
sources: [
{
type: 'file',
enabled: true,
path: tempConfigFile,
},
],
},
};

const configLoader = new ConfigLoader(mockConfig);
configLoader.reloadTimer = setInterval(() => {}, 1000);
expect(configLoader.reloadTimer).to.not.be.null;

await configLoader.stop();
expect(configLoader.reloadTimer).to.be.null;
});
});

describe('loadRemoteConfig', () => {
let configLoader;
beforeEach(async () => {
Expand All @@ -205,15 +313,15 @@ describe('ConfigLoader', () => {
enabled: true,
};

const config = await configLoader.loadFromGit(source);
const config = await configLoader.loadFromSource(source);

// Verify the loaded config has expected structure
expect(config).to.be.an('object');
expect(config).to.have.property('proxyUrl');
expect(config).to.have.property('cookieSecret');
});

it('should throw error for invalid configuration file path', async function () {
it('should throw error for invalid configuration file path (git)', async function () {
const source = {
type: 'git',
repository: 'https://github.com/finos/git-proxy.git',
Expand All @@ -223,13 +331,28 @@ describe('ConfigLoader', () => {
};

try {
await configLoader.loadFromGit(source);
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.equal('Invalid configuration file path in repository');
}
});

it('should throw error for invalid configuration file path (file)', async function () {
const source = {
type: 'file',
path: '\0', // Invalid path
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.equal('Invalid configuration file path');
}
});

it('should load configuration from http', async function () {
// eslint-disable-next-line no-invalid-this
this.timeout(10000);
Expand All @@ -240,13 +363,132 @@ describe('ConfigLoader', () => {
enabled: true,
};

const config = await configLoader.loadFromHttp(source);
const config = await configLoader.loadFromSource(source);

// Verify the loaded config has expected structure
expect(config).to.be.an('object');
expect(config).to.have.property('proxyUrl');
expect(config).to.have.property('cookieSecret');
});

it('should throw error if repository is invalid', async function () {
const source = {
type: 'git',
repository: 'invalid-repository',
path: 'proxy.config.json',
branch: 'main',
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.equal('Invalid repository URL format');
}
});

it('should throw error if branch name is invalid', async function () {
const source = {
type: 'git',
repository: 'https://github.com/finos/git-proxy.git',
path: 'proxy.config.json',
branch: '..', // invalid branch pattern
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.equal('Invalid branch name format');
}
});

it('should throw error if configuration source is invalid', async function () {
const source = {
type: 'invalid',
repository: 'https://github.com/finos/git-proxy.git',
path: 'proxy.config.json',
branch: 'main',
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.contain('Unsupported configuration source type');
}
});

it('should throw error if repository is a valid URL but not a git repository', async function () {
const source = {
type: 'git',
repository: 'https://github.com/test-org/test-repo.git',
path: 'proxy.config.json',
branch: 'main',
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.contain('Failed to clone repository');
}
});

it('should throw error if repository is a valid git repo but the branch does not exist', async function () {
const source = {
type: 'git',
repository: 'https://github.com/finos/git-proxy.git',
path: 'proxy.config.json',
branch: 'branch-does-not-exist',
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.contain('Failed to checkout branch');
}
});

it('should throw error if config path was not found', async function () {
const source = {
type: 'git',
repository: 'https://github.com/finos/git-proxy.git',
path: 'path-not-found.json',
branch: 'main',
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.contain('Configuration file not found at');
}
});

it('should throw error if config file is not valid JSON', async function () {
const source = {
type: 'git',
repository: 'https://github.com/finos/git-proxy.git',
path: 'test/fixtures/baz.js',
branch: 'main',
enabled: true,
};

try {
await configLoader.loadFromSource(source);
throw new Error('Expected error was not thrown');
} catch (error) {
expect(error.message).to.contain('Failed to read or parse configuration file');
}
});
});

describe('deepMerge', () => {
Expand Down
Loading
Loading