biz-a-cli 2.3.16 → 2.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/app.js ADDED
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from "yargs"
4
+ import axios from "axios"
5
+ import fs from "fs"
6
+ import { runInNewContext } from "vm"
7
+ import uglify from "uglify-js"
8
+ import * as tar from "tar"
9
+ import { verify, sign, privateDecrypt, constants as cryptoConstants, randomBytes, createCipheriv } from "node:crypto"
10
+ import { basename } from "node:path"
11
+ import { env } from "../envs/env.js"
12
+ import * as vm from 'node:vm'
13
+
14
+ const keyFolderPath = process.argv[1].substring(0, process.argv[1].lastIndexOf("\\bin")) + "\\key"
15
+
16
+ yargs(process.argv.slice(2))
17
+ .option("s", {
18
+ alias: "server",
19
+ describe: "Server URL (ex: https://biz-a.herokuapp.com or http://192.168.1.1 or https://finaapi.imamatek.com)",
20
+ type: "string",
21
+ demandOption: true
22
+ })
23
+ .option("i", {
24
+ alias: "dbIndex",
25
+ describe: "database index",
26
+ type: "number",
27
+ demandOption: true
28
+ })
29
+ .option("sub", {
30
+ alias: "subdomain",
31
+ describe: "Subdomain",
32
+ type: "string",
33
+ demandOption: false
34
+ })
35
+ .option("p", {
36
+ alias: "apiPort",
37
+ describe: "FINA API Port",
38
+ type: "string",
39
+ demandOption: false,
40
+ default : "212"
41
+ })
42
+ .command('add', 'Add Biz-A Application',
43
+ {
44
+ 'd': {
45
+ alias: "workingDir",
46
+ describe: "Path to templates directory",
47
+ type: "string",
48
+ demandOption: false,
49
+ default: process.cwd(),
50
+ },
51
+ 'v': {
52
+ alias: "verbose",
53
+ describe: "Print info to console",
54
+ type: "boolean",
55
+ demandOption: false,
56
+ default: false,
57
+ }
58
+ },
59
+ async (options)=>{
60
+
61
+ function printInfo(msg){if (options.verbose) {console.log(msg)}}
62
+
63
+ const prepareKeys = async ()=>{
64
+ const data = Buffer.from(JSON.stringify({issuer: 'CLI', acquirer: 'Client'})).toString('base64')
65
+ const privateKey = fs.readFileSync(`${keyFolderPath}\\cliPrivate.pem`)
66
+ const signature = sign('sha256', data, {key: privateKey, passphrase: 'Biz-A@cli', padding: cryptoConstants.RSA_PKCS1_PSS_PADDING}).toString('base64')
67
+ const res = await axios.get(env.BIZA_SERVER_LINK+'/api/issuerKey', {params: {data, signature}})
68
+ if ((res.data.data!=null) && verify('sha256', res.data.data, {key: fs.readFileSync(`${keyFolderPath}\\serverPublic.pem`), padding: cryptoConstants.RSA_PKCS1_PSS_PADDING}, Buffer.from(res.data.signature, 'base64'))) {
69
+ const resData = JSON.parse(Buffer.from(res.data.data, 'base64').toString())
70
+ const decryptedAESKey = privateDecrypt({key: privateKey, passphrase: 'Biz-A@cli', padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING}, Buffer.from(resData.issuer.key, 'base64')).toString()
71
+ const cliSignature = (signedData)=>sign('sha256', signedData, {key: privateKey, passphrase: 'Biz-A@cli', padding: cryptoConstants.RSA_PKCS1_PSS_PADDING}).toString('base64')
72
+ const acquirerData = Buffer.from(JSON.stringify(resData.acquirer)).toString('base64')
73
+ const signature = cliSignature(acquirerData)
74
+ return {encryptKey: decryptedAESKey, metadata: {acquirer: {data: acquirerData, signature}}}
75
+ }
76
+ else {
77
+ return null
78
+ }
79
+ }
80
+
81
+ function replacer(key, value) {
82
+ /*
83
+ // with line break
84
+ if (typeof value == 'function') {
85
+ let arr = value.toString().replace(/(\r\n|\n|\r)/gm, "°").split("°");
86
+ if (arr.length < 3) throw 'Function must be minimal 3 lines';
87
+ return [
88
+ 'window.Function',
89
+ getParam(arr[0]),
90
+ arr.slice(1, arr.length - 1)
91
+ ];
92
+ } else {
93
+ return value;
94
+ }
95
+ */
96
+
97
+ // without line break
98
+ if (typeof value == 'function') {
99
+ const fnScript = value.toString()
100
+ let params, body = ''
101
+ if (fnScript.indexOf('{')==-1) {
102
+ const arr = fnScript.split('=>')
103
+ params = arr[0]
104
+ body = arr.slice(1).join()
105
+ } else {
106
+ params = fnScript.split('{')[0]
107
+ body = fnScript.substring(fnScript.indexOf('{')+1, fnScript.lastIndexOf('}'))
108
+ }
109
+ params = params.replace(/(\s|=>|\(|\)|function)/gim, '')
110
+ printInfo(['window.Function', params, body])
111
+ return ['window.Function', params, body]
112
+ } else {
113
+ return value;
114
+ }
115
+ }
116
+
117
+ const minifiedIt = async (fileName) => {
118
+ const dataFile = fs.readFileSync(fileName);
119
+ printInfo(`===================\n${fileName.toUpperCase()}\n===================`)
120
+
121
+ const minifyResult = uglify.minify(dataFile.toString(), {compress: false, mangle: false})
122
+ if (minifyResult.error ) {throw (`${fileName} : ` + minifyResult.error)}
123
+ let jsMinData = minifyResult.code
124
+ jsMinData = jsMinData.replace('module.exports=get;', '') // Ex : at "executeBlockLib.js" of Imamatek App, will throw error. We need this line code for testing or debugging
125
+ const minifiedBuffer = Buffer.from(jsMinData)
126
+ printInfo(['Minify : \n', minifiedBuffer.toString()].join(''))
127
+
128
+ let stringifyData = ''
129
+ const isJsonFile = fileName.split('.').pop().toLowerCase().match(/^(json)$/)
130
+
131
+ // compile using node.js VM
132
+ try {
133
+ printInfo('Running script with VM')
134
+ if (isJsonFile) {
135
+ const vmResult = runInNewContext(minifiedBuffer.toString())
136
+ stringifyData = vmResult ? JSON.stringify((typeof vmResult=='function') ? vmResult() : (vmResult || ''), replacer) : ''
137
+ } else {
138
+ // for handling "get" function in local scope (let, var, const)
139
+ const sandbox = vm.createContext({script: ''});
140
+ vm.runInContext(
141
+ minifiedBuffer.toString() + "\n script = (typeof get=='undefined') ? '' : (typeof get=='function') ? get() : (get||'')",
142
+ sandbox
143
+ )
144
+ stringifyData = JSON.stringify(sandbox.script, replacer)
145
+ }
146
+ } catch (err){
147
+ printInfo(`${fileName} : ` + err) // Do not log it as error, we will try to compile the script using ES6 Import
148
+ stringifyData = ''
149
+ }
150
+
151
+ const isEmptyScript = (scriptText)=>((typeof scriptText=='undefined') || (scriptText===null) || (scriptText.trim()==='""') || (typeof scriptText=='string' && scriptText.trim().length==0))
152
+
153
+ // compile using ES6 Import
154
+ if (isEmptyScript(stringifyData)) {
155
+ try {
156
+ printInfo('Running script with Import function')
157
+ const lib = await import(`file://${process.cwd()}\\${fileName}`, isJsonFile ? {assert: {type: "json"}} : {})
158
+ stringifyData = JSON.stringify((typeof lib.default=='function') ? lib.default() : (lib.default || ''), replacer).replace(/ /g,'') // remove trailing whitespace
159
+ // stringifyData = stringifyData.replace(/\\r\\n/g,'') // remove line break. We can not do this because "inline css" still requires line breaks
160
+ } catch (error){
161
+ throw(`${fileName} : ` + error)
162
+ }
163
+ }
164
+
165
+ if (isEmptyScript(stringifyData)) {
166
+ throw(`${fileName} : ` + 'Failed to compile template script.\nPlease make sure the script is correct and not returning empty result')
167
+ } else {
168
+ printInfo('Array function :')
169
+ printInfo(['Stringify : \n', stringifyData].join(''))
170
+ }
171
+ // console.log('RESULT =>', stringifyData)
172
+ printInfo('===================')
173
+ return Buffer.from(stringifyData)
174
+ }
175
+
176
+ const encryptIt = (data, encryptKey)=>{
177
+ const initializeVector = randomBytes(16) // "iv" is unique for each template file
178
+ const cipher = createCipheriv('aes-256-cbc', Buffer.from(encryptKey, 'base64'), initializeVector)
179
+ return Buffer.concat([initializeVector, cipher.update(data), cipher.final()]) // we put "iv" at beginning of cipherText, seperate it when doing decryption
180
+ }
181
+
182
+ const compressIt = (fileName, folderPath)=>{
183
+ // tar v7.1.0 -> tested 10x times, 2-3x times last compressed file have empty content
184
+ // tar v.7.4.0 -> tested 1000x times, all files have content (used app.test.js)
185
+ let compressSuccess = false
186
+ const maxRetry = 10
187
+ let retryCount = 0
188
+ do {
189
+ tar.c({file: fileName, cwd: folderPath, gzip: {level:9}, strict: true, sync: true}, fs.readdirSync(folderPath))
190
+ compressSuccess = true
191
+ tar.t({file: fileName, cwd: folderPath, sync: true, onentry: (entry)=>{
192
+ if (entry.size==0) {
193
+ compressSuccess = false
194
+ }
195
+ }})
196
+ if (compressSuccess==false) {
197
+ fs.unlinkSync(fileName)
198
+ retryCount++
199
+ }
200
+ }
201
+ while ((compressSuccess==false) && (retryCount<=maxRetry))
202
+ }
203
+
204
+ const getFileList = ()=>{
205
+ return (fs.readdirSync(process.cwd())).filter((fileName)=>{
206
+ const stat = fs.statSync(fileName)
207
+ return stat.isFile() && (stat.size>0) && ((fileName.split('.').pop().toLowerCase().match(/^(js)$/) || (fileName.toLowerCase()=='menu.json')))
208
+ })
209
+ }
210
+
211
+ async function addApp() {
212
+ /*
213
+ hex => 2 char = 1 bytes => can be encrypted
214
+ base64 => 4 char = 3 bytes => can be encrypted. Smaller size compare to Hex
215
+ utf8 => 1 char = 1 - 4 bytes => can not be encrypted, encryption need precise bytes per Character. Smallest Size compare to Hex and base64
216
+ */
217
+ process.chdir(options.workingDir)
218
+ try {
219
+ const bundlingStart = performance.now()
220
+ const rootFolder = './upload/'
221
+ const bundleName = basename(process.cwd()).trim().replace(' ', '').toLowerCase()
222
+ const bundleFolder = rootFolder+bundleName+'/'
223
+ const files = getFileList()
224
+ if (files.length>0) {
225
+ const keys = await prepareKeys()
226
+ let processedFile = 0
227
+ fs.rmSync(rootFolder, {force: true, recursive: true})
228
+ fs.mkdirSync(bundleFolder, {recursive: true})
229
+ for (const file of files) {
230
+ const fileName = file.toLowerCase();
231
+ // if (['finalib.js', 'solib.js'].indexOf(fileName)==-1) {continue}
232
+ const minifiedData = await minifiedIt(fileName);
233
+ const encryptedData = encryptIt(minifiedData, keys.encryptKey)
234
+ if (fileName=='menu.json') {
235
+ keys.metadata['menu'] = encryptedData.toString('base64')
236
+ } else {
237
+ fs.writeFileSync(bundleFolder+fileName, encryptedData.toString('base64'))
238
+ }
239
+ processedFile++
240
+ }
241
+ const bundleFile = `${rootFolder}${bundleName}.tgz`
242
+ compressIt(bundleFile, bundleFolder)
243
+ console.log(`Finished packing ${processedFile} files into "${bundleFile}" (${((performance.now()-bundlingStart)/1000).toFixed(2)}s)`)
244
+
245
+ // send to FINAPI
246
+ const uploadingStart = performance.now()
247
+ const data = (fs.readFileSync(bundleFile)).toString('base64') // *.tgz to base64String
248
+ const baseUrl = 'fina/rest/TOrmMethod/%22setApp%22'
249
+ const url = options.sub ?
250
+ `${options.server}/hub/${baseUrl}?subdomain=${options.sub}` :
251
+ `${options.server}:${options.apiPort}/${baseUrl}`
252
+ const headers = {'Content-Type': 'text/plain'}
253
+ const param = { _parameters: [options.dbIndex, bundleName, data, keys.metadata] }
254
+ const res = await axios.post(url, param, { headers: headers });
255
+ if (res.data.success) {
256
+ console.log(`Finished uploading "${bundleFile}" (${((performance.now()-uploadingStart)/1000).toFixed(2)}s)`)
257
+ fs.rmSync(rootFolder, {force: true, recursive: true})
258
+ } else {
259
+ console.error(res.data.error)
260
+ }
261
+ }
262
+ } catch (e) {
263
+ console.error(e.response?.data ? e.response.data : e)
264
+ }
265
+ }
266
+
267
+ await addApp()
268
+ }
269
+ )
270
+ .command('remove', 'Remove Biz-A Application',
271
+ {
272
+ 'n': {
273
+ alias: "appName",
274
+ describe: "Application name",
275
+ type: "string",
276
+ demandOption: true,
277
+ default: ""
278
+ }
279
+ },
280
+ (options)=>{
281
+ (async () => {
282
+ try {
283
+ const baseUrl = 'fina/rest/TOrmMethod/%22deleteApp%22'
284
+ const url = options.sub ?
285
+ `${options.server}/hub/${baseUrl}?subdomain=${options.sub}` :
286
+ `${options.server}:${options.apiPort}/${baseUrl}`
287
+ const headers = {'Content-Type': 'text/plain'}
288
+ const deleteApps = options.appName.trim().replaceAll(' ', '').toLowerCase()
289
+ const param = { _parameters: [options.dbIndex, deleteApps] }
290
+ const res = await axios.post(url, param, { headers: headers });
291
+ if (res.data?.success) {
292
+ if (deleteApps=='') {
293
+ console.log('All apps removed')
294
+ } else {
295
+ const failedList = (res.data._f && (typeof res.data._f=='string')) ? res.data._f.trim().replaceAll(' ', '').toLowerCase().split(',') : []
296
+ const removeList = deleteApps.split(',')
297
+ removeList.forEach((app)=>{
298
+ console.log(`${app} ${failedList.indexOf(app)==-1 ? 'removed' : 'not found'}`)
299
+ })
300
+ }
301
+ } else {
302
+ console.error(res.data.error)
303
+ }
304
+ return res
305
+ } catch (e) {
306
+ const errMsg = (e.response?.data ? e.response.data : e)
307
+ console.error(errMsg)
308
+ return errMsg
309
+ }
310
+ })()
311
+ }
312
+ )
313
+ .parse();
package/bin/hub.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import yargs from 'yargs';
4
4
  import { io as ioc } from "socket.io-client";
package/bin/proxy.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import express from 'express';
4
4
  import cors from 'cors';
package/bin/watcher.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import express from 'express';
4
4
  import cors from 'cors';
package/envs/env.dev.js CHANGED
@@ -5,7 +5,8 @@ export const envDev = {
5
5
  CDM_MONGO_LINK: 'mongodb+srv://imm_cdm:' +
6
6
  encodeURIComponent('imm@2019') + '@imm-cdm-dev.rf6wr.mongodb.net/?retryWrites=true&w=majority',
7
7
  COMPANY_REGISTER: 'companyregister-dev',
8
- BIZA_SERVER_LINK: 'https://biz-a-dev-41e7c93a25e5.herokuapp.com'
8
+ // BIZA_SERVER_LINK: 'https://biz-a-dev-41e7c93a25e5.herokuapp.com'
9
9
  // BIZA_SERVER_LINK: 'https://www.ptimf.id/biz-a-dev.com'
10
10
  // BIZA_SERVER_LINK: 'http://localhost:3000'
11
+ BIZA_SERVER_LINK: 'http://59.60.1.22:3000'
11
12
  };
package/envs/env.js CHANGED
@@ -5,5 +5,6 @@ export const env = {
5
5
  CDM_MONGO_LINK: 'mongodb+srv://imm_cdm:' +
6
6
  encodeURIComponent('imm@2019') + '@imm-cdm.ohcqt.mongodb.net/?retryWrites=true&w=majority',
7
7
  COMPANY_REGISTER: 'companyregister',
8
- BIZA_SERVER_LINK: 'https://biz-a.herokuapp.com'
8
+ // BIZA_SERVER_LINK: 'https://biz-a.herokuapp.com'
9
+ BIZA_SERVER_LINK: 'http://localhost:3000'
9
10
  };
package/log/debug.log ADDED
File without changes
package/log/error.log ADDED
File without changes
File without changes
package/log/info.log ADDED
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biz-a-cli",
3
3
  "nameDev": "biz-a-cli-dev",
4
- "version": "2.3.16",
4
+ "version": "2.3.18",
5
5
  "versionDev": "0.0.30",
6
6
  "description": "",
7
7
  "main": "bin/index.js",
@@ -24,7 +24,8 @@
24
24
  "hub": "bin/hub.js",
25
25
  "watcher": "bin/watcher.js",
26
26
  "uploadapp": "bin/uploadApp.js",
27
- "deleteapp": "bin/deleteApp.js"
27
+ "deleteapp": "bin/deleteApp.js",
28
+ "biza": "bin/app.js"
28
29
  },
29
30
  "dependencies": {
30
31
  "axios": "^1.6.8",
@@ -36,7 +37,7 @@
36
37
  "nodemailer": "^6.9.12",
37
38
  "socket.io-client": "^4.7.5",
38
39
  "socket.io-stream": "^0.9.1",
39
- "tar": "^7.1.0",
40
+ "tar": "^7.4.0",
40
41
  "uglify-js": "^3.17.4",
41
42
  "web-push": "^3.6.7",
42
43
  "winston": "^3.13.0",
@@ -47,6 +48,11 @@
47
48
  "socket.io": "^4.7.5"
48
49
  },
49
50
  "jest": {
50
- "transform": {}
51
+ "transform": {},
52
+ "testMatch": [
53
+ "<rootDir>/tests/**",
54
+ "!<rootDir>/tests/mockData",
55
+ "!<rootDir>/tests/mockData/**"
56
+ ]
51
57
  }
52
58
  }
package/readme.md ADDED
@@ -0,0 +1,62 @@
1
+ # **This package is used to setup applications within your Biz-A Platform.**
2
+
3
+ ## I. Add BizA App
4
+
5
+ ### a. Using Static IP
6
+
7
+ BizA Add -s [IP of FINA API] -p [default: 212] -i [dbindex] -d [full or relative path to app folder]
8
+
9
+ Example :
10
+ BizA add -s http://192.168.1.1 -i 2 -d “c:\biza templates\imamatek”
11
+ BizA Add -s http://192.168.1.1 -p 1205 -i 2 -d “c:\biza templates\imamatek”
12
+
13
+ ### b. Using BizA Hub
14
+
15
+ BizA Add -s [BizA Hub Server] -i [dbindex] --sub [subdomain of BizA Hub Server] -d [full or relative path to app folder]
16
+
17
+ Example :
18
+ BizA Add -s https://biz-a.hub.com -i 2 –-sub imm -d “c:\biza templates\imamatek”
19
+
20
+ ### c. Using Domain / Sub Domain
21
+
22
+ BizA Add -s [Domain] -i [dbindex] -d [full or relative path to app folder]
23
+
24
+ Example:
25
+ BizA Add-s https://my.domain.com -i 2 -d “c:\biza templates\imamatek”
26
+
27
+ ### To get list of full argument, please type
28
+
29
+ BizA Add --help
30
+
31
+
32
+
33
+ ## II. Remove BizA App
34
+
35
+ ### a. Using Static IP
36
+
37
+ BizA remove -s [IP of FINA API] -p [default: 212] -i [dbindex] -n [appname | multiple appname]
38
+
39
+ Example :
40
+ BizA Remove -s http://192.168.1.1 -i 2
41
+ BizA Remove -s http://192.168.1.1 -p 1205 -i 2 -n “imamatek”
42
+ BizA Remove -s http://192.168.1.1 -p 1205 -i 2 -n “imamatek, spos”
43
+
44
+ ### b. Using BizA Hub
45
+
46
+ BizA Remove -s [BizA Hub Server] -i [dbindex] --sub [subdomain of BizA Hub Server] -n [appname | multiple appname]
47
+
48
+ Example :
49
+ BizA Remove -s https://biz-a.hub.com -i 2 –sub imm -n “imamatek”
50
+
51
+ ### c. Using Domain / Sub Domain
52
+
53
+ BizA Remove -s [Domain] -i [dbindex] -n [appname | multiple appname]
54
+
55
+ Example:
56
+ BizA Remove -s https://my.domain.com -i 2 -n “imamatek”
57
+
58
+ ### To get list of full argument, please type
59
+
60
+ BizA Add --help
61
+
62
+ :warning: $${\color{red}**The *uploadBizA*, *uploadApp*, *deleteApp* commands are deprecated and will be removed in the future.**}$$
@@ -0,0 +1,384 @@
1
+ import fs from "fs"
2
+ import { jest } from '@jest/globals'
3
+ import { Duplex } from 'node:stream'
4
+ import { finished } from 'node:stream/promises'
5
+ import { createDecipheriv } from 'node:crypto'
6
+ import * as tar from "tar"
7
+
8
+ describe('Biz-A Apps CLI', ()=>{
9
+
10
+ let originalOptions, originalCWD, axios
11
+ const logSpy = jest.spyOn(global.console, 'log')
12
+ const errorSpy = jest.spyOn(global.console, 'error')
13
+
14
+ async function runCommand(...options){
15
+ process.argv[1] = process.cwd()+'\\bin' // mock the key root folder
16
+ process.argv = [process.argv[0], process.argv[1], ...options]
17
+ await import('../bin/app.js')
18
+
19
+ // All below functions does not wait for ES6 import promises
20
+ // await jest.runAllTimersAsync()
21
+ // jest.runAllTimers()
22
+ // jest.runAllTicks()
23
+
24
+ await new Promise(process.nextTick) // wait for all import promise (including ES6 import promises) to be resolved
25
+ }
26
+
27
+ beforeEach(async ()=>{
28
+ originalOptions = process.argv
29
+ originalCWD = process.cwd()
30
+
31
+ // mock default export first with unstable_mockModule
32
+ jest.unstable_mockModule('axios', ()=>{return {default: jest.fn()}})
33
+ axios = (await import('axios')).default
34
+
35
+ // then we can mock classes, functions, variables, and objects inside mocked module when doing dynamic import using jest.fn()
36
+ axios.get = jest.fn()
37
+ axios.post = jest.fn()
38
+ })
39
+
40
+ afterEach(()=>{
41
+ jest.resetModules()
42
+ jest.resetAllMocks()
43
+ process.argv = originalOptions
44
+ process.chdir(originalCWD)
45
+ })
46
+
47
+ describe('Add App', ()=>{
48
+ const mockDataFolder = './tests/mockData/'
49
+ const appName = 'mockApp'
50
+ const appFolderName = mockDataFolder + appName+ '/';
51
+
52
+ function mockValidTemplates(scriptList){
53
+ fs.rmSync(appFolderName, {force: true, recursive: true})
54
+ fs.mkdirSync(appFolderName, {recursive: true})
55
+ for (const [key, value] of Object.entries(scriptList)){
56
+ fs.writeFileSync(appFolderName+key, value.act)
57
+ }
58
+ }
59
+
60
+ function mockIssuerKeyResponse(){
61
+ axios.get.mockResolvedValue({
62
+ data:{
63
+ // this data containing 'mockAESKey', which is encrypted using CLI Private key
64
+ data: 'eyJpc3N1ZXIiOnsia2V5IjoidEVmZlRwbGpsRW9seVRVcnpEQUs4b05mZEg0T2p2VE9MOHZ4c0p3MkRISC9LY09WV2t0SG5kWmxxUXJURUdoWGtoWUFtQjBobmw1RU5xdFh2TXNjcHFCMnd6eXZJUy9OdUpxeFdBQmE4Y21QbXYyUHBqRFV3eUd5LzJzbFBybG00aGJjbjJWamFPRVJiMHFpclZFNGhZVDluNlFwSnR1ZUp1NXcrVG9JN0V1eC9wdkZHeWVaOU1ham5hSTVtcVliZkNsT0pEZnV3MkpuU1RGNk1kbWg0MURualNuYk5nWkJhSjF2VWw2U1lmVDREeVJoVTkrUER6UTRMZ0xPZXlVYUlTVHQwWnpQWDIyeGI5Nlo3NlE0ZFNGUzhwRzRJRk0vMFpHbnI5eXB0NmNhRFlseW5VUWVFOHVJMklJMnpZcjFNdE12RnkzaDEyUU5Za0tLS3NaaVNRV00vQXRPTm5sa00rcjFKSFpMZW42b3lKYlV1cEgvRmZxQmViQWdRUThLcUVENnN2UlljSXBKOWh3UlJDb2RvNmlHaVVTMUVZZnh2N3pFMDhwL281Q2JmdFRKTXpISjh3T1JZbkdaTDh5cWttT2V0Y3dJL0p6U2dGVDNST2E5V3ZMRkFHWkIrcWdEOTBNeGhwT3kyVzRlbS9lUVBpeHNadDR2UGl2T1l0QVZYZTBLL0lJR0tlZ3VRZ0NzeUZENkRmajQxYkNXbm9oUEgzNWl1Y2h1cGxKVUhsSmszMTRqLytkbTBieEZ6MWFzdDBKanVaRFp3bkVSeVJVbTNFSWZNU2kxVGRRaXRWRjI2bFJ3a1NwK0VSRHh0ZllHWCs3V1U3OUhyUFF3MHBuU3NzU29FbVFzQ2VXU0dFc1AwbEtpTlBjLzBCeDNzbjdNMnJpMzJmZUZLRHM9In0sImFjcXVpcmVyIjp7ImRhdGEiOiJtT1NabzB0N0ZZb2M2a0tuZlNURk04S3VHcUtzLzU2MGtERGdldDMrRW9UTUZZaW5sbThwRFRqc2xVK2VoWnRidk1TejJ1bmNPck5rQ1phQy9LM0JrbmM5T3BZTDZ2SXVGNmY2alZrbjJDS25xbnRnUXBqUkY4T2dYK3NEKzhLbnBMVll2QmhMdkJydjI2RTBHTnk5YTNwbCt2MHNna3EycXJoeUJCeVc3VnpRL3h1Y2FCdDRrUjE0N2FpaTlSaThvUXEzRU1kZ3BPN2crYmxLMFk3eGt5ekQ2YU1JSDFSR1lSMTgwaU94akVqYVpiVE1uQkUwS1RJUnBEMnkwL25MTU5VTHZTUlBraVMyTUh5cmI1QWJCbWNQR1hzVUVvZFc0OHM0QVdmS2hZRldLbzV5anQwZlhCVCtUbEdaWmYrcDJINnRzSVNUYjZSKzlHWFk5c1EzNE5IdmFCRTZtNzlURUVQWTNkeUl3Vm5WQTIzR0FqL211bzI5M2dMTlp0R090ZWcrR1VlWXIzWXViQUZnL2VrN0NyTThGdGRQZC92R3NjdlJ4bkNEYnhmU3U3YXUzUFd1RFB5djk0ZjhXaXVMcWlKUGFQRmFZaUE4SW1lZklnY2ROKzMrTWZDRkdyR1dsWHlZM1pxVVI5NEhteGVjd1Q5Q256N2g2NHc0L3c0b0c5bU9CWmVsRk14dzlxUVlOWTZ5ZzRrakRBa2xUK29GMUszd213VUpPVTlhc3NCVEQ1SXZONlJRSEV0SU02ZmhKY1R0Zm5tK3l0Y201b3hjY3ZrN2tlVDExR2xjVi9Oa3EwNDRKb0NFL0drbzV2TWovQ29ERklJd0VjSENnQ1RvVGE0SForcWUzTWc2Wm1CazhvL2pwV3VFbTF5anJMcUdMSmFOTEJXNjlTUT0iLCJzaWduYXR1cmUiOiJzZDJXYnNDVy9oeXlMMnIzcmVhSFcyZndzVy85THVKNkp6MW0zUUNkWVA1NkZUU1ovSHM4c0Z6V2hmdVhtS1JJdEd5cnA4RGxMNTJkVkw1eTNnYmRIc0VoMDRITDhiWXIwS2lFU2RlbnVPMnoyZkdBWTlsclVjRC92alpZNlAwOHBVQTBEbXIreTk2UjQzdGVhTnZaMkplMklMWW1GZzdNOGF2cDZJZWk5WllzUmxyMFg2TmlzcUlXTWxDeHZOLzdadGNzcWQ3QndtWEhZRmlQWVkxaC9KTUtJdEREZnBneGgzU3RITlZQb01JU09FcTVkbGVRMjJNaTlPL2trZ2pkcmlBRDZsZ1hCZ214Q0FDaE5VQzVEZW5vUVIwQUlTREMvTW9udVA5ZldoMktwMkV2SlRGakR0eUJFdFZZekZZME82NEcxOVNhSFRnRmdxTFd2eXJDL2gzK1BqREIyTTdxOFBXOWVDNjlwM0ZCdUt1R0U3aTkrTmpKeWh4eFJoWWlxbW5MWjFxN3lFUzNudkIrNjJHRDZrUlR6YU5zdjFCUTRFSVpBR1NSaUF4d3o0aWo0ZldtdWJhaEd2WGlseGUzYzE1S09aa0RzcWRpQklIL1RWYzc1TUVBUVYrVVNlbXNEUnF2eUZ3ZFBobW9oM25KeVdmbFdsMU05eVNhdU1MaUFPV2c1bXhhSkdqQ0NtMGV5SHZaWUFFOFNWUjViWFJOZjIrNkpBQUJ2Mk5iSEJIVEJxM3V1cWpEVFZZdFZTRmQ3QmsvUS9NdVRiQnNjYUNqVUtybmtMcUpDTEw4akV0NnJlQnJLdlNPZll1c0txWHBWRmZMV1dHd1ZWSEViTzZVQ0xYTmIzY0c1MXRuNmpOS3NmN29wQXhBTFhUNFRXVjk0RjB6KzRpMjNYOD0ifX0=',
65
+ // sign using BizA Server PrivateKey, to be verified by BizA CLI using Server PublicKey
66
+ signature: 'Y9Vs9mOV5fqjcX1ZQh8N0vubV6J5+rx1Lvcrb8T9nBONMJIS74lqRdnO4a84rDCXdda0QOJs5750OdE69/ognriSqdvJ5qjb4sjYAuzXmbxGOuU4ptWMAl9CVfi7JdISdXaDMn5o2E9SuuQLwBMUvX6a9p93/L+TK/pSLuv3cwZqaCtiyhsZV1Q1mSz5xpEfGZR//0Vdaj7pZYZ7CDjQAiq6tMc1Mm7azDmxEwDmNqTMvPKJusGTTHvIFH9MbGTsHgq7DzhsGBVW0fyQGx4ycqB5u9D9j9YffjdlFZ3xncxgq/NVHJzHPbosrG2blW3yWr0Sg4P34AAOhNMtAbbrZfa5HYQrGLB5MbOY3s8EiXItufKzz5xTKDVdLER7kWk6th+a7c548TfqWkrkepPEvgBgmGoMw/PvXFsMVMKqRoF31lN7tumKnVyVzz/EwrsVVc7QrsJxfODHgU0bNNxi6gEb+prqKy0ZWmWwtf7coLWq/ybC2yFMrqUjymoJBGvNoLBURihD2m645+qudNlcJlLoyTs8B3KRCwFKtBH2X9eW5ZzDpIQAXDge6Bz/668Xg6DB8TzvpNr0sPOkuXXSKhQnb0SkR7QTHzpFDcg88Ur9Nnnh9aSZZRj1nRo5LawNto+9684SzpIfbjzAnbTHLQ6oY28SIdihKVjc0wj9BZk='
67
+ }
68
+ })
69
+ axios.post.mockResolvedValue({data: {success: true}, status: 200})
70
+ }
71
+
72
+ beforeEach(()=>{
73
+ fs.rmSync(mockDataFolder, {recursive: true, force: true})
74
+ })
75
+
76
+ afterAll(()=>{
77
+ fs.rmSync(mockDataFolder, {recursive: true, force: true})
78
+ })
79
+
80
+ const stressTestCount = 10;
81
+ it.each(Array(stressTestCount).fill().map((v,i)=>i+1))(`shall compress and encrypt app scripts (stress test %p of ${stressTestCount})`, async ()=>{
82
+ const mockScripts = {
83
+ 'a.js' : {
84
+ act: 'get = function () {return {modelA: {}}}',
85
+ exp: '{"modelA":{}}'
86
+ },
87
+ 'b.js' : {
88
+ act: 'const get = function () {\nreturn {"modelB": {id: null}}};',
89
+ exp: '{"modelB":{"id":null}}'
90
+ },
91
+ 'c.js' : {
92
+ act: 'let get = function () {return {modelC: {}, function: {func: () => { \nreturn ["lib"];\n}\n}}};',
93
+ exp: '{"modelC":{},"function":{"func":["window.Function","","return[\\\"lib\\\"]"]}}'
94
+ },
95
+ 'd.js' : {
96
+ act: 'var get = function () {return {model: {}, tableName: "", fields: [], function: {}}};\nmodule.exports = get;',
97
+ exp: '{"model":{},"tableName":"","fields":[],"function":{}}'
98
+ },
99
+ 'menu.json' : {
100
+ // act: JSON.stringify([
101
+ act: `[
102
+ {"menuName": "", "caption": "Home", "link": ["./main"],"subMenu": []},
103
+ {"menuName": "", "caption": "Personalia","link": [], "subMenu": [
104
+ {"menuName": "","caption": "Data Master","link": [], "subMenu": []},
105
+ {
106
+ "menuName": "",
107
+ "caption": "Gaji",
108
+ "link": [],
109
+ "subMenu": [
110
+ {
111
+ "menuName": "",
112
+ "caption": "Parameter",
113
+ "link": [
114
+ "./form",
115
+ "gajiparam",
116
+ null
117
+ ],
118
+ "subMenu": []
119
+ },
120
+ {
121
+ "menuName": "",
122
+ "caption": "Perubahan",
123
+ "link": [],
124
+ "subMenu": [
125
+ {
126
+ "menuName": "",
127
+ "caption": "Form Perubahan Gaji",
128
+ "link": [
129
+ "./form",
130
+ "gajiubah",
131
+ null
132
+ ],
133
+ "subMenu": []
134
+ },
135
+ {
136
+ "menuName": "",
137
+ "caption": "Daftar Perubahan Gaji",
138
+ "link": [
139
+ "./list",
140
+ "gajiubahdaftar"
141
+ ],
142
+ "subMenu": []
143
+ }
144
+ ]
145
+ }
146
+ ]
147
+ }
148
+ ]}
149
+ ]`,
150
+ // ]),
151
+ // not saved as templates file, but as metadata of curret App
152
+ exp: "[{\"menuName\":\"\",\"caption\":\"Home\",\"link\":[\"./main\"],\"subMenu\":[]},{\"menuName\":\"\",\"caption\":\"Personalia\",\"link\":[],\"subMenu\":[{\"menuName\":\"\",\"caption\":\"Data Master\",\"link\":[],\"subMenu\":[]},{\"menuName\":\"\",\"caption\":\"Gaji\",\"link\":[],\"subMenu\":[{\"menuName\":\"\",\"caption\":\"Parameter\",\"link\":[\"./form\",\"gajiparam\",null],\"subMenu\":[]},{\"menuName\":\"\",\"caption\":\"Perubahan\",\"link\":[],\"subMenu\":[{\"menuName\":\"\",\"caption\":\"Form Perubahan Gaji\",\"link\":[\"./form\",\"gajiubah\",null],\"subMenu\":[]},{\"menuName\":\"\",\"caption\":\"Daftar Perubahan Gaji\",\"link\":[\"./list\",\"gajiubahdaftar\"],\"subMenu\":[]}]}]}]}]"
153
+ }
154
+ }
155
+
156
+ mockValidTemplates(mockScripts)
157
+ mockIssuerKeyResponse()
158
+ const mockAESKey = Buffer.from('hAnHadaJXJaq/9fCFMNmjkrB61CBPXJid6vbtXgG8Ug=', 'base64')
159
+
160
+ await runCommand('add', '-s', 'https://a.b.c', '-p', '1205', '-i', '2', '-d', appFolderName)
161
+
162
+ expect(axios.get.mock.calls).toHaveLength(1)
163
+ const getArgs = axios.get.mock.calls[0]
164
+ expect(getArgs).toHaveLength(2)
165
+ expect(getArgs[0]).toBe('http://localhost:3000/api/issuerKey')
166
+ expect(getArgs[1]).toHaveProperty('params')
167
+ const params = getArgs[1].params
168
+ expect(params).toHaveProperty('data')
169
+ expect(params.data).toBe('eyJpc3N1ZXIiOiJDTEkiLCJhY3F1aXJlciI6IkNsaWVudCJ9') // BizA CLI Private key
170
+ expect(params).toHaveProperty('signature')
171
+ expect(params.signature).not.toBeNull() // PSS signature is not deterministic
172
+
173
+ expect(axios.post.mock.calls).toHaveLength(1)
174
+ const postArgs = axios.post.mock.calls[0]
175
+ expect(postArgs).toHaveLength(3)
176
+ expect(postArgs[0]).toBe('https://a.b.c:1205/fina/rest/TOrmMethod/%22setApp%22')
177
+
178
+ const apiParams = postArgs[1]._parameters
179
+ expect(apiParams[0]).toBe(2)
180
+ expect(apiParams[1]).toBe(appName.toLowerCase())
181
+
182
+ // encrypted template scripts is not deterministic, because IV (initialize vector) using 16 random bytes
183
+ const templateBuffer = Buffer.from(apiParams[2], 'base64')
184
+ const decryptScripts = {}
185
+ const stream = new Duplex()
186
+ stream.push(templateBuffer)
187
+ stream.push(null)
188
+ stream.pipe(
189
+ // tar.x -> will extract and write to file
190
+ // tar.t -> only extract
191
+ tar.t({
192
+ strict: true
193
+ , sync: true
194
+ , onReadEntry: entry=>{
195
+ const chunks = [];
196
+ entry.on('data', chunk => chunks.push(chunk));
197
+ entry.on('end', () => {
198
+ expect(entry.path.trim().toLowerCase()).not.toBe('menu.json') // shall save "menu.json" as App.metadata
199
+ const cipherText = Buffer.from(Buffer.concat(chunks).toString(), 'base64')
200
+ const decryptInitializeVector = cipherText.subarray(0, 16)
201
+ const decryptData = cipherText.subarray(16)
202
+ const decipher = createDecipheriv('aes-256-cbc', mockAESKey, decryptInitializeVector)
203
+ const decrypted = Buffer.concat([decipher.update(decryptData), decipher.final()])
204
+ decryptScripts[entry.path] = decrypted.toString()
205
+ })
206
+ }
207
+ })
208
+ .on('finish', ()=>stream.end())
209
+ )
210
+ await finished(stream) // wait for untar process to finish
211
+ for (const [fileName, script] of Object.entries(decryptScripts)){
212
+ expect(script).toBe(mockScripts[fileName].exp)
213
+ }
214
+
215
+ // app.metadata : {
216
+ // "acquirer": { // generate by BizA Server
217
+ // "data": "", //containing AES Key for template script encryption or deccryption, which is encrypted using BizA Client publicKey
218
+ // "signature": "", // sign using BizA CLI Private key, to be verified by BizA Client using CLI Public Key
219
+ // },
220
+ // "menu" : "" // contain encrypted "menu.json"
221
+ // }
222
+ expect(apiParams[3]).toHaveProperty('acquirer')
223
+ expect(apiParams[3].acquirer).toHaveProperty('data')
224
+ expect(apiParams[3].acquirer.data).toBe('eyJkYXRhIjoibU9TWm8wdDdGWW9jNmtLbmZTVEZNOEt1R3FLcy81NjBrRERnZXQzK0VvVE1GWWlubG04cERUanNsVStlaFp0YnZNU3oydW5jT3JOa0NaYUMvSzNCa25jOU9wWUw2dkl1RjZmNmpWa24yQ0tucW50Z1FwalJGOE9nWCtzRCs4S25wTFZZdkJoTHZCcnYyNkUwR055OWEzcGwrdjBzZ2txMnFyaHlCQnlXN1Z6US94dWNhQnQ0a1IxNDdhaWk5Umk4b1FxM0VNZGdwTzdnK2JsSzBZN3hreXpENmFNSUgxUkdZUjE4MGlPeGpFamFaYlRNbkJFMEtUSVJwRDJ5MC9uTE1OVUx2U1JQa2lTMk1IeXJiNUFiQm1jUEdYc1VFb2RXNDhzNEFXZktoWUZXS281eWp0MGZYQlQrVGxHWlpmK3AySDZ0c0lTVGI2Uis5R1hZOXNRMzROSHZhQkU2bTc5VEVFUFkzZHlJd1ZuVkEyM0dBai9tdW8yOTNnTE5adEdPdGVnK0dVZVlyM1l1YkFGZy9lazdDck04RnRkUGQvdkdzY3ZSeG5DRGJ4ZlN1N2F1M1BXdURQeXY5NGY4V2l1THFpSlBhUEZhWWlBOEltZWZJZ2NkTiszK01mQ0ZHckdXbFh5WTNacVVSOTRIbXhlY3dUOUNuejdoNjR3NC93NG9HOW1PQlplbEZNeHc5cVFZTlk2eWc0a2pEQWtsVCtvRjFLM3dtd1VKT1U5YXNzQlRENUl2TjZSUUhFdElNNmZoSmNUdGZubSt5dGNtNW94Y2N2azdrZVQxMUdsY1YvTmtxMDQ0Sm9DRS9Ha281dk1qL0NvREZJSXdFY0hDZ0NUb1RhNEhaK3FlM01nNlptQms4by9qcFd1RW0xeWpyTHFHTEphTkxCVzY5U1E9Iiwic2lnbmF0dXJlIjoic2QyV2JzQ1cvaHl5TDJyM3JlYUhXMmZ3c1cvOUx1SjZKejFtM1FDZFlQNTZGVFNaL0hzOHNGeldoZnVYbUtSSXRHeXJwOERsTDUyZFZMNXkzZ2JkSHNFaDA0SEw4YllyMEtpRVNkZW51TzJ6MmZHQVk5bHJVY0QvdmpaWTZQMDhwVUEwRG1yK3k5NlI0M3RlYU52WjJKZTJJTFltRmc3TThhdnA2SWVpOVpZc1JscjBYNk5pc3FJV01sQ3h2Ti83WnRjc3FkN0J3bVhIWUZpUFlZMWgvSk1LSXRERGZwZ3hoM1N0SE5WUG9NSVNPRXE1ZGxlUTIyTWk5Ty9ra2dqZHJpQUQ2bGdYQmdteENBQ2hOVUM1RGVub1FSMEFJU0RDL01vbnVQOWZXaDJLcDJFdkpURmpEdHlCRXRWWXpGWTBPNjRHMTlTYUhUZ0ZncUxXdnlyQy9oMytQakRCMk03cThQVzllQzY5cDNGQnVLdUdFN2k5K05qSnloeHhSaFlpcW1uTFoxcTd5RVMzbnZCKzYyR0Q2a1JUemFOc3YxQlE0RUlaQUdTUmlBeHd6NGlqNGZXbXViYWhHdlhpbHhlM2MxNUtPWmtEc3FkaUJJSC9UVmM3NU1FQVFWK1VTZW1zRFJxdnlGd2RQaG1vaDNuSnlXZmxXbDFNOXlTYXVNTGlBT1dnNW14YUpHakNDbTBleUh2WllBRThTVlI1YlhSTmYyKzZKQUFCdjJOYkhCSFRCcTN1dXFqRFRWWXRWU0ZkN0JrL1EvTXVUYkJzY2FDalVLcm5rTHFKQ0xMOGpFdDZyZUJyS3ZTT2ZZdXNLcVhwVkZmTFdXR3dWVkhFYk82VUNMWE5iM2NHNTF0bjZqTktzZjdvcEF4QUxYVDRUV1Y5NEYweis0aTIzWDg9In0=')
225
+ expect(apiParams[3].acquirer).toHaveProperty('signature')
226
+ expect(apiParams[3].acquirer.signature).not.toBeNull() // PSS signature is not deterministic
227
+
228
+ expect(apiParams[3]).toHaveProperty('menu') // shall save "menu.json" as App.metadata
229
+ expect(apiParams[3].menu).not.toBeNull() // IV (initialize vector) using 16 random bytes
230
+
231
+ expect(postArgs[2]).toStrictEqual({"headers": {"Content-Type": "text/plain"}})
232
+
233
+ expect(logSpy.mock.calls[0][0]).toContain(`Finished packing ${Object.keys(mockScripts).length} files into "./upload/${appName.toLowerCase()}.tgz"`)
234
+ expect(logSpy.mock.calls[1][0]).toContain(`Finished uploading "./upload/${appName.toLocaleLowerCase()}.tgz"`)
235
+ })
236
+
237
+ /* it('Invalid script syntax', async ()=>{
238
+ mockValidTemplates({
239
+ 'a.js' : {act: 'get = function () {return {modelA: {}}'} // missing close closure at the end
240
+ })
241
+ mockIssuerKeyResponse()
242
+ await runCommand('add', '-s', 'https://a.b.c', '-p', '1205', '-i', '2', '-d', appFolderName, "-v")
243
+
244
+ expect(logSpy.mock.calls.length).toBe(1)
245
+ expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
246
+
247
+ expect(errorSpy.mock.calls.length).toBe(1)
248
+ expect(errorSpy.mock.calls[0][0]).toBe('a.js : SyntaxError: Unexpected token: eof, expected: punc «}»')
249
+ })
250
+
251
+ it('Failed to compile script with node sandbox and ES6 Import', async ()=>{
252
+ mockValidTemplates({
253
+ 'a.js' : {act: 'get = function () {return {modelA: {}}}\n modul.expor = get;\n'} // wrong "module.export"
254
+ })
255
+ mockIssuerKeyResponse()
256
+ await runCommand('add', '-s', 'https://a.b.c', '-p', '1205', '-i', '2', '-d', appFolderName, "-v")
257
+
258
+ expect(logSpy.mock.calls.length).toBe(5)
259
+ expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
260
+ expect(logSpy.mock.calls[1][0]).toBe('Minify : \nget=function(){return{modelA:{}}};modul.expor=get;')
261
+ expect(logSpy.mock.calls[2][0]).toBe('Running script with VM') // node sandbox (VN) error as console.log
262
+ expect(logSpy.mock.calls[3][0]).toBe('a.js : ReferenceError: modul is not defined')
263
+ expect(logSpy.mock.calls[4][0]).toBe('Running script with Import function')
264
+
265
+ expect(errorSpy.mock.calls.length).toBe(1)
266
+ expect(errorSpy.mock.calls[0][0]).toBe("a.js : ReferenceError: modul is not defined") // ES6 import error
267
+ })
268
+
269
+ it('Shall not allow empty script', async ()=>{
270
+ mockValidTemplates({
271
+ 'a.js' : {act: 'get = function () {}'} // empty script
272
+ })
273
+ mockIssuerKeyResponse()
274
+ await runCommand('add', '-s', 'https://a.b.c', '-p', '1205', '-i', '2', '-d', appFolderName, "-v")
275
+
276
+ expect(logSpy.mock.calls.length).toBe(4)
277
+ expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
278
+ expect(logSpy.mock.calls[1][0]).toBe('Minify : \nget=function(){};')
279
+ expect(logSpy.mock.calls[2][0]).toBe('Running script with VM') // node sandbox (VN) error as console.log
280
+ expect(logSpy.mock.calls[3][0]).toBe('Running script with Import function')
281
+
282
+ expect(errorSpy.mock.calls.length).toBe(1)
283
+ expect(errorSpy.mock.calls[0][0]).toBe('a.js : Failed to compile template script.\nPlease make sure the script is correct and not returning empty result') // ES6 import error
284
+ }) */
285
+
286
+ const errorHandlingTestCases = [
287
+ // [test case name, scripts, expectedFn()]
288
+ [
289
+ 'Invalid script syntax',
290
+ {act: 'get = function () {return {modelA: {}}'}, // missing close closure at the end',
291
+ ()=>{
292
+ expect(logSpy.mock.calls.length).toBe(1)
293
+ expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
294
+
295
+ expect(errorSpy.mock.calls.length).toBe(1)
296
+ expect(errorSpy.mock.calls[0][0]).toBe('a.js : SyntaxError: Unexpected token: eof, expected: punc «}»')
297
+ }
298
+ ],
299
+ [
300
+ 'Failed to compile script with node sandbox and ES6 Import',
301
+ {act: 'get = function () {return {modelA: {}}}\n modul.expor = get;\n'}, // wrong "module.export"
302
+ ()=>{
303
+ expect(logSpy.mock.calls.length).toBe(5)
304
+ expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
305
+ expect(logSpy.mock.calls[1][0]).toBe('Minify : \nget=function(){return{modelA:{}}};modul.expor=get;')
306
+ expect(logSpy.mock.calls[2][0]).toBe('Running script with VM') // node sandbox (VN) error as console.log
307
+ expect(logSpy.mock.calls[3][0]).toBe('a.js : ReferenceError: modul is not defined')
308
+ expect(logSpy.mock.calls[4][0]).toBe('Running script with Import function')
309
+
310
+ expect(errorSpy.mock.calls.length).toBe(1)
311
+ expect(errorSpy.mock.calls[0][0]).toBe("a.js : ReferenceError: modul is not defined") // ES6 import error
312
+ }
313
+ ],
314
+ [
315
+ 'Shall not allow empty script',
316
+ {act: 'get = function () {}'}, // empty script
317
+ ()=>{
318
+ expect(logSpy.mock.calls.length).toBe(4)
319
+ expect(logSpy.mock.calls[0][0]).toBe('===================\nA.JS\n===================')
320
+ expect(logSpy.mock.calls[1][0]).toBe('Minify : \nget=function(){};')
321
+ expect(logSpy.mock.calls[2][0]).toBe('Running script with VM') // node sandbox (VN) error as console.log
322
+ expect(logSpy.mock.calls[3][0]).toBe('Running script with Import function')
323
+
324
+ expect(errorSpy.mock.calls.length).toBe(1)
325
+ expect(errorSpy.mock.calls[0][0]).toBe('a.js : Failed to compile template script.\nPlease make sure the script is correct and not returning empty result')
326
+ }
327
+ ],
328
+ ]
329
+ it.each(errorHandlingTestCases)('%p', async (name, scripts, expectedFn)=>{
330
+ mockValidTemplates({'a.js' : scripts})
331
+ mockIssuerKeyResponse()
332
+ await runCommand('add', '-s', 'https://a.b.c', '-p', '1205', '-i', '2', '-d', appFolderName, "-v")
333
+ expectedFn()
334
+ })
335
+
336
+ })
337
+
338
+ describe('Remove App', ()=>{
339
+ it('shall remove specific apps', async ()=>{
340
+ axios.post.mockResolvedValue({data: {success: true}, status: 200})
341
+
342
+ await runCommand('remove', '-s', 'https://finaapi.imamatek.com', '-p', '1205', '-i', '1', '-n', 'myApp')
343
+
344
+ expect(axios.post.mock.calls).toHaveLength(1)
345
+ expect(axios.post).toHaveBeenCalledWith(
346
+ 'https://finaapi.imamatek.com:1205/fina/rest/TOrmMethod/%22deleteApp%22',
347
+ {_parameters: [1, 'myapp']},
348
+ {headers: {'Content-Type': 'text/plain'}}
349
+ )
350
+ expect(logSpy).toHaveBeenCalledWith('myapp removed')
351
+ })
352
+
353
+ it('shall remove multiple apps', async ()=>{
354
+ axios.post.mockResolvedValue({data: {success: true, _f: 'notExistApp'}, status: 200})
355
+
356
+ await runCommand('remove', '-i', '2', '-s', 'https://www.AA.io', '-p', '2708', '-n', 'firstApp, notExistApp, secondApp')
357
+
358
+ expect(axios.post.mock.calls).toHaveLength(1)
359
+ expect(axios.post).toHaveBeenCalledWith(
360
+ 'https://www.AA.io:2708/fina/rest/TOrmMethod/%22deleteApp%22',
361
+ {_parameters: [2, 'firstapp,notexistapp,secondapp']},
362
+ {headers: {'Content-Type': 'text/plain'}}
363
+ )
364
+ expect(logSpy).toHaveBeenCalledWith('firstapp removed')
365
+ expect(logSpy).toHaveBeenCalledWith('secondapp removed')
366
+ expect(logSpy).toHaveBeenCalledWith('notexistapp not found')
367
+ })
368
+
369
+ it('shall remove all apps', async ()=>{
370
+ axios.post.mockResolvedValue({data: {success: true}, _f: '', status: 200})
371
+
372
+ await runCommand('remove', '-s', 'https://finaapi.imamatek.com', '-p', '1205', '-i', '2')
373
+
374
+ expect(axios.post.mock.calls).toHaveLength(1)
375
+ expect(axios.post).toHaveBeenCalledWith(
376
+ 'https://finaapi.imamatek.com:1205/fina/rest/TOrmMethod/%22deleteApp%22',
377
+ {_parameters: [2, '']},
378
+ {headers: {'Content-Type': 'text/plain'}}
379
+ )
380
+ expect(logSpy).toHaveBeenCalledWith('All apps removed')
381
+ })
382
+ })
383
+
384
+ })