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..7e5db5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,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", @@ -1443,6 +1444,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "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__" + ] }