Skip to content

Commit 278e4cb

Browse files
authored
♻️ refactor(config.ts): Addition of UnitTest environment and unittest for commands/config.ts#getConfig (#330)
* feat(jest.config.ts): update jest preset for TS ESM support and ignore patterns feat(package.json): add test:unit script with NODE_OPTIONS for ESM refactor(src/commands/config.ts): improve dotenv usage with dynamic paths feat(src/commands/config.ts): allow custom config and env paths in getConfig refactor(src/commands/config.ts): streamline environment variable access feat(test/unit): add unit tests for config handling and utility functions - Implement unit tests for `getConfig` function to ensure correct behavior in various scenarios including default values, global config, and local env file precedence. - Add utility function `prepareFile` for creating temporary files during tests, facilitating testing of file-based configurations. * feat(e2e.yml): add unit-test job to GitHub Actions for running unit tests on pull requests * ci(test.yml): add GitHub Actions workflow for unit and e2e tests on pull requests * refactor(config.ts): streamline environment variable access using process.env directly test(config.test.ts): add setup and teardown for environment variables in tests to ensure test isolation * feat(package.json): add `test:all` script to run all tests in Docker refactor(package.json): consolidate Docker build steps into `test:docker-build` script for DRY principle fix(package.json): ensure `test:unit:docker` and `test:e2e:docker` scripts use the same Docker image and remove container after run chore(test/Dockerfile): remove default CMD to allow dynamic test script execution in Docker * refactor(config.test.ts): anonymize API keys in tests for better security practices * feat(config.test.ts): add tests for OCO_ANTHROPIC_API_KEY configuration * refactor(config.ts): streamline path imports and remove unused DotenvParseOutput - Simplify path module imports by removing default import and using named imports for `pathJoin` and `pathResolve`. - Remove unused `DotenvParseOutput` import to clean up the code. * refactor(config.test.ts): simplify API key mock values for clarity in tests * test(config.test.ts): remove tests for default config values and redundant cases - Removed tests that checked for default config values when no config or env files are present, as these scenarios are now handled differently. - Eliminated tests for empty global config and local env files to streamline testing focus on actual config loading logic. - Removed test for prioritizing local env over global config due to changes in config loading strategy, simplifying the configuration management.
1 parent e19305d commit 278e4cb

File tree

7 files changed

+171
-12
lines changed

7 files changed

+171
-12
lines changed

.github/workflows/e2e.yml renamed to .github/workflows/test.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
name: E2E Testing
1+
name: Testing
22

33
on: [pull_request]
44

55
jobs:
6+
unit-test:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
node-version: [20.x]
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Use Node.js ${{ matrix.node-version }}
14+
uses: actions/setup-node@v2
15+
with:
16+
node-version: ${{ matrix.node-version }}
17+
- name: Install dependencies
18+
run: npm install
19+
- name: Run Unit Tests
20+
run: npm run test:unit
621
e2e-test:
722
runs-on: ubuntu-latest
823
strategy:

jest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ const config: Config = {
1111
"node_modules",
1212
"src",
1313
],
14-
preset: 'ts-jest',
14+
preset: 'ts-jest/presets/js-with-ts-esm',
1515
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
1616
testEnvironment: "node",
1717
testRegex: [
1818
'.*\\.test\\.ts$',
1919
],
20+
transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'],
2021
transform: {
2122
'^.+\\.(ts|tsx)$': ['ts-jest', {
2223
diagnostics: false,
24+
useESM: true
2325
}],
2426
}
2527
};

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@
4848
"deploy": "npm version patch && npm run build:push && git push --tags && npm publish --tag latest",
4949
"lint": "eslint src --ext ts && tsc --noEmit",
5050
"format": "prettier --write src",
51+
"test:all": "npm run test:unit:docker && npm run test:e2e:docker",
52+
"test:docker-build": "docker build -t oco-test -f test/Dockerfile .",
53+
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest test/unit",
54+
"test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit",
5155
"test:e2e": "jest test/e2e",
52-
"test:e2e:docker": "docker build -t oco-e2e -f test/Dockerfile . && DOCKER_CONTENT_TRUST=0 docker run oco-e2e"
56+
"test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e"
5357
},
5458
"devDependencies": {
5559
"@commitlint/types": "^17.4.4",

src/commands/config.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ import * as dotenv from 'dotenv';
44
import { existsSync, readFileSync, writeFileSync } from 'fs';
55
import { parse as iniParse, stringify as iniStringify } from 'ini';
66
import { homedir } from 'os';
7-
import { join as pathJoin } from 'path';
7+
import { join as pathJoin, resolve as pathResolve } from 'path';
88

99
import { intro, outro } from '@clack/prompts';
1010

1111
import { COMMANDS } from '../CommandsEnum';
1212
import { getI18nLocal } from '../i18n';
1313

14-
dotenv.config();
15-
1614
export enum CONFIG_KEYS {
1715
OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY',
1816
OCO_ANTHROPIC_API_KEY = 'OCO_ANTHROPIC_API_KEY',
@@ -248,9 +246,17 @@ export type ConfigType = {
248246
[key in CONFIG_KEYS]?: any;
249247
};
250248

251-
const configPath = pathJoin(homedir(), '.opencommit');
252-
253-
export const getConfig = (): ConfigType | null => {
249+
const defaultConfigPath = pathJoin(homedir(), '.opencommit');
250+
const defaultEnvPath = pathResolve(process.cwd(), '.env');
251+
252+
export const getConfig = ({
253+
configPath = defaultConfigPath,
254+
envPath = defaultEnvPath
255+
}: {
256+
configPath?: string
257+
envPath?: string
258+
} = {}): ConfigType | null => {
259+
dotenv.config({ path: envPath });
254260
const configFromEnv = {
255261
OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY,
256262
OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY,
@@ -306,7 +312,7 @@ export const getConfig = (): ConfigType | null => {
306312
return config;
307313
};
308314

309-
export const setConfig = (keyValues: [key: string, value: string][]) => {
315+
export const setConfig = (keyValues: [key: string, value: string][], configPath: string = defaultConfigPath) => {
310316
const config = getConfig() || {};
311317

312318
for (const [configKey, configValue] of keyValues) {

test/Dockerfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,3 @@ RUN ls -la
1717

1818
RUN npm install
1919
RUN npm run build
20-
21-
CMD ["npm", "run", "test:e2e"]

test/unit/config.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { getConfig } from '../../src/commands/config';
2+
import { prepareFile } from './utils';
3+
4+
describe('getConfig', () => {
5+
const originalEnv = { ...process.env };
6+
function resetEnv(env: NodeJS.ProcessEnv) {
7+
Object.keys(process.env).forEach((key) => {
8+
if (!(key in env)) {
9+
delete process.env[key];
10+
} else {
11+
process.env[key] = env[key];
12+
}
13+
});
14+
}
15+
16+
beforeEach(() => {
17+
resetEnv(originalEnv);
18+
});
19+
20+
afterAll(() => {
21+
resetEnv(originalEnv);
22+
});
23+
24+
it('return config values from the global config file', async () => {
25+
const configFile = await prepareFile(
26+
'.opencommit',
27+
`
28+
OCO_OPENAI_API_KEY="sk-key"
29+
OCO_ANTHROPIC_API_KEY="secret-key"
30+
OCO_TOKENS_MAX_INPUT="8192"
31+
OCO_TOKENS_MAX_OUTPUT="1000"
32+
OCO_OPENAI_BASE_PATH="/openai/api"
33+
OCO_DESCRIPTION="true"
34+
OCO_EMOJI="true"
35+
OCO_MODEL="gpt-4"
36+
OCO_LANGUAGE="de"
37+
OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m"
38+
OCO_PROMPT_MODULE="@commitlint"
39+
OCO_AI_PROVIDER="ollama"
40+
OCO_GITPUSH="false"
41+
OCO_ONE_LINE_COMMIT="true"
42+
`
43+
);
44+
const config = getConfig({ configPath: configFile.filePath, envPath: '' });
45+
46+
expect(config).not.toEqual(null);
47+
expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key');
48+
expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key');
49+
expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192);
50+
expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000);
51+
expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api');
52+
expect(config!['OCO_DESCRIPTION']).toEqual(true);
53+
expect(config!['OCO_EMOJI']).toEqual(true);
54+
expect(config!['OCO_MODEL']).toEqual('gpt-4');
55+
expect(config!['OCO_LANGUAGE']).toEqual('de');
56+
expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m');
57+
expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint');
58+
expect(config!['OCO_AI_PROVIDER']).toEqual('ollama');
59+
expect(config!['OCO_GITPUSH']).toEqual(false);
60+
expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true);
61+
62+
await configFile.cleanup();
63+
});
64+
65+
it('return config values from the local env file', async () => {
66+
const envFile = await prepareFile(
67+
'.env',
68+
`
69+
OCO_OPENAI_API_KEY="sk-key"
70+
OCO_ANTHROPIC_API_KEY="secret-key"
71+
OCO_TOKENS_MAX_INPUT="8192"
72+
OCO_TOKENS_MAX_OUTPUT="1000"
73+
OCO_OPENAI_BASE_PATH="/openai/api"
74+
OCO_DESCRIPTION="true"
75+
OCO_EMOJI="true"
76+
OCO_MODEL="gpt-4"
77+
OCO_LANGUAGE="de"
78+
OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m"
79+
OCO_PROMPT_MODULE="@commitlint"
80+
OCO_AI_PROVIDER="ollama"
81+
OCO_GITPUSH="false"
82+
OCO_ONE_LINE_COMMIT="true"
83+
`
84+
);
85+
const config = getConfig({ configPath: '', envPath: envFile.filePath });
86+
87+
expect(config).not.toEqual(null);
88+
expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key');
89+
expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key');
90+
expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192);
91+
expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000);
92+
expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api');
93+
expect(config!['OCO_DESCRIPTION']).toEqual(true);
94+
expect(config!['OCO_EMOJI']).toEqual(true);
95+
expect(config!['OCO_MODEL']).toEqual('gpt-4');
96+
expect(config!['OCO_LANGUAGE']).toEqual('de');
97+
expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m');
98+
expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint');
99+
expect(config!['OCO_AI_PROVIDER']).toEqual('ollama');
100+
expect(config!['OCO_GITPUSH']).toEqual(false);
101+
expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true);
102+
103+
await envFile.cleanup();
104+
});
105+
});

test/unit/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import path from 'path';
2+
import { mkdtemp, rm, writeFile } from 'fs';
3+
import { promisify } from 'util';
4+
import { tmpdir } from 'os';
5+
const fsMakeTempDir = promisify(mkdtemp);
6+
const fsRemove = promisify(rm);
7+
const fsWriteFile = promisify(writeFile);
8+
9+
/**
10+
* Prepare tmp file for the test
11+
*/
12+
export async function prepareFile(
13+
fileName: string,
14+
content: string
15+
): Promise<{
16+
filePath: string;
17+
cleanup: () => Promise<void>;
18+
}> {
19+
const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-'));
20+
const filePath = path.resolve(tempDir, fileName);
21+
await fsWriteFile(filePath, content);
22+
const cleanup = async () => {
23+
return fsRemove(tempDir, { recursive: true });
24+
};
25+
return {
26+
filePath,
27+
cleanup
28+
};
29+
}

0 commit comments

Comments
 (0)