From 56b95ecccca989b4509ff84c2e1c152c890af620 Mon Sep 17 00:00:00 2001 From: Sundar Date: Fri, 26 Mar 2021 11:36:55 +0530 Subject: [PATCH] Added L0 tests. (#22) * Added L0 tests. * Updated according to review comments. --- .github/workflows/test.yml | 81 ++------------ .gitignore | 1 + __tests__/run.test.ts | 204 ++++++++++++++++++++++++++++++++++-- __tests__/sample-secret.yml | 35 +++++++ src/login.ts | 8 +- tsconfig.json | 2 +- 6 files changed, 250 insertions(+), 81 deletions(-) create mode 100644 __tests__/sample-secret.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d8b4441..075a04b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,80 +1,21 @@ -on: +name: "build-test" +on: # rebuild any PRs and main branch changes pull_request: + branches: + - master + - 'releases/*' push: branches: - master - 'releases/*' jobs: - build_test_job: - name: 'Build and test job' - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [windows-latest, ubuntu-latest, macos-latest] + build: # make sure build/ci works properly + runs-on: ubuntu-latest steps: - - - name: 'Checking out repo code' - uses: actions/checkout@v1 - - - name: Extract branch name - id: extract_branch - run: | - echo "##[set-output name=branchname;]$(echo ${GITHUB_REF##*/})" - - - name: 'Install dependency for master' - if: github.event.pull_request.base.ref == 'master' || steps.extract_branch.outputs.branchname == 'master' + - uses: actions/checkout@v1 + - name: build and run tests run: | npm install - - - name: 'Install dependency for releases' - if: github.event.pull_request.base.ref == 'releases/v1' || steps.extract_branch.outputs.branchname == 'releases/v1' - run: | - npm install --only=dev - - - name: 'Run L0 tests' - run: | - npm run test - - name : 'Run test coverage' - if: runner.os == 'Windows' && (github.event.pull_request.base.ref == 'releases/v1' || github.event.pull_request.base.ref == 'master') - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - $coverage_result = npm run test-coverage - $start = $false; - $middle = $false; - $end = $false; - $count = 0; - - foreach ($j in $coverage_result) - { - if ($j.tostring().startswith("----------")) - { - if (!$start) - { - $start = $true; - $start_index = $count - } - elseif (!$middle) - { - $middle = $true; - } - elseif (!$end) - { - $end = $true; - $end_index = $count - } - } - $count++ - } - - $tbl_md = $coverage_result[($start_index+1)..($end_index-1)] -join "\n" - $summary = $coverage_result[($end_index + 1)..$count] -join "\n" - $comment = $tbl_md + "\n" + $summary - $url = "https://api.github.com/repos/${env:GITHUB_REPOSITORY}/issues/${env:PR_NUMBER}/comments" - $headers = @{ - "Authorization" = "token ${env:GITHUB_TOKEN}" - } - $body = "{ `"body`": `"${comment}`" }" - Invoke-RestMethod -Method POST -Uri $url -Headers $headers -Body $body \ No newline at end of file + npm build + npm test diff --git a/.gitignore b/.gitignore index d109d827..794407c2 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,4 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ node_modules +coverage diff --git a/__tests__/run.test.ts b/__tests__/run.test.ts index 4db99d1e..1b861179 100644 --- a/__tests__/run.test.ts +++ b/__tests__/run.test.ts @@ -1,7 +1,199 @@ -import { run } from '../src/login' +import * as run from '../src/login' +import * as os from 'os'; +import * as io from '@actions/io'; +import * as toolCache from '@actions/tool-cache'; +import * as core from '@actions/core'; +import * as fs from 'fs'; +import * as jsyaml from 'js-yaml'; +import * as path from 'path'; -describe('This is a placeholder for intial test cases, to be removed', () => { - test('Dummy test case', async () => { - await expect(run()).rejects.toThrow(); - }) -}) \ No newline at end of file +var mockStatusCode; +const mockExecFn = jest.fn().mockImplementation(() => mockStatusCode); +jest.mock('@actions/exec/lib/toolrunner', () => { + return { + ToolRunner: jest.fn().mockImplementation(() => { + return { + exec: mockExecFn + } + }) + } +}); + + +describe('Testing all functions.', () => { + 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('getKubectlPath() - return path to existing kubectl', async () => { + jest.spyOn(io, 'which').mockResolvedValue('pathToKubectl'); + + expect(await run.getKubectlPath()).toBe('pathToKubectl'); + expect(io.which).toBeCalled(); + }); + + test('getKubectlPath() - throw error when kubectl not installed', async () => { + jest.spyOn(io, 'which').mockResolvedValue(''); + jest.spyOn(toolCache, 'findAllVersions').mockReturnValue([]); + + await expect(run.getKubectlPath()).rejects.toThrow('Kubectl is not installed'); + expect(io.which).toBeCalled(); + expect(toolCache.findAllVersions).toBeCalled(); + }); + + test('getKubectlPath() - return path to kubectl in toolCache', async () => { + jest.spyOn(io, 'which').mockResolvedValue(''); + jest.spyOn(toolCache, 'findAllVersions').mockReturnValue(['v1.15.0']); + jest.spyOn(toolCache, 'find').mockReturnValue('pathToTool'); + jest.spyOn(os, 'type').mockReturnValue('Windows_NT'); + + expect(await run.getKubectlPath()).toBe(path.join('pathToTool', 'kubectl.exe')); + expect(io.which).toBeCalled(); + expect(toolCache.findAllVersions).toBeCalled(); + expect(toolCache.find).toBeCalledWith('kubectl', 'v1.15.0'); + }); + + test('getKubeconfig() - throw error on invalid input', () => { + jest.spyOn(core, 'getInput').mockReturnValue('invalid'); + + expect(() => run.getKubeconfig()).toThrow('Invalid method specified. Acceptable values are kubeconfig and service-account.'); + expect(core.getInput).toBeCalled(); + }); + + test('getKubeconfig() - return kubeconfig from input', () => { + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'kubeconfig'; + if (inputName == 'kubeconfig') return '###'; + }); + jest.spyOn(core, 'debug').mockImplementation(); + + expect(run.getKubeconfig()).toBe('###'); + expect(core.getInput).toBeCalledWith('method', { required: true }); + expect(core.getInput).toBeCalledWith('kubeconfig', { required: true }); + }); + + test('getKubeconfig() - create kubeconfig from secret provided and return it', () => { + jest.spyOn(core, 'debug').mockImplementation(); + const k8Secret = fs.readFileSync('__tests__/sample-secret.yml').toString(); + const kubeconfig = JSON.stringify({ + "apiVersion": "v1", + "kind": "Config", + "clusters": [ + { + "cluster": { + "certificate-authority-data": 'LS0tLS1CRUdJTiBDRWyUSUZJQ', + "server": 'https://testing-dns-4za.hfp.earth.azmk8s.io:443' + } + } + ], + "users": [ + { + "user": { + "token": Buffer.from('ZXlKaGJHY2lPcUpTVXpJMU5pSX=', 'base64').toString() + } + } + ] + }); + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'service-account'; + if (inputName == 'k8s-url') return 'https://testing-dns-4za.hfp.earth.azmk8s.io:443'; + if (inputName == 'k8s-secret') return k8Secret; + }); + + expect(run.getKubeconfig()).toBe(kubeconfig); + expect(core.getInput).toBeCalledTimes(3); + }); + + test('getKubeconfig() - throw error if empty config provided', () => { + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'service-account'; + if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443'; + if (inputName == 'k8s-secret') return ''; + }); + + expect(() => run.getKubeconfig()).toThrow('The service account secret yaml specified is invalid. Make sure that its a valid yaml and try again.'); + expect(core.getInput).toBeCalledTimes(3); + }); + + test('getKubeconfig() - throw error if data field doesn\'t exist', () => { + var k8SecretYaml = fs.readFileSync('__tests__/sample-secret.yml').toString(); + var k8SecretObject = jsyaml.safeLoad(k8SecretYaml); + delete k8SecretObject['data']; + k8SecretYaml = jsyaml.dump(k8SecretObject); + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'service-account'; + if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443'; + if (inputName == 'k8s-secret') return k8SecretYaml; + }); + + expect(() => run.getKubeconfig()).toThrow('The service account secret yaml does not contain data; field. Make sure that its present and try again.'); + expect(core.getInput).toBeCalledTimes(3); + }); + + test('getKubeconfig() - throw error if data.token field doesn\'t exist', () => { + var k8SecretYaml = fs.readFileSync('__tests__/sample-secret.yml').toString(); + var k8SecretObject = jsyaml.safeLoad(k8SecretYaml); + delete k8SecretObject['data']['token']; + k8SecretYaml = jsyaml.dump(k8SecretObject); + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'service-account'; + if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443'; + if (inputName == 'k8s-secret') return k8SecretYaml; + }); + + expect(() => run.getKubeconfig()).toThrow('The service account secret yaml does not contain data.token; field. Make sure that its present and try again.'); + expect(core.getInput).toBeCalledTimes(3); + }); + + test('getKubeconfig() - throw error if data[ca.crt] field doesn\'t exist', () => { + var k8SecretYaml = fs.readFileSync('__tests__/sample-secret.yml').toString(); + var k8SecretObject = jsyaml.safeLoad(k8SecretYaml); + delete k8SecretObject['data']['ca.crt']; + k8SecretYaml = jsyaml.dump(k8SecretObject); + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'service-account'; + if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443'; + if (inputName == 'k8s-secret') return k8SecretYaml; + }); + + expect(() => run.getKubeconfig()).toThrow('The service account secret yaml does not contain data[ca.crt]; field. Make sure that its present and try again.'); + expect(core.getInput).toBeCalledTimes(3); + }); + + test('setContext() - set context using kubectl', async () => { + jest.spyOn(core, 'getInput').mockReturnValue('abc'); + jest.spyOn(io, 'which').mockResolvedValue('pathToKubectl'); + mockStatusCode = 0; + + expect(await run.setContext('/pathToKubeconfig')); + expect(mockExecFn).toBeCalledTimes(2); + }); + + test('run() - create kubeconfig, exportvariable and give appropriate access', async () => { + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + jest.spyOn(fs, 'chmodSync').mockImplementation(() => {}); + jest.spyOn(core, 'exportVariable').mockImplementation(() => {}); + jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => { + if (inputName == 'method') return 'kubeconfig'; + if (inputName == 'kubeconfig') return '###'; + if (inputName == 'context') return ''; + }); + process.env['RUNNER_TEMP'] = 'tempDirPath' + jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567); + + expect(run.run()); + expect(fs.writeFileSync).toHaveBeenCalledWith(path.join('tempDirPath', 'kubeconfig_1234561234567'), '###'); + expect(fs.chmodSync).toHaveBeenCalledWith(path.join('tempDirPath', 'kubeconfig_1234561234567'), '600'); + expect(core.exportVariable).toHaveBeenCalledWith('KUBECONFIG', path.join('tempDirPath', 'kubeconfig_1234561234567')); + }); +}); diff --git a/__tests__/sample-secret.yml b/__tests__/sample-secret.yml new file mode 100644 index 00000000..e7674a66 --- /dev/null +++ b/__tests__/sample-secret.yml @@ -0,0 +1,35 @@ +apiVersion: v1 +data: + ca.crt: LS0tLS1CRUdJTiBDRWyUSUZJQ + namespace: ZGVmBXUsLdA== + token: ZXlKaGJHY2lPcUpTVXpJMU5pSX= +kind: Secret +metadata: + annotations: + kubernetes.io/service-account.name: default + kubernetes.io/service-account.uid: e1414a3z-22fe-48d1-ab9e-18e4a5b91c + creationTimestamp: "2020-03-02T06:40:31Z" + managedFields: + - apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:data: + .: {} + f:ca.crt: {} + f:namespace: {} + f:token: {} + f:metadata: + f:annotations: + .: {} + f:kubernetes.io/service-account.name: {} + f:kubernetes.io/service-account.uid: {} + f:type: {} + manager: kube-controller-manager + operation: Update + time: "2020-03-02T06:40:31Z" + name: default-token-bl8ra + namespace: default + resourceVersion: "278" + selfLink: /api/v1/namespaces/default/secrets/default-token-bl8ra + uid: e6d8b21b-2e3a-4606-98za-54fb44fdc +type: kubernetes.io/service-account-token diff --git a/src/login.ts b/src/login.ts index 747d72cf..886cb6be 100644 --- a/src/login.ts +++ b/src/login.ts @@ -8,7 +8,7 @@ import { ToolRunner } from "@actions/exec/lib/toolrunner"; import * as jsyaml from 'js-yaml'; import * as util from 'util'; -function getKubeconfig(): string { +export function getKubeconfig(): string { const method = core.getInput('method', {required: true}); if (method == 'kubeconfig') { const kubeconfig = core.getInput('kubeconfig', {required : true}); @@ -66,7 +66,7 @@ function getKubeconfig(): string { } } -function getExecutableExtension(): string { +export function getExecutableExtension(): string { if (os.type().match(/^Win/)) { return '.exe'; } @@ -74,7 +74,7 @@ function getExecutableExtension(): string { return ''; } -async function getKubectlPath() { +export async function getKubectlPath() { let kubectlPath = await io.which('kubectl', false); if (!kubectlPath) { const allVersions = toolCache.findAllVersions('kubectl'); @@ -88,7 +88,7 @@ async function getKubectlPath() { return kubectlPath; } -async function setContext(kubeconfigPath: string) { +export async function setContext(kubeconfigPath: string) { let context = core.getInput('context'); if (context) { //To use kubectl commands, the environment variable KUBECONFIG needs to be set for this step diff --git a/tsconfig.json b/tsconfig.json index 3555b597..9471851e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,4 +7,4 @@ "node_modules", "__tests__" ] -} \ No newline at end of file +}