diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..21217c7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: "build-test" +on: # rebuild any PRs and main branch changes + pull_request: + branches: + - master + - 'releases/*' + push: + branches: + - master + - 'releases/*' + +jobs: + build: # make sure build/ci works properly + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - run: | + npm install + npm build + npm test \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad46b30..016bf90 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ typings/ # next.js build output .next + +node_modules +coverage diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts new file mode 100644 index 0000000..92012b0 --- /dev/null +++ b/__tests__/run.test.ts @@ -0,0 +1,195 @@ +import * as run from '../src/run' +import * as os from 'os'; +import * as toolCache from '@actions/tool-cache'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as core from '@actions/core'; + +describe('run.ts', () => { + test('getExecutableExtension() - return .exe when os is Windows', () => { + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + + expect(run.getExecutableExtension()).toBe('.exe'); + expect(os.type).toBeCalled(); + }); + + test('getExecutableExtension() - return empty string for non-windows OS', () => { + jest.spyOn(os, 'type').mockReturnValue('Darwin'); + + expect(run.getExecutableExtension()).toBe(''); + expect(os.type).toBeCalled(); + }); + + test('getHelmDownloadURL() - return the URL to download helm for Linux', () => { + jest.spyOn(os, 'type').mockReturnValue('Linux'); + const kubectlLinuxUrl = 'https://get.helm.sh/helm-v3.2.1-linux-amd64.zip' + + expect(run.getHelmDownloadURL('v3.2.1')).toBe(kubectlLinuxUrl); + expect(os.type).toBeCalled(); + }); + + test('getHelmDownloadURL() - return the URL to download helm for Darwin', () => { + jest.spyOn(os, 'type').mockReturnValue('Darwin'); + const kubectlDarwinUrl = 'https://get.helm.sh/helm-v3.2.1-darwin-amd64.zip' + + expect(run.getHelmDownloadURL('v3.2.1')).toBe(kubectlDarwinUrl); + expect(os.type).toBeCalled(); + }); + + test('getHelmDownloadURL() - return the URL to download helm for Windows', () => { + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + + const kubectlWindowsUrl = 'https://get.helm.sh/helm-v3.2.1-windows-amd64.zip' + expect(run.getHelmDownloadURL('v3.2.1')).toBe(kubectlWindowsUrl); + expect(os.type).toBeCalled(); + }); + + test('getStableHelmVersion() - download stable version file, read version and return it', async () => { + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool'); + const response = JSON.stringify( + [ + { + 'tag_name': 'v4.0.0' + }, { + 'tag_name': 'v3.0.0' + }, { + 'tag_name': 'v2.0.0' + } + ] + ); + jest.spyOn(fs, 'readFileSync').mockReturnValue(response); + + expect(await run.getStableHelmVersion()).toBe('v4.0.0'); + expect(toolCache.downloadTool).toBeCalled(); + expect(fs.readFileSync).toBeCalledWith('pathToTool', 'utf8'); + }); + + test('getStableHelmVersion() - return default version if error occurs while getting latest version', async () => { + jest.spyOn(toolCache, 'downloadTool').mockRejectedValue('Unable to download'); + jest.spyOn(core, 'warning').mockImplementation(); + + expect(await run.getStableHelmVersion()).toBe('v3.2.1'); + expect(toolCache.downloadTool).toBeCalled(); + expect(core.warning).toBeCalledWith("Cannot get the latest Helm info from https://api.github.com/repos/helm/helm/releases. Error Unable to download. Using default Helm version v3.2.1."); + }); + + test('walkSync() - return path to the all files matching fileToFind in dir', () => { + jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => { + if (file == 'mainFolder') return ['file1' as unknown as fs.Dirent, 'file2' as unknown as fs.Dirent, 'folder1' as unknown as fs.Dirent, 'folder2' as unknown as fs.Dirent]; + if (file == path.join('mainFolder', 'folder1')) return ['file11' as unknown as fs.Dirent, 'file12' as unknown as fs.Dirent]; + if (file == path.join('mainFolder', 'folder2')) return ['file21' as unknown as fs.Dirent, 'file22' as unknown as fs.Dirent]; + }); + jest.spyOn(core, 'debug').mockImplementation(); + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = (file as string).toLowerCase().indexOf('file') == -1 ? true: false + return { isDirectory: () => isDirectory } as fs.Stats; + }); + + expect(run.walkSync('mainFolder', null, 'file21')).toEqual([path.join('mainFolder', 'folder2', 'file21')]); + expect(fs.readdirSync).toBeCalledTimes(3); + expect(fs.statSync).toBeCalledTimes(8); + }); + + test('walkSync() - return empty array if no file with name fileToFind exists', () => { + jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => { + if (file == 'mainFolder') return ['file1' as unknown as fs.Dirent, 'file2' as unknown as fs.Dirent, 'folder1' as unknown as fs.Dirent, 'folder2' as unknown as fs.Dirent]; + if (file == path.join('mainFolder', 'folder1')) return ['file11' as unknown as fs.Dirent, 'file12' as unknown as fs.Dirent]; + if (file == path.join('mainFolder', 'folder2')) return ['file21' as unknown as fs.Dirent, 'file22' as unknown as fs.Dirent]; + }); + jest.spyOn(core, 'debug').mockImplementation(); + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = (file as string).toLowerCase().indexOf('file') == -1 ? true: false + return { isDirectory: () => isDirectory } as fs.Stats; + }); + + expect(run.walkSync('mainFolder', null, 'helm.exe')).toEqual([]); + expect(fs.readdirSync).toBeCalledTimes(3); + expect(fs.statSync).toBeCalledTimes(8); + }); + + test('findHelm() - change access permissions and find the helm in given directory', () => { + jest.spyOn(fs, 'chmodSync').mockImplementation(() => {}); + jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => { + if (file == 'mainFolder') return ['helm.exe' as unknown as fs.Dirent]; + }); + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = (file as string).indexOf('folder') == -1 ? false: true + return { isDirectory: () => isDirectory } as fs.Stats; + }); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + + expect(run.findHelm('mainFolder')).toBe(path.join('mainFolder', 'helm.exe')); + }); + + test('findHelm() - throw error if executable not found', () => { + jest.spyOn(fs, 'chmodSync').mockImplementation(() => {}); + jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => { + if (file == 'mainFolder') return []; + }); + jest.spyOn(fs, 'statSync').mockImplementation((file) => { return { isDirectory: () => true } as fs.Stats}); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + expect(() => run.findHelm('mainFolder')).toThrow('Helm executable not found in path mainFolder'); + }); + + test('downloadHelm() - download helm and return path to it', async () => { + jest.spyOn(toolCache, 'find').mockReturnValue(''); + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool'); + const response = JSON.stringify([{'tag_name': 'v4.0.0'}]); + jest.spyOn(fs, 'readFileSync').mockReturnValue(response); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + jest.spyOn(fs, 'chmodSync').mockImplementation(() => {}); + jest.spyOn(toolCache, 'extractZip').mockResolvedValue('pathToUnzippedHelm'); + jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir'); + jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => ['helm.exe' as unknown as fs.Dirent]); + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = (file as string).indexOf('folder') == -1 ? false: true + return { isDirectory: () => isDirectory } as fs.Stats; + }); + + expect(await run.downloadHelm(null)).toBe(path.join('pathToCachedDir', 'helm.exe')); + expect(toolCache.find).toBeCalledWith('helm', 'v4.0.0'); + expect(toolCache.downloadTool).toBeCalledWith('https://get.helm.sh/helm-v4.0.0-windows-amd64.zip'); + expect(fs.chmodSync).toBeCalledWith('pathToTool', '777'); + expect(toolCache.extractZip).toBeCalledWith('pathToTool'); + expect(fs.chmodSync).toBeCalledWith(path.join('pathToCachedDir', 'helm.exe'), '777'); + }); + + test('downloadHelm() - throw error if unable to download', async () => { + jest.spyOn(toolCache, 'find').mockReturnValue(''); + jest.spyOn(toolCache, 'downloadTool').mockImplementation(async () => { throw 'Unable to download'}); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + + await expect(run.downloadHelm('v3.2.1')).rejects.toThrow('Failed to download Helm from location https://get.helm.sh/helm-v3.2.1-windows-amd64.zip'); + expect(toolCache.find).toBeCalledWith('helm', 'v3.2.1'); + expect(toolCache.downloadTool).toBeCalledWith('https://get.helm.sh/helm-v3.2.1-windows-amd64.zip'); + }); + + test('downloadHelm() - return path to helm tool with same version from toolCache', async () => { + jest.spyOn(toolCache, 'find').mockReturnValue('pathToCachedDir'); + jest.spyOn(fs, 'chmodSync').mockImplementation(() => {}); + + expect(await run.downloadHelm('v3.2.1')).toBe(path.join('pathToCachedDir', 'helm.exe')); + expect(toolCache.find).toBeCalledWith('helm', 'v3.2.1'); + expect(fs.chmodSync).toBeCalledWith(path.join('pathToCachedDir', 'helm.exe'), '777'); + }); + + test('downloadHelm() - throw error is helm is not found in path', async () => { + jest.spyOn(toolCache, 'find').mockReturnValue(''); + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool'); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + jest.spyOn(fs, 'chmodSync').mockImplementation(); + jest.spyOn(toolCache, 'extractZip').mockResolvedValue('pathToUnzippedHelm'); + jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir'); + jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => []); + jest.spyOn(fs, 'statSync').mockImplementation((file) => { + const isDirectory = (file as string).indexOf('folder') == -1 ? false: true + return { isDirectory: () => isDirectory } as fs.Stats; + }); + + await expect(run.downloadHelm('v3.2.1')).rejects.toThrow('Helm executable not found in path pathToCachedDir'); + expect(toolCache.find).toBeCalledWith('helm', 'v3.2.1'); + expect(toolCache.downloadTool).toBeCalledWith('https://get.helm.sh/helm-v3.2.1-windows-amd64.zip'); + expect(fs.chmodSync).toBeCalledWith('pathToTool', '777'); + expect(toolCache.extractZip).toBeCalledWith('pathToTool'); + }); +}); \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..286c327 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + clearMocks: true, + moduleFileExtensions: ['js', 'ts'], + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.ts$': 'ts-jest' + }, + verbose: true, + coverageThreshold: { + "global": { + "branches": 0, + "functions": 14, + "lines": 27, + "statements": 27 + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 9d3d9a0..e91075e 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,14 @@ "main": "lib/run.js", "scripts": { "build": "tsc", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest", + "test-coverage": "jest --coverage" }, "devDependencies": { "@types/node": "^12.0.10", - "typescript": "^3.5.2" + "typescript": "^3.5.2", + "jest": "^26.0.1", + "@types/jest": "^25.2.2", + "ts-jest": "^25.5.1" } } diff --git a/src/run.ts b/src/run.ts index c689085..616d1b5 100644 --- a/src/run.ts +++ b/src/run.ts @@ -20,14 +20,14 @@ const LATEST_HELM2_VERSION = '2.*'; const LATEST_HELM3_VERSION = '3.*'; const helmAllReleasesUrl = 'https://api.github.com/repos/helm/helm/releases'; -function getExecutableExtension(): string { +export function getExecutableExtension(): string { if (os.type().match(/^Win/)) { return '.exe'; } return ''; } -function getHelmDownloadURL(version: string): string { +export function getHelmDownloadURL(version: string): string { switch (os.type()) { case 'Linux': return util.format('https://get.helm.sh/helm-%s-linux-amd64.zip', version); @@ -66,7 +66,7 @@ export async function getStableHelmVersion(): Promise { return stableHelmVersion; } -var walkSync = function (dir, filelist, fileToFind) { +export var walkSync = function (dir, filelist, fileToFind) { var files = fs.readdirSync(dir); filelist = filelist || []; files.forEach(function (file) { @@ -83,15 +83,15 @@ var walkSync = function (dir, filelist, fileToFind) { return filelist; }; -async function downloadHelm(version: string): Promise { - if (!version) { version = await getLatestHelmVersionFor("v3"); } +export async function downloadHelm(version: string): Promise { + if (!version) { version = await getStableHelmVersion(); } let cachedToolpath = toolCache.find(helmToolName, version); if (!cachedToolpath) { let helmDownloadPath; try { helmDownloadPath = await toolCache.downloadTool(getHelmDownloadURL(version)); } catch (exception) { - throw new Error(util.format("Failed to download Helm from location ", getHelmDownloadURL(version))); + throw new Error(util.format("Failed to download Helm from location", getHelmDownloadURL(version))); } fs.chmodSync(helmDownloadPath, '777'); @@ -101,7 +101,7 @@ async function downloadHelm(version: string): Promise { const helmpath = findHelm(cachedToolpath); if (!helmpath) { - throw new Error(util.format("Helm executable not found in path ", cachedToolpath)); + throw new Error(util.format("Helm executable not found in path", cachedToolpath)); } fs.chmodSync(helmpath, '777'); @@ -151,19 +151,19 @@ function isValidVersion(version: string, type: string): boolean { return version.indexOf('rc') == -1; } -function findHelm(rootFolder: string): string { +export function findHelm(rootFolder: string): string { fs.chmodSync(rootFolder, '777'); var filelist: string[] = []; walkSync(rootFolder, filelist, helmToolName + getExecutableExtension()); - if (!filelist) { - throw new Error(util.format("Helm executable not found in path ", rootFolder)); + if (!filelist || filelist.length == 0) { + throw new Error(util.format("Helm executable not found in path", rootFolder)); } else { return filelist[0]; } } -async function run() { +export async function run() { let version = core.getInput('version', { 'required': true }); if (process.env['HELM_INSTALLER_LEGACY_VERSIONING'] == 'true') { diff --git a/tsconfig.json b/tsconfig.json index a82bd17..5bcb480 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,7 +45,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */