From 904a13d65ddfe990c6c53788d69982d9bc16ce12 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 23 Mar 2023 22:51:18 -0700 Subject: [PATCH] Add release functions tests --- __mocks__/@actions/github.ts | 41 ++++++ __tests__/release.test.ts | 236 +++++++++++++++++++++++++++++++++++ package-lock.json | 91 ++++++++++++++ package.json | 1 + tsconfig.json | 20 +-- 5 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 __mocks__/@actions/github.ts create mode 100644 __tests__/release.test.ts diff --git a/__mocks__/@actions/github.ts b/__mocks__/@actions/github.ts new file mode 100644 index 0000000..4f68ece --- /dev/null +++ b/__mocks__/@actions/github.ts @@ -0,0 +1,41 @@ +const mockApi = { + rest: { + issues: { + addLabels: jest.fn(), + removeLabel: jest.fn(), + }, + pulls: { + get: jest.fn().mockResolvedValue({}), + listFiles: { + endpoint: { + merge: jest.fn().mockReturnValue({}), + }, + }, + }, + repos: { + getContent: jest.fn(), + listReleases: jest.fn(), + createRelease: jest.fn(), + updateRelease: jest.fn(), + generateReleaseNotes: jest.fn(), + }, + }, + paginate: jest.fn().mockImplementation(async (method, options) => { + const response = await method(options) + return response.data + }), +} +export const context = { + payload: { + pull_request: { + number: 123, + }, + }, + repo: { + owner: 'monalisa', + repo: 'helloworld', + }, + ref: 'refs/heads/main', +} + +export const getOctokit = jest.fn().mockImplementation(() => mockApi) diff --git a/__tests__/release.test.ts b/__tests__/release.test.ts new file mode 100644 index 0000000..35affa3 --- /dev/null +++ b/__tests__/release.test.ts @@ -0,0 +1,236 @@ +import {getRelease, createOrUpdateRelease} from '../src/release' +import * as github from '@actions/github' +import {Inputs} from '../src/context' + +const fs = jest.requireActual('fs') + +jest.mock('@actions/core') +jest.mock('@actions/github') + +let gh: ReturnType + +describe('getRelease', () => { + beforeEach(() => { + jest.clearAllMocks() + gh = github.getOctokit('_') + }) + + it('should return the latest release when multiple releases exist', async () => { + const mockResponse: any = { + headers: {}, + status: 200, + data: [ + { + tag_name: 'v1.0.2', + target_commitish: 'main', + draft: false, + }, + { + tag_name: 'v1.0.1', + target_commitish: 'main', + draft: false, + }, + { + tag_name: 'v1.0.0', + target_commitish: 'main', + draft: false, + }, + ], + } + + const mockReleases = jest.spyOn(gh.rest.repos, 'listReleases') + mockReleases.mockResolvedValue(mockResponse) + + const [releases, latestRelease] = await getRelease(gh) + + expect(releases).toHaveLength(3) + expect(latestRelease).toBe('v1.0.2') + }) + + it('should return the latest for the current branch', async () => { + const mockResponse: any = { + headers: {}, + status: 200, + data: [ + { + tag_name: 'v1.0.2', + target_commitish: 'dev', + draft: false, + }, + { + tag_name: 'v1.0.1', + target_commitish: 'main', + draft: false, + }, + { + tag_name: 'v1.0.0', + target_commitish: 'main', + draft: false, + }, + ], + } + + const mockReleases = jest.spyOn(gh.rest.repos, 'listReleases') + mockReleases.mockResolvedValue(mockResponse) + + const [releases, latestRelease] = await getRelease(gh) + + expect(releases).toHaveLength(3) + expect(latestRelease).toBe('v1.0.1') + }) + + it('should return the latest non-draft release', async () => { + const mockResponse: any = { + headers: {}, + status: 200, + data: [ + { + tag_name: 'v1.0.2', + target_commitish: 'dev', + draft: false, + }, + { + tag_name: 'v1.0.1', + target_commitish: 'main', + draft: true, + }, + { + tag_name: 'v1.0.0', + target_commitish: 'main', + draft: false, + }, + ], + } + + const mockReleases = jest.spyOn(gh.rest.repos, 'listReleases') + mockReleases.mockResolvedValue(mockResponse) + + const [releases, latestRelease] = await getRelease(gh) + + expect(releases).toHaveLength(3) + expect(latestRelease).toBe('v1.0.0') + }) + + it('should return v0.0.0 when no releases exist', async () => { + const mockResponse: any = { + headers: {}, + status: 200, + data: [], + } + + const mockReleases = jest.spyOn(gh.rest.repos, 'listReleases') + mockReleases.mockResolvedValue(mockResponse) + + const [releases, latestRelease] = await getRelease(gh) + + expect(releases).toHaveLength(0) + expect(latestRelease).toBe('v0.0.0') + }) +}) + +describe('createOrUpdateRelease', () => { + let mockResponse: any + let mockNotes: any + const inputs: Inputs = { + githubToken: '_', + majorLabel: 'major', + minorLabel: 'minor', + header: 'header', + footer: 'footer', + } + beforeEach(() => { + jest.clearAllMocks() + gh = github.getOctokit('_') + mockResponse = { + headers: {}, + status: 200, + data: [ + { + id: 1, + tag_name: 'v1.0.0', + target_commitish: 'main', + draft: false, + body: 'header', + }, + { + id: 2, + tag_name: 'v1.0.1', + target_commitish: 'main', + draft: true, + body: 'header', + }, + ], + } + + mockNotes = { + headers: {}, + status: 200, + data: { + body: 'header', + name: 'v1.0.1', + }, + } + }) + + it('should create a new release draft', async () => { + const mockInputCreate: any = { + headers: {}, + status: 200, + data: [ + { + id: 1, + tag_name: 'v1.0.0', + target_commitish: 'main', + draft: false, + }, + ], + } + + const mockReleases = jest.spyOn(gh.rest.repos, 'createRelease') + mockReleases.mockResolvedValue(mockResponse) + + const mockRelease = jest.spyOn(gh.rest.repos, 'listReleases') + mockRelease.mockResolvedValue(mockInputCreate) + + const mockReleaseNotes = jest.spyOn(gh.rest.repos, 'generateReleaseNotes') + mockReleaseNotes.mockResolvedValue(mockNotes) + + const response = await createOrUpdateRelease(gh, inputs, mockInputCreate.data, 'v1.0.0', 'v1.0.1') + + expect(mockReleases).toHaveBeenCalledTimes(1) + }) + + it('should update an existing release draft', async () => { + const mockInputUpdate: any = { + headers: {}, + status: 200, + data: [ + { + id: 1, + tag_name: 'v1.0.0', + target_commitish: 'main', + draft: false, + }, + { + id: 2, + tag_name: 'v1.0.1', + target_commitish: 'main', + draft: true, + }, + ], + } + + const mockReleases = jest.spyOn(gh.rest.repos, 'updateRelease') + mockReleases.mockResolvedValue(mockResponse) + + const mockRelease = jest.spyOn(gh.rest.repos, 'listReleases') + mockRelease.mockResolvedValue(mockInputUpdate) + + const mockReleaseNotes = jest.spyOn(gh.rest.repos, 'generateReleaseNotes') + mockReleaseNotes.mockResolvedValue(mockNotes) + + const response = await createOrUpdateRelease(gh, inputs, mockInputUpdate.data, 'v1.0.0', 'v1.0.1') + + expect(mockReleases).toHaveBeenCalledTimes(1) + }) +}) diff --git a/package-lock.json b/package-lock.json index 33de5ed..e4d4d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "semver": "^7.3.8" }, "devDependencies": { + "@types/jest": "^27.4.1", "@types/js-yaml": "^4.0.5", "@types/node": "^18.15.3", "@typescript-eslint/parser": "^5.55.0", @@ -1443,6 +1444,96 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", diff --git a/package.json b/package.json index 8d47cea..0174f1c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "semver": "^7.3.8" }, "devDependencies": { + "@types/jest": "^29.5.0", "@types/js-yaml": "^4.0.5", "@types/node": "^18.15.3", "@typescript-eslint/parser": "^5.55.0", diff --git a/tsconfig.json b/tsconfig.json index f6e7cb5..9416c69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,16 @@ { "compilerOptions": { - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "outDir": "./lib", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ }, - "exclude": ["node_modules", "**/*.test.ts"] + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/__mocks__" + ] }