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 +313 -0
- package/bin/hub.js +1 -1
- package/bin/proxy.js +1 -1
- package/bin/watcher.js +1 -1
- package/envs/env.dev.js +2 -1
- package/envs/env.js +2 -1
- package/log/debug.log +0 -0
- package/log/error.log +0 -0
- package/log/exception.log +0 -0
- package/log/info.log +0 -0
- package/package.json +10 -4
- package/readme.md +62 -0
- package/tests/app.test.js +384 -0
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
package/bin/proxy.js
CHANGED
package/bin/watcher.js
CHANGED
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.
|
|
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.
|
|
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
|
+
})
|