From e1328c0c0f785370690f617e5a57a7ec46247aa0 Mon Sep 17 00:00:00 2001 From: jeeyo Date: Wed, 19 Jul 2023 19:00:40 +0700 Subject: [PATCH 1/2] add test for App class --- Dockerfile | 2 +- src/CodeCoachError.ts | 7 ++ src/Config/Config.spec.ts | 19 ++--- src/Config/Config.ts | 6 +- src/Config/index.ts | 2 +- src/Provider/GitLab/GitLabMRService.ts | 56 +++++++++----- src/app.spec.ts | 101 +++++++++++++++++++++++++ src/app.ts | 73 +++++++++--------- src/index.ts | 23 ++++++ 9 files changed, 218 insertions(+), 71 deletions(-) create mode 100644 src/CodeCoachError.ts create mode 100644 src/app.spec.ts create mode 100644 src/index.ts diff --git a/Dockerfile b/Dockerfile index de134d5..f385516 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,4 @@ RUN yarn install --production COPY --from=build /app/dist ./dist RUN npm link -ENTRYPOINT ["/app/dist/app.js"] \ No newline at end of file +ENTRYPOINT ["/app/dist/index.js"] diff --git a/src/CodeCoachError.ts b/src/CodeCoachError.ts new file mode 100644 index 0000000..eee3ecb --- /dev/null +++ b/src/CodeCoachError.ts @@ -0,0 +1,7 @@ +export default class CodeCoachError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, CodeCoachError.prototype); + this.name = 'CodeCoachError'; + } +} diff --git a/src/Config/Config.spec.ts b/src/Config/Config.spec.ts index d0c6e3e..6ad9f12 100644 --- a/src/Config/Config.spec.ts +++ b/src/Config/Config.spec.ts @@ -1,4 +1,5 @@ import { BuildLogFile } from './@types'; +import { ConfigParser } from './Config'; const mockGitHubRepo = 'https://github.com/codeleague/codecoach.git'; const mockGitHubPr = 42; @@ -70,8 +71,7 @@ describe('Config parsing Test', () => { }; it('should be able to parse GitHub config provided by environment variables', async () => { - process.argv = GITHUB_ENV_ARGS; - const config = (await import('./Config')).configs; + const config = ConfigParser(GITHUB_ENV_ARGS); expect(config.vcs).toBe('github'); expect(config.githubRepoUrl).toBe(mockGitHubRepo); expect(config.githubPr).toBe(mockGitHubPr); @@ -84,8 +84,7 @@ describe('Config parsing Test', () => { }); it('should be able to parse GitHub config provided by file', async () => { - process.argv = GITHUB_FILE_ARGS; - const config = (await import('./Config')).configs; + const config = ConfigParser(GITHUB_FILE_ARGS); expect(config.vcs).toBe('github'); expect(config.githubRepoUrl).toBe(mockGitHubRepo); expect(config.githubPr).toBe(mockGitHubPr); @@ -98,8 +97,7 @@ describe('Config parsing Test', () => { }); it('should be able to parse GitLab config provided by environment variables', async () => { - process.argv = GITLAB_ENV_ARGS; - const config = (await import('./Config')).configs; + const config = ConfigParser(GITLAB_ENV_ARGS); expect(config.vcs).toBe('gitlab'); expect(config.gitlabHost).toBe(mockGitLabHost); expect(config.gitlabProjectId).toBe(mockGitLabProjectId); @@ -113,8 +111,7 @@ describe('Config parsing Test', () => { }); it('should be able to parse GitLab config provided by file', async () => { - process.argv = GITLAB_FILE_ARGS; - const config = (await import('./Config')).configs; + const config = ConfigParser(GITLAB_FILE_ARGS); expect(config.vcs).toBe('gitlab'); expect(config.gitlabHost).toBe(mockGitLabHost); expect(config.gitlabProjectId).toBe(mockGitLabProjectId); @@ -128,16 +125,14 @@ describe('Config parsing Test', () => { }); it('should be able to parse dryRun config provided by environment variables', async () => { - process.argv = DRYRUN_ENV_ARGS; - const config = (await import('./Config')).configs; + const config = ConfigParser(DRYRUN_ENV_ARGS); expect(config.dryRun).toBe(true); validateBuildLog(config.buildLogFile); }); it('should be able to parse dryRun config provided by file', async () => { - process.argv = DRYRUN_FILE_ARGS; - const config = (await import('./Config')).configs; + const config = ConfigParser(DRYRUN_FILE_ARGS); expect(config.dryRun).toBe(true); validateBuildLog(config.buildLogFile); diff --git a/src/Config/Config.ts b/src/Config/Config.ts index 8473540..0b73937 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -123,7 +123,7 @@ and is build root directory (optional (Will use current context as cwd)). }) .strict() .help() - .wrap(120) - .parse(process.argv.slice(1)) as ConfigArgument; + .wrap(120); -export const configs = args; +export const ConfigParser = (argv: string[]): ConfigArgument => + args.parse(argv) as ConfigArgument; diff --git a/src/Config/index.ts b/src/Config/index.ts index 2062f95..313ab95 100644 --- a/src/Config/index.ts +++ b/src/Config/index.ts @@ -1,3 +1,3 @@ -export { configs } from './Config'; +export { ConfigParser } from './Config'; export * from './@types'; export * from './@enums'; diff --git a/src/Provider/GitLab/GitLabMRService.ts b/src/Provider/GitLab/GitLabMRService.ts index 1bedb3c..ecdbd1e 100644 --- a/src/Provider/GitLab/GitLabMRService.ts +++ b/src/Provider/GitLab/GitLabMRService.ts @@ -9,20 +9,27 @@ import { import * as Resource from '@gitbeaker/core/dist/types/resources'; import { Gitlab } from '@gitbeaker/node'; -import { configs } from '../../Config'; - export class GitLabMRService implements IGitLabMRService { - private readonly projectId: number; - private readonly mrIid: number; + private readonly gitlabHost: string; + private readonly gitlabProjectId: number; + private readonly gitlabMrIid: number; + private readonly gitlabToken: string; private readonly api: Resource.Gitlab; - constructor() { - this.projectId = configs.gitlabProjectId; - this.mrIid = configs.gitlabMrIid; + constructor( + gitlabHost: string, + gitlabProjectId: number, + gitlabMrIid: number, + gitlabToken: string, + ) { + this.gitlabHost = gitlabHost; + this.gitlabProjectId = gitlabProjectId; + this.gitlabMrIid = gitlabMrIid; + this.gitlabToken = gitlabToken; this.api = new Gitlab({ - host: configs.gitlabHost, - token: configs.gitlabToken, + host: this.gitlabHost, + token: this.gitlabToken, }); } @@ -43,9 +50,14 @@ export class GitLabMRService implements IGitLabMRService { new_line: line, }; - await this.api.MergeRequestDiscussions.create(this.projectId, this.mrIid, body, { - position, - }); + await this.api.MergeRequestDiscussions.create( + this.gitlabProjectId, + this.gitlabMrIid, + body, + { + position, + }, + ); } async getCurrentUserId(): Promise { @@ -54,21 +66,26 @@ export class GitLabMRService implements IGitLabMRService { } async listAllNotes(): Promise { - return await this.api.MergeRequestNotes.all(this.projectId, this.mrIid); + return await this.api.MergeRequestNotes.all(this.gitlabProjectId, this.gitlabMrIid); } async deleteNote(noteId: number): Promise { - await this.api.MergeRequestNotes.remove(this.projectId, this.mrIid, noteId); + await this.api.MergeRequestNotes.remove( + this.gitlabProjectId, + this.gitlabMrIid, + noteId, + ); } // github can do someone fancy shit here we cant async createNote(note: string): Promise { - await this.api.MergeRequestNotes.create(this.projectId, this.mrIid, note); + await this.api.MergeRequestNotes.create(this.gitlabProjectId, this.gitlabMrIid, note); } async diff(): Promise { - const changes = (await this.api.MergeRequests.changes(this.projectId, this.mrIid)) - .changes; + const changes = ( + await this.api.MergeRequests.changes(this.gitlabProjectId, this.gitlabMrIid) + ).changes; if (!changes) { return []; @@ -81,7 +98,10 @@ export class GitLabMRService implements IGitLabMRService { } async getLatestVersion(): Promise { - const versions = await this.api.MergeRequests.versions(this.projectId, this.mrIid); + const versions = await this.api.MergeRequests.versions( + this.gitlabProjectId, + this.gitlabMrIid, + ); const collected = versions.filter((v) => v.state === 'collected'); if (collected.length === 0) throw new Error('No collected version in MR'); diff --git a/src/app.spec.ts b/src/app.spec.ts new file mode 100644 index 0000000..0c490ce --- /dev/null +++ b/src/app.spec.ts @@ -0,0 +1,101 @@ +import { App } from './app'; +import CodeCoachError from './CodeCoachError'; +import { ConfigArgument } from './Config'; +import { File } from './File'; +import { VCSEngine } from './Provider/CommonVCS/VCSEngine'; +import { GitLabAdapter } from './Provider/GitLab/GitLabAdapter'; +import { GitLabMRService } from './Provider/GitLab/GitLabMRService'; +import { GitHubAdapter } from './Provider/GitHub/GitHubAdapter'; +import { GitHubPRService } from './Provider/GitHub/GitHubPRService'; + +jest.mock('./File'); +jest.mock('./Provider/CommonVCS/VCSEngine'); +jest.mock('./Provider/GitLab/GitLabAdapter'); +jest.mock('./Provider/GitLab/GitLabMRService'); +jest.mock('./Provider/GitHub/GitHubAdapter'); +jest.mock('./Provider/GitHub/GitHubPRService'); + +const mockedVCSEngine = VCSEngine as jest.MockedClass; +const mockedGitLabAdapter = GitLabAdapter as jest.MockedClass; +const mockedGitLabMRService = GitLabMRService as jest.MockedClass; +const mockedGitHubAdapter = GitHubAdapter as jest.MockedClass; +const mockedGitHubPRService = GitHubPRService as jest.MockedClass; + +describe('App', () => { + it('should not require VCS when dry-run', async () => { + const mockedWriteFileHelper = jest.fn(); + File.writeFileHelper = mockedWriteFileHelper; + + const configs = ({ buildLogFile: [], dryRun: true } as unknown) as ConfigArgument; + const app = new App(configs); + await app.start(); + + expect(mockedWriteFileHelper).toBeCalled(); + }); + + it('should throw "VCS adapter is not found" error when run without VCS', async () => { + const app = new App(({ buildLogFile: [] } as unknown) as ConfigArgument); + const fn = async () => await app.start(); + + await expect(fn).rejects.toThrowError(CodeCoachError); + await expect(fn).rejects.toThrowError('VCS adapter is not found'); + }); + + it('should initialize GitLabAdapter and GitLabMRService correctly', async () => { + const vcsReportFn = jest.fn().mockResolvedValue(true); + mockedVCSEngine.mockImplementationOnce(() => { + return ({ + report: vcsReportFn, + } as unknown) as VCSEngine; + }); + + const configs = ({ + vcs: 'gitlab', + gitlabHost: 'https://gitlab.com', + gitlabProjectId: 1234, + gitlabMrIid: 99, + gitlabToken: 'fakegitlabtoken', + buildLogFile: [], + } as unknown) as ConfigArgument; + + const app = new App(configs); + await app.start(); + + expect(vcsReportFn).toBeCalledTimes(1); + expect(mockedGitLabMRService).toBeCalledWith( + configs.gitlabHost, + configs.gitlabProjectId, + configs.gitlabMrIid, + configs.gitlabToken, + ); + expect(mockedGitLabAdapter).toBeCalledTimes(1); + }); + + it('should initialize GitHubAdapter and GitHubPRService correctly', async () => { + const vcsReportFn = jest.fn().mockResolvedValue(true); + mockedVCSEngine.mockImplementationOnce(() => { + return ({ + report: vcsReportFn, + } as unknown) as VCSEngine; + }); + + const configs = ({ + vcs: 'github', + githubRepoUrl: 'https://github.com/codeleague/codecoach', + githubPr: 1234, + githubToken: 'fakegithubtoken', + buildLogFile: [], + } as unknown) as ConfigArgument; + + const app = new App(configs); + await app.start(); + + expect(vcsReportFn).toBeCalledTimes(1); + expect(mockedGitHubPRService).toBeCalledWith( + configs.githubToken, + configs.githubRepoUrl, + configs.githubPr, + ); + expect(mockedGitHubAdapter).toBeCalledTimes(1); + }); +}); diff --git a/src/app.ts b/src/app.ts index 7f00611..3f49c10 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,4 @@ -#!/usr/bin/env node - -import { BuildLogFile, configs, ProjectType } from './Config'; +import { BuildLogFile, ConfigArgument, ProjectType } from './Config'; import { File } from './File'; import { Log } from './Logger'; import { @@ -22,47 +20,59 @@ import { VCSEngine } from './Provider/CommonVCS/VCSEngine'; import { GitLabAdapter } from './Provider/GitLab/GitLabAdapter'; import { VCSAdapter } from './Provider/@interfaces/VCSAdapter'; import { AnalyzerBot } from './AnalyzerBot/AnalyzerBot'; +import CodeCoachError from './CodeCoachError'; + +export class App { + private vcs: VCS; + private readonly configs: ConfigArgument; -class App { - private vcs: VCS | null = null; + constructor(configs: ConfigArgument) { + this.configs = configs; + } async start(): Promise { - if (!configs.dryRun) { - const adapter = App.getAdapter(); + if (!this.configs.dryRun) { + const adapter = this.getAdapter(); if (!adapter) { - Log.error('VCS adapter is not found'); - process.exit(1); + throw new CodeCoachError('VCS adapter is not found'); } - const analyzer = new AnalyzerBot(configs); - this.vcs = new VCSEngine(configs, analyzer, adapter); + const analyzer = new AnalyzerBot(this.configs); + this.vcs = new VCSEngine(this.configs, analyzer, adapter); } - const logs = await this.parseBuildData(configs.buildLogFile); + const logs = await this.parseBuildData(this.configs.buildLogFile); Log.info('Build data parsing completed'); const reportToVcs = this.reportToVcs(logs); - const logToFile = App.writeLogToFile(logs); + const logToFile = this.writeLogToFile(logs); const [passed] = await Promise.all([reportToVcs, logToFile]); if (!passed) { - Log.error('There are some linting error and exit code reporting is enabled'); - process.exit(1); + throw new CodeCoachError( + 'There are some linting error and exit code reporting is enabled', + ); } } - private static getAdapter(): VCSAdapter | undefined { - if (configs.vcs === 'github') { + private getAdapter(): VCSAdapter | undefined { + if (this.configs.vcs === 'github') { const githubPRService = new GitHubPRService( - configs.githubToken, - configs.githubRepoUrl, - configs.githubPr, + this.configs.githubToken, + this.configs.githubRepoUrl, + this.configs.githubPr, ); return new GitHubAdapter(githubPRService); - } else if (configs.vcs === 'gitlab') { - return new GitLabAdapter(new GitLabMRService()); + } else if (this.configs.vcs === 'gitlab') { + const gitlabMRService = new GitLabMRService( + this.configs.gitlabHost, + this.configs.gitlabProjectId, + this.configs.gitlabMrIid, + this.configs.gitlabToken, + ); + return new GitLabAdapter(gitlabMRService); } } - private static getParser(type: ProjectType, cwd: string): Parser { + private getParser(type: ProjectType, cwd: string): Parser { switch (type) { case ProjectType.dotnetbuild: return new DotnetBuildParser(cwd); @@ -87,7 +97,7 @@ class App { const logsTasks = files.map(async ({ type, path, cwd }) => { Log.debug('Parsing', { type, path, cwd }); const content = await File.readFileHelper(path); - const parser = App.getParser(type, cwd); + const parser = this.getParser(type, cwd); return parser.parse(content); }); @@ -95,7 +105,7 @@ class App { } private async reportToVcs(logs: LogType[]): Promise { - if (!this.vcs) { + if (this.configs.dryRun) { Log.info('Dry run enabled, skip reporting'); return true; } @@ -110,9 +120,9 @@ class App { } } - private static async writeLogToFile(logs: LogType[]): Promise { + private async writeLogToFile(logs: LogType[]): Promise { try { - await File.writeFileHelper(configs.output, JSON.stringify(logs, null, 2)); + await File.writeFileHelper(this.configs.output, JSON.stringify(logs, null, 2)); Log.info('Write output completed'); } catch (error) { Log.error('Write output failed', { error }); @@ -120,12 +130,3 @@ class App { } } } - -new App().start().catch((error) => { - if (error instanceof Error) { - const { stack, message } = error; - Log.error('Unexpected error', { stack, message }); - } - Log.error('Unexpected error', { error }); - process.exit(2); -}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..307f633 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +import CodeCoachError from './CodeCoachError'; +import { ConfigParser } from './Config'; +import { Log } from './Logger'; +import { App } from './app'; + +const cliOptions = ConfigParser(process.argv); + +new App(cliOptions).start().catch((error) => { + if (error instanceof CodeCoachError) { + Log.error(error.message); + process.exit(1); + } + + if (error instanceof Error) { + const { stack, message } = error; + Log.error('Unexpected error', { stack, message }); + } + + Log.error('Unexpected error', { error }); + process.exit(2); +}); From de9e77b45fa7052267ed37788b240fcaff121b52 Mon Sep 17 00:00:00 2001 From: jeeyo Date: Wed, 19 Jul 2023 19:48:28 +0700 Subject: [PATCH 2/2] change entrypoint path --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 213c550..581f329 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "prepublishOnly": "yarn build" }, "bin": { - "codecoach": "dist/app.js" + "codecoach": "dist/index.js" }, "files": [ "dist/**/*"