diff --git a/src/services/nodejs/coverage/clover.xml b/src/services/nodejs/coverage/clover.xml
new file mode 100644
index 0000000..10c5e89
--- /dev/null
+++ b/src/services/nodejs/coverage/clover.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/services/nodejs/coverage/coverage-final.json b/src/services/nodejs/coverage/coverage-final.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/src/services/nodejs/coverage/coverage-final.json
@@ -0,0 +1 @@
+{}
diff --git a/src/services/nodejs/coverage/lcov-report/base.css b/src/services/nodejs/coverage/lcov-report/base.css
new file mode 100644
index 0000000..f418035
--- /dev/null
+++ b/src/services/nodejs/coverage/lcov-report/base.css
@@ -0,0 +1,224 @@
+body, html {
+ margin:0; padding: 0;
+ height: 100%;
+}
+body {
+ font-family: Helvetica Neue, Helvetica, Arial;
+ font-size: 14px;
+ color:#333;
+}
+.small { font-size: 12px; }
+*, *:after, *:before {
+ -webkit-box-sizing:border-box;
+ -moz-box-sizing:border-box;
+ box-sizing:border-box;
+ }
+h1 { font-size: 20px; margin: 0;}
+h2 { font-size: 14px; }
+pre {
+ font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
+ margin: 0;
+ padding: 0;
+ -moz-tab-size: 2;
+ -o-tab-size: 2;
+ tab-size: 2;
+}
+a { color:#0074D9; text-decoration:none; }
+a:hover { text-decoration:underline; }
+.strong { font-weight: bold; }
+.space-top1 { padding: 10px 0 0 0; }
+.pad2y { padding: 20px 0; }
+.pad1y { padding: 10px 0; }
+.pad2x { padding: 0 20px; }
+.pad2 { padding: 20px; }
+.pad1 { padding: 10px; }
+.space-left2 { padding-left:55px; }
+.space-right2 { padding-right:20px; }
+.center { text-align:center; }
+.clearfix { display:block; }
+.clearfix:after {
+ content:'';
+ display:block;
+ height:0;
+ clear:both;
+ visibility:hidden;
+ }
+.fl { float: left; }
+@media only screen and (max-width:640px) {
+ .col3 { width:100%; max-width:100%; }
+ .hide-mobile { display:none!important; }
+}
+
+.quiet {
+ color: #7f7f7f;
+ color: rgba(0,0,0,0.5);
+}
+.quiet a { opacity: 0.7; }
+
+.fraction {
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+ font-size: 10px;
+ color: #555;
+ background: #E8E8E8;
+ padding: 4px 5px;
+ border-radius: 3px;
+ vertical-align: middle;
+}
+
+div.path a:link, div.path a:visited { color: #333; }
+table.coverage {
+ border-collapse: collapse;
+ margin: 10px 0 0 0;
+ padding: 0;
+}
+
+table.coverage td {
+ margin: 0;
+ padding: 0;
+ vertical-align: top;
+}
+table.coverage td.line-count {
+ text-align: right;
+ padding: 0 5px 0 20px;
+}
+table.coverage td.line-coverage {
+ text-align: right;
+ padding-right: 10px;
+ min-width:20px;
+}
+
+table.coverage td span.cline-any {
+ display: inline-block;
+ padding: 0 5px;
+ width: 100%;
+}
+.missing-if-branch {
+ display: inline-block;
+ margin-right: 5px;
+ border-radius: 3px;
+ position: relative;
+ padding: 0 4px;
+ background: #333;
+ color: yellow;
+}
+
+.skip-if-branch {
+ display: none;
+ margin-right: 10px;
+ position: relative;
+ padding: 0 4px;
+ background: #ccc;
+ color: white;
+}
+.missing-if-branch .typ, .skip-if-branch .typ {
+ color: inherit !important;
+}
+.coverage-summary {
+ border-collapse: collapse;
+ width: 100%;
+}
+.coverage-summary tr { border-bottom: 1px solid #bbb; }
+.keyline-all { border: 1px solid #ddd; }
+.coverage-summary td, .coverage-summary th { padding: 10px; }
+.coverage-summary tbody { border: 1px solid #bbb; }
+.coverage-summary td { border-right: 1px solid #bbb; }
+.coverage-summary td:last-child { border-right: none; }
+.coverage-summary th {
+ text-align: left;
+ font-weight: normal;
+ white-space: nowrap;
+}
+.coverage-summary th.file { border-right: none !important; }
+.coverage-summary th.pct { }
+.coverage-summary th.pic,
+.coverage-summary th.abs,
+.coverage-summary td.pct,
+.coverage-summary td.abs { text-align: right; }
+.coverage-summary td.file { white-space: nowrap; }
+.coverage-summary td.pic { min-width: 120px !important; }
+.coverage-summary tfoot td { }
+
+.coverage-summary .sorter {
+ height: 10px;
+ width: 7px;
+ display: inline-block;
+ margin-left: 0.5em;
+ background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
+}
+.coverage-summary .sorted .sorter {
+ background-position: 0 -20px;
+}
+.coverage-summary .sorted-desc .sorter {
+ background-position: 0 -10px;
+}
+.status-line { height: 10px; }
+/* yellow */
+.cbranch-no { background: yellow !important; color: #111; }
+/* dark red */
+.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
+.low .chart { border:1px solid #C21F39 }
+.highlighted,
+.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
+ background: #C21F39 !important;
+}
+/* medium red */
+.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
+/* light red */
+.low, .cline-no { background:#FCE1E5 }
+/* light green */
+.high, .cline-yes { background:rgb(230,245,208) }
+/* medium green */
+.cstat-yes { background:rgb(161,215,106) }
+/* dark green */
+.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
+.high .chart { border:1px solid rgb(77,146,33) }
+/* dark yellow (gold) */
+.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
+.medium .chart { border:1px solid #f9cd0b; }
+/* light yellow */
+.medium { background: #fff4c2; }
+
+.cstat-skip { background: #ddd; color: #111; }
+.fstat-skip { background: #ddd; color: #111 !important; }
+.cbranch-skip { background: #ddd !important; color: #111; }
+
+span.cline-neutral { background: #eaeaea; }
+
+.coverage-summary td.empty {
+ opacity: .5;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ line-height: 1;
+ color: #888;
+}
+
+.cover-fill, .cover-empty {
+ display:inline-block;
+ height: 12px;
+}
+.chart {
+ line-height: 0;
+}
+.cover-empty {
+ background: white;
+}
+.cover-full {
+ border-right: none !important;
+}
+pre.prettyprint {
+ border: none !important;
+ padding: 0 !important;
+ margin: 0 !important;
+}
+.com { color: #999 !important; }
+.ignore-none { color: #999; font-weight: normal; }
+
+.wrapper {
+ min-height: 100%;
+ height: auto !important;
+ height: 100%;
+ margin: 0 auto -48px;
+}
+.footer, .push {
+ height: 48px;
+}
diff --git a/src/services/nodejs/coverage/lcov-report/block-navigation.js b/src/services/nodejs/coverage/lcov-report/block-navigation.js
new file mode 100644
index 0000000..cc12130
--- /dev/null
+++ b/src/services/nodejs/coverage/lcov-report/block-navigation.js
@@ -0,0 +1,87 @@
+/* eslint-disable */
+var jumpToCode = (function init() {
+ // Classes of code we would like to highlight in the file view
+ var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
+
+ // Elements to highlight in the file listing view
+ var fileListingElements = ['td.pct.low'];
+
+ // We don't want to select elements that are direct descendants of another match
+ var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
+
+ // Selecter that finds elements on the page to which we can jump
+ var selector =
+ fileListingElements.join(', ') +
+ ', ' +
+ notSelector +
+ missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
+
+ // The NodeList of matching elements
+ var missingCoverageElements = document.querySelectorAll(selector);
+
+ var currentIndex;
+
+ function toggleClass(index) {
+ missingCoverageElements
+ .item(currentIndex)
+ .classList.remove('highlighted');
+ missingCoverageElements.item(index).classList.add('highlighted');
+ }
+
+ function makeCurrent(index) {
+ toggleClass(index);
+ currentIndex = index;
+ missingCoverageElements.item(index).scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center'
+ });
+ }
+
+ function goToPrevious() {
+ var nextIndex = 0;
+ if (typeof currentIndex !== 'number' || currentIndex === 0) {
+ nextIndex = missingCoverageElements.length - 1;
+ } else if (missingCoverageElements.length > 1) {
+ nextIndex = currentIndex - 1;
+ }
+
+ makeCurrent(nextIndex);
+ }
+
+ function goToNext() {
+ var nextIndex = 0;
+
+ if (
+ typeof currentIndex === 'number' &&
+ currentIndex < missingCoverageElements.length - 1
+ ) {
+ nextIndex = currentIndex + 1;
+ }
+
+ makeCurrent(nextIndex);
+ }
+
+ return function jump(event) {
+ if (
+ document.getElementById('fileSearch') === document.activeElement &&
+ document.activeElement != null
+ ) {
+ // if we're currently focused on the search input, we don't want to navigate
+ return;
+ }
+
+ switch (event.which) {
+ case 78: // n
+ case 74: // j
+ goToNext();
+ break;
+ case 66: // b
+ case 75: // k
+ case 80: // p
+ goToPrevious();
+ break;
+ }
+ };
+})();
+window.addEventListener('keydown', jumpToCode);
diff --git a/src/services/nodejs/coverage/lcov-report/favicon.png b/src/services/nodejs/coverage/lcov-report/favicon.png
new file mode 100644
index 0000000..c1525b8
Binary files /dev/null and b/src/services/nodejs/coverage/lcov-report/favicon.png differ
diff --git a/src/services/nodejs/coverage/lcov-report/index.html b/src/services/nodejs/coverage/lcov-report/index.html
new file mode 100644
index 0000000..9e228c5
--- /dev/null
+++ b/src/services/nodejs/coverage/lcov-report/index.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+ Code coverage report for All files
+
+
+
+
+
+
+
+
+
+
+
+
All files
+
+
+
+ Unknown%
+ Statements
+ 0/0
+
+
+
+
+ Unknown%
+ Branches
+ 0/0
+
+
+
+
+ Unknown%
+ Functions
+ 0/0
+
+
+
+
+ Unknown%
+ Lines
+ 0/0
+
+
+
+
+
+ Press n or j to go to the next uncovered block, b, p or k for the previous block.
+
+
+
+ Filter:
+
+
+
+
+
+
+
+
+
+ File |
+ |
+ Statements |
+ |
+ Branches |
+ |
+ Functions |
+ |
+ Lines |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/services/nodejs/coverage/lcov-report/prettify.css b/src/services/nodejs/coverage/lcov-report/prettify.css
new file mode 100644
index 0000000..b317a7c
--- /dev/null
+++ b/src/services/nodejs/coverage/lcov-report/prettify.css
@@ -0,0 +1 @@
+.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
diff --git a/src/services/nodejs/coverage/lcov-report/prettify.js b/src/services/nodejs/coverage/lcov-report/prettify.js
new file mode 100644
index 0000000..b322523
--- /dev/null
+++ b/src/services/nodejs/coverage/lcov-report/prettify.js
@@ -0,0 +1,2 @@
+/* eslint-disable */
+window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^`);
- } else if (call.startsWith("ajax")) {
- const [_, src] = call.split(",");
- resolve(
- ``,
- );
- } else if (call.startsWith("cache")) {
- const [_, timeout] = call.split(",");
- resolve(loadFromCache(timeout, txn));
- } else if (call.startsWith("log")) {
- const logging = call.split(",");
- if (logging.length > 2) {
- resolve(logMessage(logging[1], logging[2]));
- } else {
- resolve(logMessage("info", logging[1]));
- }
- } else if (call.startsWith("code")) {
- const [_, script] = call.split(",");
- executeCustomScript(script, req, resolve, reject);
- } else {
- // No other methods are currently implemented
- resolve(`${call} is not supported`);
- }
- });
-}
-
-async function processRequest(req, res, params) {
- const path = url.parse(req.url).pathname;
- logger.info("Request Headers:", req.headers);
- if (endpoints.hasOwnProperty(path)) {
- try {
- const results = [];
- for (let i = 0; i < endpoints[path].length; i++) {
- const call = endpoints[path][i];
- results.push(await processCall(call, req));
- }
- var contype = req.headers["content-type"];
-
- if (req.query.output && req.query.output === "javascript") {
- res.send(results);
- } else {
- res.send(results);
- }
- } catch (reason) {
- logger.error(reason.message);
- res
- .status(typeof reason.code === "number" ? reason.code : 500)
- .send(reason.message);
- }
- } else {
- res.status(404).send("404");
- }
-}
-
-app.get("/**", function (req, res) {
- processRequest(req, res, req.query);
-});
-
-app.post("/**", function (req, res) {
- processRequest(req, res, req.body);
-});
-
-var server = app.listen(port, () =>
- console.log(`Running ${config.name} (type: ${config.type}) on port ${port}`),
-);
-console.log(`Configuration:`);
-console.log(JSON.stringify(config));
-console.log(`Endpoints:`);
-console.log(JSON.stringify(endpoints));
-
-if (config.hasOwnProperty("options")) {
- server.on("connection", (socket) => {
- if (config.options.hasOwnProperty("connectionDelay")) {
- sleep.msleep(config.options.connectionDelay);
- }
- if (
- config.options.hasOwnProperty("lossRate") &&
- parseFloat(config.options.lossRate) >= Math.random()
- ) {
- socket.end();
- throw new Error("An error occurred");
- }
- });
-}
+startServer(config, logger, customCodeDir, port);
diff --git a/src/services/nodejs/package.json b/src/services/nodejs/package.json
index 036bb1d..3f6c920 100644
--- a/src/services/nodejs/package.json
+++ b/src/services/nodejs/package.json
@@ -1,21 +1,21 @@
{
+ "name": "app-sim-",
"dependencies": {
"chance": "^1.1.3",
"cronmatch": "^0.1.1",
"express": "^4.17.1",
- "log4js": "^3.0.6",
- "morgan": "^1.9.1",
- "request": "^2.88.2",
- "request-promise": "^4.2.4",
- "sleep": "^6.3.0"
+ "log4js": "^6.9.1"
},
"devDependencies": {
- "eslint": "^6.2.1",
- "eslint-config-standard": "^14.0.0",
- "eslint-plugin-import": "^2.18.2",
- "eslint-plugin-json": "^1.4.0",
- "eslint-plugin-node": "^9.1.0",
- "eslint-plugin-promise": "^4.2.1",
- "eslint-plugin-standard": "^4.0.1"
- }
+ "eslint": "9.19.0",
+ "eslint-plugin-json": "^4.0.1",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-promise": "^7.2.1",
+ "eslint-plugin-standard": "^4.0.1",
+ "supertest": "^7.0.0"
+ },
+ "scripts": {
+ "test": "node --test tests/**/*.js"
+ },
+ "type": "module"
}
diff --git a/src/services/nodejs/run.sh b/src/services/nodejs/run.sh
index f87865e..d6167c4 100755
--- a/src/services/nodejs/run.sh
+++ b/src/services/nodejs/run.sh
@@ -1,2 +1,2 @@
#!/bin/bash
-env CUSTOM_CODE_DIR="./scripts" APP_CONFIG="$(<../examples/frontend.json)" nodemon index.js 8080
+env CUSTOM_CODE_DIR="./scripts" APP_CONFIG="$(<../../../examples/frontend.json)" node --watch index.js 8080
diff --git a/src/services/nodejs/src/app.js b/src/services/nodejs/src/app.js
new file mode 100644
index 0000000..5d0a6dc
--- /dev/null
+++ b/src/services/nodejs/src/app.js
@@ -0,0 +1,104 @@
+
+import express from "express";
+import bodyParser from "body-parser";
+import url from "url";
+import { msleep } from "./sleep.js";
+import processCall from "./processCall.js";
+
+
+function createApp(config, logger, customCodeDir) {
+
+ const endpoints = config.endpoints.http;
+
+
+ Object.keys(endpoints).forEach(function (key) {
+ if (!key.startsWith("/")) {
+ endpoints["/" + key] = endpoints[key];
+ delete endpoints[key];
+ }
+ });
+
+ const app = express();
+
+ app.use(bodyParser.json()); // for parsing application/json
+ app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
+
+ app.use((req, res, next) => {
+ const start = process.hrtime();
+ res.on('finish', () => {
+ const duration = process.hrtime(start);
+ const responseTime = duration[0] * 1000 + duration[1] / 1e6;
+ const logMessage = `${req.ip} - "${req.method} ${req.originalUrl} HTTP/${req.httpVersion}" ${res.statusCode} ${res.get('Content-Length') || 0} "${req.headers['user-agent']}" - ${responseTime.toFixed(3)} ms`;
+ logger.debug(logMessage);
+ });
+ next();
+ });
+
+ async function processRequest(req, res, params) {
+ const path = new URL(req.url, `http://${req.headers.host}`).pathname;
+ logger.info("Request Headers:", req.headers);
+ if (endpoints.hasOwnProperty(path)) {
+ try {
+ const results = [];
+ for (let i = 0; i < endpoints[path].length; i++) {
+ const call = endpoints[path][i];
+ results.push(await processCall(call, req, logger, customCodeDir));
+ }
+ if (req.query.output && req.query.output === "javascript") {
+ res.send(results);
+ } else {
+ res.send(results);
+ }
+ } catch (reason) {
+ logger.error(reason);
+ res
+ .status(typeof reason.code === "number" ? reason.code : 500)
+ .send(reason.message);
+ }
+ } else {
+ res.status(404).send("404");
+ }
+ }
+
+
+
+
+ app.get("/**", function (req, res) {
+ processRequest(req, res, req.query);
+ });
+
+ app.post("/**", function (req, res) {
+ processRequest(req, res, req.body);
+ });
+
+
+ return app;
+}
+
+function startServer(config, logger, customCodeDir, port = 8080) {
+
+ const app = createApp(config, logger, customCodeDir)
+
+ const server = app.listen(port, () =>
+ logger.info(`Running ${config.name} (type: ${config.type}) on port ${port}`),
+ );
+ logger.debug(`Configuration:`);
+ logger.debug(JSON.stringify(config));
+
+ if (config.hasOwnProperty("options")) {
+ server.on("connection", (socket) => {
+ if (config.options.hasOwnProperty("connectionDelay")) {
+ msleep(config.options.connectionDelay);
+ }
+ if (
+ config.options.hasOwnProperty("lossRate") &&
+ parseFloat(config.options.lossRate) >= Math.random()
+ ) {
+ socket.end();
+ throw new Error("An error occurred");
+ }
+ });
+ }
+}
+
+export { createApp, startServer };
\ No newline at end of file
diff --git a/src/services/nodejs/src/app.test.js b/src/services/nodejs/src/app.test.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/services/nodejs/src/commands/buildResponse.js b/src/services/nodejs/src/commands/buildResponse.js
new file mode 100644
index 0000000..e2b1782
--- /dev/null
+++ b/src/services/nodejs/src/commands/buildResponse.js
@@ -0,0 +1,12 @@
+function buildResponse(timeout) {
+ const start = process.hrtime();
+ let elapsed = process.hrtime(start);
+ let response = "";
+ while (elapsed[0] * 1000000000 + elapsed[1] < timeout * 1000000) {
+ response += " ";
+ elapsed = process.hrtime(start);
+ }
+ return response.length + " slow response";
+}
+
+export default buildResponse;
\ No newline at end of file
diff --git a/src/services/nodejs/src/commands/callRemoteService.js b/src/services/nodejs/src/commands/callRemoteService.js
new file mode 100644
index 0000000..5b11bb0
--- /dev/null
+++ b/src/services/nodejs/src/commands/callRemoteService.js
@@ -0,0 +1,43 @@
+import http from "http";
+import https from "https";
+import { URL } from "url";
+
+function callRemoteService(
+ call,
+ catchExceptions,
+ remoteTimeout,
+ req,
+ resolve,
+ reject,
+) {
+ let headers = {
+ "Content-Type": "application/json",
+ };
+ // removed logic to decide what happens w/ w/o agent
+ headers = req.headers;
+ const opts = {
+ ...new URL(call),
+ headers,
+ };
+
+ const h = opts.protocol === "https:" ? https : http;
+
+ const r = h
+ .get(opts, function (res, req) {
+ const body = [];
+ res.on("data", (chunk) => body.push(chunk));
+ res.on("end", () => resolve(body.join("")));
+ })
+ .on("error", function (err) {
+ if (catchExceptions) {
+ resolve(err);
+ } else {
+ reject(err);
+ }
+ });
+ r.setTimeout(remoteTimeout, function () {
+ reject({ code: 500, message: "Read timed out" });
+ });
+}
+
+export default callRemoteService;
\ No newline at end of file
diff --git a/src/services/nodejs/src/commands/executeCustomScript.js b/src/services/nodejs/src/commands/executeCustomScript.js
new file mode 100644
index 0000000..0f4f4da
--- /dev/null
+++ b/src/services/nodejs/src/commands/executeCustomScript.js
@@ -0,0 +1,31 @@
+
+import { msleep } from "../sleep.js"
+import path from "path";
+import Chance from "chance";
+
+const chance = new Chance();
+
+function executeCustomScript(customCodeDir, script, req, resolve, reject) {
+ var r = require(path.join(customCodeDir, script))({
+ logger: logger,
+ req: req,
+ cronmatch: cronmatch,
+ sleep: msleep,
+ chance: chance,
+ });
+ if (r === false) {
+ reject(`Script ${script} was not executed successfully`);
+ } else if (
+ typeof r === "object" &&
+ r.hasOwnProperty("code") &&
+ r.hasOwnProperty("code")
+ ) {
+ reject({ code: r.code, message: r.message });
+ } else if (typeof r === "string") {
+ resolve(r);
+ } else {
+ resolve(`Script ${script} was executed successfully`);
+ }
+}
+
+export default executeCustomScript;
\ No newline at end of file
diff --git a/src/services/nodejs/src/commands/logMessage.js b/src/services/nodejs/src/commands/logMessage.js
new file mode 100644
index 0000000..b66d659
--- /dev/null
+++ b/src/services/nodejs/src/commands/logMessage.js
@@ -0,0 +1,11 @@
+
+function logMessage(logger, level, message) {
+ if (logger.hasOwnProperty(level)) {
+ logger[level](message);
+ } else {
+ logger.info(message);
+ }
+ return "Logged (" + level + "): " + message;
+ }
+
+ export default logMessage;
\ No newline at end of file
diff --git a/src/services/nodejs/src/logger.js b/src/services/nodejs/src/logger.js
new file mode 100644
index 0000000..c690fad
--- /dev/null
+++ b/src/services/nodejs/src/logger.js
@@ -0,0 +1,29 @@
+import log4js from "log4js";
+
+export const logDir = process.env.LOG_DIRECTORY ? process.env.LOG_DIRECTORY : ".";
+
+log4js.configure({
+ appenders: {
+ FILE: {
+ type: "file",
+ filename: `${logDir}/node.log`,
+ layout: {
+ type: "pattern",
+ pattern: "%d{yyyy-MM-dd hh:mm:ss,SSS} [%z] [%X{AD.requestGUID}] %p %c - %m",
+ },
+ },
+ CONSOLE: {
+ type: "stdout",
+ layout: {
+ type: "pattern",
+ pattern: "%d{yyyy-MM-dd hh:mm:ss,SSS} [%z] [%X{AD.requestGUID}] %p %c - %m",
+ },
+ },
+ },
+ categories: { default: { appenders: ["CONSOLE", "FILE"], level: "info" } },
+});
+
+var logger = log4js.getLogger();
+logger.level = "debug";
+
+export default logger;
\ No newline at end of file
diff --git a/src/services/nodejs/src/processCall.js b/src/services/nodejs/src/processCall.js
new file mode 100644
index 0000000..ca3d248
--- /dev/null
+++ b/src/services/nodejs/src/processCall.js
@@ -0,0 +1,115 @@
+import cronmatch from "cronmatch";
+
+import logMessage from "./commands/logMessage.js";
+import buildResponse from "./commands/buildResponse.js";
+import callRemoteService from "./commands/callRemoteService.js";
+import { exec } from "child_process";
+
+const commands = {
+ "error": ([code, message], _, reject) => {
+ reject({ code, message });
+ },
+ "sleep": ([timeout], resolve, _) => {
+ setTimeout(function () {
+ resolve(`Slept for ${timeout}`);
+ }, timeout);
+ },
+ "slow": ([timeout], resolve, _) => {
+ resolve(buildResponse(timeout));
+ },
+ "image": ([src], resolve, _) => {
+ resolve(`
`);
+ },
+ "script": ([src], resolve, _) => {
+ resolve(``);
+ },
+ "ajax": ([src], resolve, _) => {
+ resolve(
+ ``,
+ );
+ },
+ "log": (args, resolve, _, logger) => {
+ if (args.length > 1) {
+ console.log(args[0], args[1], args.length)
+ resolve(logMessage(logger, args[0], args[1]));
+ } else {
+ resolve(logMessage(logger, "info", args[0]));
+ }
+ }
+}
+
+function preprocessCall(call) {
+
+ const result = {
+ call: call,
+ remoteTimeout: 1073741824,
+ catchExceptions: true,
+ skip: false,
+ skipReason: ""
+ }
+
+ // If call is an array, select one element as call
+ if (Array.isArray(call)) {
+ result.call = call[Math.floor(Math.random() * call.length)];
+ }
+ // If call is an object, check for probability
+ if (typeof call === "object") {
+ if (
+ call.hasOwnProperty("probability") &&
+ call.probability <= Math.random()
+ ) {
+ skipReason = `${call.call} was not probable`
+ result.skip = true;
+ } else if (
+ call.hasOwnProperty("schedule") &&
+ !cronmatch.match(call.schedule, new Date())
+ ) {
+ skipReason = `${call.call} was not scheduled`
+ result.skip = true
+ }
+
+ if (call.hasOwnProperty("remoteTimeout")) {
+ result.remoteTimeout = call.remoteTimeout;
+ }
+ if (call.hasOwnProperty("catchExceptions")) {
+ result.catchExceptions = call.catchExceptions;
+ }
+ return result
+ }
+
+ return result;
+}
+
+function processCall(unprocessedCall, req, logger, customCodeDir) {
+ return new Promise(function (resolve, reject) {
+ logger.debug(unprocessedCall);
+ const { call, remoteTimeout, catchExceptions, skipReason, skip } = preprocessCall(unprocessedCall);
+ if (skip) {
+ resolve(skipReason);
+ } else {
+ if (call.startsWith("http://") || call.startsWith("https://")) {
+ callRemoteService(
+ call,
+ catchExceptions,
+ remoteTimeout,
+ req,
+ resolve,
+ reject,
+ );
+ } else {
+ const [cmd, ...args] = call.split(",");
+ if(commands.hasOwnProperty(cmd)) {
+ commands[cmd](args, resolve, reject, logger);
+ } else if(cmd === "code") {
+ const [scripts] = args;
+ executeCustomScript(customCodeDir, script, req, resolve, reject)
+ } else {
+ resolve(`${cmd} is not supported`);
+ }
+ }
+ }
+ });
+
+}
+
+export default processCall;
\ No newline at end of file
diff --git a/src/services/nodejs/src/sleep.js b/src/services/nodejs/src/sleep.js
new file mode 100644
index 0000000..6570489
--- /dev/null
+++ b/src/services/nodejs/src/sleep.js
@@ -0,0 +1,10 @@
+
+function msleep(n) {
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, n);
+}
+
+function sleep(n) {
+ msleep(n*1000);
+}
+
+export { msleep, sleep };
\ No newline at end of file
diff --git a/src/services/nodejs/tests/app.test.js b/src/services/nodejs/tests/app.test.js
new file mode 100644
index 0000000..a208764
--- /dev/null
+++ b/src/services/nodejs/tests/app.test.js
@@ -0,0 +1,120 @@
+import { describe, before, it, mock } from 'node:test';
+import assert from 'node:assert';
+import supertest from 'supertest';
+
+import http from 'http';
+import https from 'https';
+
+import { createApp } from '../src/app.js';
+
+describe('Test the app with example configurations', async () => {
+ const logger = {
+ debug: () => { },
+ info: () => { },
+ error: () => { },
+ warn: () => { }
+ }
+ it('runs an app and returns a 404 for an empty config', (t, done) => {
+ let app = createApp({
+ 'endpoints': {
+ 'http': {}
+ },
+ }, logger);
+ supertest(app).get('/hello').expect(404).end(done)
+ });
+ it('runs an app and returns a valid response for a basic config', (t, done) => {
+ let app = createApp({
+ 'endpoints': {
+ 'http': {
+ '/hello': [
+ "sleep,50",
+ ]
+ }
+ },
+ }, logger);
+ supertest(app).get('/hello').expect(200).then((response) => {
+ assert.strictEqual(response.text, '["Slept for 50"]');
+ done();
+ });
+ });
+ it('runs an app and returns an error response for a basic config with error', (t, done) => {
+ let app = createApp({
+ 'endpoints': {
+ 'http': {
+ '/hello': [
+ "error,500,This is an error"
+ ]
+ }
+ },
+ }, logger);
+ supertest(app).get('/hello').expect(500).then((response) => {
+ assert.strictEqual(response.text, 'This is an error');
+ done();
+ });
+ });
+ it('writes a log for a basic config with a log message', (t, done) => {
+ mock.method(logger, 'warn');
+ let app = createApp({
+ 'endpoints': {
+ 'http': {
+ '/hello': [
+ "log,warn,This is a log message"
+ ]
+ }
+ },
+ }, logger);
+ supertest(app).get('/hello').expect(200).end(() => {
+ assert.strictEqual(logger.warn.mock.callCount(), 1);
+ done();
+ })
+ });
+ it('returns some HTML based on the configuration', (t, done) => {
+ mock.method(logger, 'warn');
+ let app = createApp({
+ 'endpoints': {
+ 'http': {
+ '/hello': [
+ "image,/image.jpg",
+ "script,/script.js",
+ "ajax,/ajax.json"
+ ]
+ }
+ },
+ }, logger);
+ supertest(app).get('/hello').expect(200).then((response) => {
+ assert.strictEqual(response.text, '["
","",""]');
+ done();
+ })
+ });
+ it('connects with a remote service for HTTP calls', (t, done) => {
+ // Mock http.request
+ mock.method(http, 'get', (options, callback) => {
+ const mockResponse = new http.IncomingMessage();
+ mockResponse.statusCode = 200;
+ mockResponse.headers = { 'content-type': 'application/json' };
+
+ process.nextTick(() => {
+ callback(mockResponse);
+ mockResponse.emit('data', 'Hello from example');
+ mockResponse.emit('end');
+ });
+
+ return new http.ClientRequest();
+ });
+ let app = createApp({
+ 'endpoints': {
+ 'http': {
+ '/hello': [
+ "http://example/hello",
+ "https://mockservice/echo"
+ ]
+ }
+ },
+ }, logger);
+ supertest(app).get('/hello').expect(200).then((response) => {
+ assert.strictEqual(http.get.mock.callCount(), 2);
+ assert.strictEqual(response.text, '["Hello from example","Hello from example"]');
+ done();
+ });
+ });
+});
\ No newline at end of file