diff --git a/.github/workflows/e2e.yml b/.github/workflows/test.yml similarity index 63% rename from .github/workflows/e2e.yml rename to .github/workflows/test.yml index f4f092d2..b1e49e31 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,23 @@ -name: E2E Testing +name: Testing on: [pull_request] jobs: + unit-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Run Unit Tests + run: npm run test:unit e2e-test: runs-on: ubuntu-latest strategy: diff --git a/jest.config.ts b/jest.config.ts index 2130dc13..6bca3938 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -11,15 +11,17 @@ const config: Config = { "node_modules", "src", ], - preset: 'ts-jest', + preset: 'ts-jest/presets/js-with-ts-esm', setupFilesAfterEnv: ['/test/jest-setup.ts'], testEnvironment: "node", testRegex: [ '.*\\.test\\.ts$', ], + transformIgnorePatterns: ['node_modules/(?!cli-testing-library)'], transform: { '^.+\\.(ts|tsx)$': ['ts-jest', { diagnostics: false, + useESM: true }], } }; diff --git a/package.json b/package.json index 56e6a652..6764a61a 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,12 @@ "deploy": "npm version patch && npm run build:push && git push --tags && npm publish --tag latest", "lint": "eslint src --ext ts && tsc --noEmit", "format": "prettier --write src", + "test:all": "npm run test:unit:docker && npm run test:e2e:docker", + "test:docker-build": "docker build -t oco-test -f test/Dockerfile .", + "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest test/unit", + "test:unit:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:unit", "test:e2e": "jest test/e2e", - "test:e2e:docker": "docker build -t oco-e2e -f test/Dockerfile . && DOCKER_CONTENT_TRUST=0 docker run oco-e2e" + "test:e2e:docker": "npm run test:docker-build && DOCKER_CONTENT_TRUST=0 docker run --rm oco-test npm run test:e2e" }, "devDependencies": { "@commitlint/types": "^17.4.4", diff --git a/src/commands/config.ts b/src/commands/config.ts index e038312e..d877db38 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -4,15 +4,13 @@ import * as dotenv from 'dotenv'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { parse as iniParse, stringify as iniStringify } from 'ini'; import { homedir } from 'os'; -import { join as pathJoin } from 'path'; +import { join as pathJoin, resolve as pathResolve } from 'path'; import { intro, outro } from '@clack/prompts'; import { COMMANDS } from '../CommandsEnum'; import { getI18nLocal } from '../i18n'; -dotenv.config(); - export enum CONFIG_KEYS { OCO_OPENAI_API_KEY = 'OCO_OPENAI_API_KEY', OCO_ANTHROPIC_API_KEY = 'OCO_ANTHROPIC_API_KEY', @@ -248,9 +246,17 @@ export type ConfigType = { [key in CONFIG_KEYS]?: any; }; -const configPath = pathJoin(homedir(), '.opencommit'); - -export const getConfig = (): ConfigType | null => { +const defaultConfigPath = pathJoin(homedir(), '.opencommit'); +const defaultEnvPath = pathResolve(process.cwd(), '.env'); + +export const getConfig = ({ + configPath = defaultConfigPath, + envPath = defaultEnvPath +}: { + configPath?: string + envPath?: string +} = {}): ConfigType | null => { + dotenv.config({ path: envPath }); const configFromEnv = { OCO_OPENAI_API_KEY: process.env.OCO_OPENAI_API_KEY, OCO_ANTHROPIC_API_KEY: process.env.OCO_ANTHROPIC_API_KEY, @@ -306,7 +312,7 @@ export const getConfig = (): ConfigType | null => { return config; }; -export const setConfig = (keyValues: [key: string, value: string][]) => { +export const setConfig = (keyValues: [key: string, value: string][], configPath: string = defaultConfigPath) => { const config = getConfig() || {}; for (const [configKey, configValue] of keyValues) { diff --git a/test/Dockerfile b/test/Dockerfile index 80c8f3c2..5b779174 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -17,5 +17,3 @@ RUN ls -la RUN npm install RUN npm run build - -CMD ["npm", "run", "test:e2e"] diff --git a/test/unit/config.test.ts b/test/unit/config.test.ts new file mode 100644 index 00000000..4c596aea --- /dev/null +++ b/test/unit/config.test.ts @@ -0,0 +1,105 @@ +import { getConfig } from '../../src/commands/config'; +import { prepareFile } from './utils'; + +describe('getConfig', () => { + const originalEnv = { ...process.env }; + function resetEnv(env: NodeJS.ProcessEnv) { + Object.keys(process.env).forEach((key) => { + if (!(key in env)) { + delete process.env[key]; + } else { + process.env[key] = env[key]; + } + }); + } + + beforeEach(() => { + resetEnv(originalEnv); + }); + + afterAll(() => { + resetEnv(originalEnv); + }); + + it('return config values from the global config file', async () => { + const configFile = await prepareFile( + '.opencommit', + ` +OCO_OPENAI_API_KEY="sk-key" +OCO_ANTHROPIC_API_KEY="secret-key" +OCO_TOKENS_MAX_INPUT="8192" +OCO_TOKENS_MAX_OUTPUT="1000" +OCO_OPENAI_BASE_PATH="/openai/api" +OCO_DESCRIPTION="true" +OCO_EMOJI="true" +OCO_MODEL="gpt-4" +OCO_LANGUAGE="de" +OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m" +OCO_PROMPT_MODULE="@commitlint" +OCO_AI_PROVIDER="ollama" +OCO_GITPUSH="false" +OCO_ONE_LINE_COMMIT="true" +` + ); + const config = getConfig({ configPath: configFile.filePath, envPath: '' }); + + expect(config).not.toEqual(null); + expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key'); + expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key'); + expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192); + expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000); + expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api'); + expect(config!['OCO_DESCRIPTION']).toEqual(true); + expect(config!['OCO_EMOJI']).toEqual(true); + expect(config!['OCO_MODEL']).toEqual('gpt-4'); + expect(config!['OCO_LANGUAGE']).toEqual('de'); + expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m'); + expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint'); + expect(config!['OCO_AI_PROVIDER']).toEqual('ollama'); + expect(config!['OCO_GITPUSH']).toEqual(false); + expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true); + + await configFile.cleanup(); + }); + + it('return config values from the local env file', async () => { + const envFile = await prepareFile( + '.env', + ` +OCO_OPENAI_API_KEY="sk-key" +OCO_ANTHROPIC_API_KEY="secret-key" +OCO_TOKENS_MAX_INPUT="8192" +OCO_TOKENS_MAX_OUTPUT="1000" +OCO_OPENAI_BASE_PATH="/openai/api" +OCO_DESCRIPTION="true" +OCO_EMOJI="true" +OCO_MODEL="gpt-4" +OCO_LANGUAGE="de" +OCO_MESSAGE_TEMPLATE_PLACEHOLDER="$m" +OCO_PROMPT_MODULE="@commitlint" +OCO_AI_PROVIDER="ollama" +OCO_GITPUSH="false" +OCO_ONE_LINE_COMMIT="true" + ` + ); + const config = getConfig({ configPath: '', envPath: envFile.filePath }); + + expect(config).not.toEqual(null); + expect(config!['OCO_OPENAI_API_KEY']).toEqual('sk-key'); + expect(config!['OCO_ANTHROPIC_API_KEY']).toEqual('secret-key'); + expect(config!['OCO_TOKENS_MAX_INPUT']).toEqual(8192); + expect(config!['OCO_TOKENS_MAX_OUTPUT']).toEqual(1000); + expect(config!['OCO_OPENAI_BASE_PATH']).toEqual('/openai/api'); + expect(config!['OCO_DESCRIPTION']).toEqual(true); + expect(config!['OCO_EMOJI']).toEqual(true); + expect(config!['OCO_MODEL']).toEqual('gpt-4'); + expect(config!['OCO_LANGUAGE']).toEqual('de'); + expect(config!['OCO_MESSAGE_TEMPLATE_PLACEHOLDER']).toEqual('$m'); + expect(config!['OCO_PROMPT_MODULE']).toEqual('@commitlint'); + expect(config!['OCO_AI_PROVIDER']).toEqual('ollama'); + expect(config!['OCO_GITPUSH']).toEqual(false); + expect(config!['OCO_ONE_LINE_COMMIT']).toEqual(true); + + await envFile.cleanup(); + }); +}); diff --git a/test/unit/utils.ts b/test/unit/utils.ts new file mode 100644 index 00000000..14433442 --- /dev/null +++ b/test/unit/utils.ts @@ -0,0 +1,29 @@ +import path from 'path'; +import { mkdtemp, rm, writeFile } from 'fs'; +import { promisify } from 'util'; +import { tmpdir } from 'os'; +const fsMakeTempDir = promisify(mkdtemp); +const fsRemove = promisify(rm); +const fsWriteFile = promisify(writeFile); + +/** + * Prepare tmp file for the test + */ +export async function prepareFile( + fileName: string, + content: string +): Promise<{ + filePath: string; + cleanup: () => Promise; +}> { + const tempDir = await fsMakeTempDir(path.join(tmpdir(), 'opencommit-test-')); + const filePath = path.resolve(tempDir, fileName); + await fsWriteFile(filePath, content); + const cleanup = async () => { + return fsRemove(tempDir, { recursive: true }); + }; + return { + filePath, + cleanup + }; +}