From e7a0fa18faef9971a60f30d541bf95d972ca20c8 Mon Sep 17 00:00:00 2001 From: anirudhprasad-sap <126493692+anirudhprasad-sap@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:49:18 +0200 Subject: [PATCH] [Misc] Improve code coverage (#16) --- bin/cap-op-plugin.js | 7 +-- package-lock.json | 120 +++++++++++++++++++++++++++++++++++- package.json | 3 +- test/add.test.js | 15 ----- test/cap-op-plugin.test.js | 100 ++++++++++++++++++++++++++++++ test/util.test.js | 123 +++++++++++++++++++++++++++++++++++++ 6 files changed, 347 insertions(+), 21 deletions(-) create mode 100644 test/cap-op-plugin.test.js create mode 100644 test/util.test.js diff --git a/bin/cap-op-plugin.js b/bin/cap-op-plugin.js index b603411..4682899 100755 --- a/bin/cap-op-plugin.js +++ b/bin/cap-op-plugin.js @@ -1,16 +1,16 @@ #!/usr/bin/env node /* eslint-disable no-console */ +const isCli = require.main === module const cds = require('@sap/cds-dk') const yaml = require('@sap/cds-foss').yaml const Mustache = require('mustache') + const { ask, mergeObj, isCAPOperatorChart } = require('../lib/util') -const isCli = require.main === module const SUPPORTED = { 'generate-runtime-values': ['--with-input-yaml'] } async function capOperatorPlugin(cmd, option, inputYamlPath) { - try { if (!cmd) return _usage() if (!Object.keys(SUPPORTED).includes(cmd)) return _usage(`Unknown command ${cmd}.`) @@ -54,8 +54,7 @@ EXAMPLES } async function generateRuntimeValues(option, inputYamlPath) { - - if (!((cds.utils.exists('chart') && isCAPOperatorChart('chart')))) { + if (!((cds.utils.exists('chart') && isCAPOperatorChart(cds.utils.path.join(cds.root,'chart'))))) { throw new Error("No CAP Operator chart found in the project. Please run 'cds add cap-operator --force' to add the CAP Operator chart folder.") } diff --git a/package-lock.json b/package-lock.json index f80848b..8f2e093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,14 @@ "dependencies": { "mustache": "^4.2.0" }, + "bin": { + "cap-op-plugin": "bin/cap-op-plugin.js" + }, "devDependencies": { "chai": "^4.3.10", "mocha": "^10.3.0", - "nyc": "^15.1.0" + "nyc": "^15.1.0", + "sinon": "^18.0.0" }, "peerDependencies": { "@sap/cds": ">=7", @@ -4287,6 +4291,50 @@ "node": ">=14" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5775,6 +5823,12 @@ "node": ">=6" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5796,6 +5850,12 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6001,6 +6061,25 @@ "node": ">= 0.6" } }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -6798,6 +6877,45 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 95776e5..50d17a4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "devDependencies": { "chai": "^4.3.10", "mocha": "^10.3.0", - "nyc": "^15.1.0" + "nyc": "^15.1.0", + "sinon": "^18.0.0" }, "scripts": { "test": "mocha --timeout 20000", diff --git a/test/add.test.js b/test/add.test.js index 5824f01..c2ad8d4 100644 --- a/test/add.test.js +++ b/test/add.test.js @@ -106,19 +106,4 @@ describe('cds add cap-operator', () => { expect(getFileHash(join(__dirname,'files/expectedChart/values.schema.json'))).to.equal(getFileHash(join(bookshop, 'chart/values.schema.json'))) expect(getFileHash(join(__dirname,'files/expectedChart/valuesWithDestination.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/values.yaml'))) }) - - it('Generate runtime-values file', async () => { - await cds.utils.copy(join('test/files', 'input_values.yaml'), join(bookshop, 'input_values.yaml')) - execSync(`cds add cap-operator`, { cwd: bookshop }) - execSync(`npx cap-op-plugin generate-runtime-values --with-input-yaml input_values.yaml`, { cwd: bookshop }) - - expect(getFileHash(join(__dirname,'files/expectedChart/runtime-values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/runtime-values.yaml'))) - }) - - it('Generate runtime-values file using wrong input_values.yaml', async () => { - await cds.utils.copy(join('test/files', 'input_values_wrong.yaml'), join(bookshop, 'input_values_wrong.yaml')) - execSync(`cds add cap-operator`, { cwd: bookshop }) - - expect(() => execSync(`npx cap-op-plugin generate-runtime-values --with-input-yaml input_values_wrong.yaml`, { cwd: bookshop })).to.throw(`'appName', 'capOperatorSubdomain', 'clusterDomain', 'globalAccountId', 'providerSubdomain' and 'tenantId' are mandatory fields in the input yaml file.`) - }) }) diff --git a/test/cap-op-plugin.test.js b/test/cap-op-plugin.test.js new file mode 100644 index 0000000..7a84658 --- /dev/null +++ b/test/cap-op-plugin.test.js @@ -0,0 +1,100 @@ +const cds = require('@sap/cds-dk') +const { join } = require('path') +const { execSync } = require('child_process') +const { expect } = require("chai") +const readline = require('readline') +const sinon = require('sinon') + +const TempUtil = require('./tempUtil') +const tempUtil = new TempUtil(__filename, { local: true }) + +const { getFileHash, updateDependency, setupHack, undoSetupHack } = require('./util') +const { capOperatorPlugin } = require('../bin/cap-op-plugin') + +describe('cap-op-plugin', () => { + let temp, bookshop + let rlInterface + let rlQuestion + + before(async () => { + await tempUtil.cleanUp() + temp = await tempUtil.mkTempFolder() + bookshop = join(temp, 'bookshop') + execSync(`cds init bookshop --add multitenancy,approuter,xsuaa,html5-repo`, { cwd: temp }) + updateDependency(bookshop) + execSync(`npm install`, { cwd: bookshop }) + setupHack(bookshop) + }) + + afterEach(async () => { + if (cds.utils.exists(join(bookshop, 'chart'))) execSync(`rm -r chart`, { cwd: bookshop }) + }) + + after(async () => { + undoSetupHack(bookshop) + await tempUtil.cleanUp() + }) + + it('Generate runtime-values file', async () => { + await cds.utils.copy(join('test/files', 'input_values.yaml'), join(bookshop, 'input_values.yaml')) + execSync(`cds add cap-operator`, { cwd: bookshop }) + execSync(`npx cap-op-plugin generate-runtime-values --with-input-yaml input_values.yaml`, { cwd: bookshop }) + + expect(getFileHash(join(__dirname, 'files/expectedChart/runtime-values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/runtime-values.yaml'))) + }) + + it('Generate runtime-values file using wrong input_values.yaml', async () => { + await cds.utils.copy(join('test/files', 'input_values_wrong.yaml'), join(bookshop, 'input_values_wrong.yaml')) + execSync(`cds add cap-operator`, { cwd: bookshop }) + + expect(() => execSync(`npx cap-op-plugin generate-runtime-values --with-input-yaml input_values_wrong.yaml`, { cwd: bookshop })).to.throw(`'appName', 'capOperatorSubdomain', 'clusterDomain', 'globalAccountId', 'providerSubdomain' and 'tenantId' are mandatory fields in the input yaml file.`) + }) + + it('Generate runtime-values without chart', async () => { + expect(() => execSync(`npx cap-op-plugin generate-runtime-values`, { cwd: bookshop })).to.throw(`No CAP Operator chart found in the project. Please run 'cds add cap-operator --force' to add the CAP Operator chart folder.`) + }) + + it('Generate runtime-values usage help', async () => { + expect(() => execSync(`npx cap-op-plugin`, { cwd: bookshop })).to.throw(` +USAGE + + cap-op-plugin [--with-input-yaml ] + +COMMANDS + + generate-runtime-values generate runtime-values.yaml file for the cap-operator chart + +EXAMPLES + + cap-op-plugin generate-runtime-values + cap-op-plugin generate-runtime-values --with-input-yaml /path/to/input.yaml +`) + }) + + it('Generate runtime-values via prompts', async () => { + execSync(`cds add cap-operator`, { cwd: bookshop }) + + rlQuestion = sinon.stub() + rlInterface = { + question: rlQuestion, + close: sinon.stub() + } + sinon.stub(readline, 'createInterface').returns(rlInterface) + + rlQuestion.onFirstCall().callsArgWith(1, 'bkshop') + rlQuestion.onSecondCall().callsArgWith(1, '') + rlQuestion.onThirdCall().callsArgWith(1, 'c-abc.kyma.ondemand.com') + rlQuestion.onCall(3).callsArgWith(1, 'dc94db56-asda-adssa-dada-123456789012') + rlQuestion.onCall(4).callsArgWith(1, 'bem-aad-sadad-123456789012') + rlQuestion.onCall(5).callsArgWith(1, 'dasdsd-1234-1234-1234-123456789012') + rlQuestion.onCall(6).callsArgWith(1, 'sdasd-4c4d-4d4d-4d4d-123456789012') + rlQuestion.onCall(7).callsArgWith(1, 'regcred') + + cds.root = bookshop + await capOperatorPlugin('generate-runtime-values') + + expect(getFileHash(join(__dirname, 'files/expectedChart/runtime-values.yaml'))).to.equal(getFileHash(join(bookshop, 'chart/runtime-values.yaml'))) + + sinon.restore() + }) +}) diff --git a/test/util.test.js b/test/util.test.js new file mode 100644 index 0000000..9aed6c7 --- /dev/null +++ b/test/util.test.js @@ -0,0 +1,123 @@ +const { expect } = require("chai") +const { ask, mergeObj } = require("../lib/util") +const readline = require('readline') + +const sinon = require('sinon') + +describe("ask function", () => { + let rlInterface + let rlQuestion + + beforeEach(() => { + rlQuestion = sinon.stub() + rlInterface = { + question: rlQuestion, + close: sinon.stub() + } + sinon.stub(readline, 'createInterface').returns(rlInterface) + }) + + afterEach(() => { + sinon.restore() + }) + + it('should prompt the user with the given questions and suggestions', async () => { + const questions = [ + ['What is your name?', 'John Doe', true], + ['What is your age?', '30', true] + ] + + rlQuestion.onFirstCall().callsArgWith(1, 'Alice') + rlQuestion.onSecondCall().callsArgWith(1, '25') + + const answers = await ask(...questions) + + expect(answers).to.deep.equal(['Alice', '25']) + }) + + it('should use the suggestion if no input is provided and it is not mandatory', async () => { + const questions = [ + ['What is your name?', 'John Doe', false], + ['What is your age?', '30', false] + ] + + rlQuestion.onFirstCall().callsArgWith(1, '') + rlQuestion.onSecondCall().callsArgWith(1, '') + + const answers = await ask(...questions) + + expect(answers).to.deep.equal(['John Doe', '30']) + }) + + it('should re-ask mandatory questions if no input is provided', async () => { + const questions = [ + ['What is your name?', '', true], + ['What is your age?', '', true] + ] + + rlQuestion.onFirstCall().callsArgWith(1, '') + rlQuestion.onSecondCall().callsArgWith(1, 'Alice') + rlQuestion.onThirdCall().callsArgWith(1, '25') + + const answers = await ask(...questions) + + expect(answers).to.deep.equal(['Alice', '25']) + }) +}) + +describe("mergeObj function", () => { + it("should merge two objects recursively", () => { + const source = { + prop1: "value1", + prop2: { + nestedProp1: "nestedValue1", + nestedProp2: "nestedValue2" + }, + prop3: ["item1", "item2"] + } + const target = { + prop1: "value2", + prop2: { + nestedProp1: "nestedValue3", + nestedProp3: "nestedValue4" + }, + prop3: ["item3", "item4"] + } + const expected = { + prop1: "value2", + prop2: { + nestedProp1: "nestedValue3", + nestedProp2: "nestedValue2", + nestedProp3: "nestedValue4" + }, + prop3: ["item1", "item2", "item3", "item4"] + } + const result = mergeObj(source, target) + expect(result).to.deep.equal(expected) + }) + + it("should handle empty objects and arrays", () => { + const source = {} + const target = [] + const expected = [] + const result = mergeObj(source, target) + expect(result).to.deep.equal(expected) + }) + + it("should handle null and undefined values", () => { + const source = { + prop1: null, + prop2: undefined + } + const target = { + prop1: "value1", + prop2: "value2" + } + const expected = { + prop1: "value1", + prop2: "value2" + } + const result = mergeObj(source, target) + expect(result).to.deep.equal(expected) + }) +})