@usebruno/js 0.2.0 → 0.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebruno/js",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "main": "src/index.js",
5
5
  "files": [
6
6
  "src",
@@ -14,14 +14,16 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@usebruno/query": "0.1.0",
17
- "atob": "^2.1.2",
18
17
  "ajv": "^8.12.0",
18
+ "atob": "^2.1.2",
19
+ "axios": "^0.26.0",
19
20
  "btoa": "^1.2.1",
20
21
  "crypto-js": "^4.1.1",
21
22
  "json-query": "^2.2.2",
22
23
  "lodash": "^4.17.21",
23
24
  "moment": "^2.29.4",
24
25
  "nanoid": "3.3.4",
26
+ "node-fetch": "2.*",
25
27
  "uuid": "^9.0.0"
26
28
  }
27
- }
29
+ }
package/src/bru.js CHANGED
@@ -5,6 +5,10 @@ class Bru {
5
5
  this._collectionVariables = collectionVariables;
6
6
  }
7
7
 
8
+ getProcessEnv(key) {
9
+ return process.env[key];
10
+ }
11
+
8
12
  getEnvVar(key) {
9
13
  return this._envVariables[key];
10
14
  }
@@ -16,7 +20,7 @@ class Bru {
16
20
 
17
21
  // gracefully ignore if key is not present in environment
18
22
  if(!this._envVariables.hasOwnProperty(key)) {
19
- return;
23
+ return;
20
24
  }
21
25
 
22
26
  this._envVariables[key] = value;
@@ -35,4 +39,4 @@ class Bru {
35
39
  }
36
40
  }
37
41
 
38
- module.exports = Bru;
42
+ module.exports = Bru;
@@ -1,26 +1,26 @@
1
1
  class BrunoResponse {
2
2
  constructor(res) {
3
3
  this.res = res;
4
- this.status = res.status;
5
- this.statusText = res.statusText;
6
- this.headers = res.headers;
7
- this.body = res.data;
4
+ this.status = res ? res.status : null;
5
+ this.statusText = res ? res.statusText : null;
6
+ this.headers = res ? res.headers : null;
7
+ this.body = res ? res.data : null;
8
8
  }
9
9
 
10
10
  getStatus() {
11
- return this.res.status;
11
+ return this.res ? this.res.status : null;
12
12
  }
13
13
 
14
14
  getHeader(name) {
15
- return this.res.header[name];
15
+ return (this.res && this.res.headers) ? this.res.headers[name] : null;
16
16
  }
17
17
 
18
18
  getHeaders() {
19
- return this.res.headers;
19
+ return this.res ? this.res.headers : null;
20
20
  }
21
21
 
22
22
  getBody() {
23
- return this.res.data;
23
+ return this.res ? this.res.data : null;
24
24
  }
25
25
  }
26
26
 
@@ -1,17 +1,49 @@
1
1
  const _ = require('lodash');
2
- const chai = require('chai');
2
+ const chai = require('chai');
3
+ const { nanoid } = require('nanoid');
3
4
  const Bru = require('../bru');
4
5
  const BrunoRequest = require('../bruno-request');
5
- const { evaluateJsExpression, createResponseParser } = require('../utils');
6
+ const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
6
7
 
7
8
  const { expect } = chai;
9
+ chai.use(function (chai, utils) {
10
+ // Custom assertion for checking if a variable is JSON
11
+ chai.Assertion.addProperty('json', function () {
12
+ const obj = this._obj;
13
+ const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) && obj.constructor === Object;
14
+
15
+ this.assert(
16
+ isJson,
17
+ `expected ${utils.inspect(obj)} to be JSON`,
18
+ `expected ${utils.inspect(obj)} not to be JSON`
19
+ );
20
+ });
21
+ });
22
+
23
+ // Custom assertion for matching regex
24
+ chai.use(function (chai, utils) {
25
+ chai.Assertion.addMethod('match', function (regex) {
26
+ const obj = this._obj;
27
+ let match = false;
28
+ if(obj === undefined) {
29
+ match = false;
30
+ } else {
31
+ match = regex.test(obj);
32
+ }
33
+
34
+ this.assert(
35
+ match,
36
+ `expected ${utils.inspect(obj)} to match ${regex}`,
37
+ `expected ${utils.inspect(obj)} not to match ${regex}`
38
+ );
39
+ });
40
+ });
8
41
 
9
42
  /**
10
43
  * Assertion operators
11
44
  *
12
45
  * eq : equal to
13
46
  * neq : not equal to
14
- * like : like
15
47
  * gt : greater than
16
48
  * gte : greater than or equal to
17
49
  * lt : less than
@@ -20,7 +52,6 @@ const { expect } = chai;
20
52
  * notIn : not in
21
53
  * contains : contains
22
54
  * notContains : not contains
23
- * count : count
24
55
  * length : length
25
56
  * matches : matches
26
57
  * notMatches : not matches
@@ -47,15 +78,26 @@ const parseAssertionOperator = (str = '') => {
47
78
  }
48
79
 
49
80
  const operators = [
50
- 'eq', 'neq', 'like', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
51
- 'contains', 'notContains', 'count', 'length', 'matches', 'notMatches',
81
+ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',
82
+ 'contains', 'notContains', 'length', 'matches', 'notMatches',
52
83
  'startsWith', 'endsWith', 'between', 'isEmpty', 'isNull', 'isUndefined',
53
84
  'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
54
85
  ];
55
86
 
87
+ const unaryOperators = [
88
+ 'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
89
+ ];
90
+
56
91
  const [operator, ...rest] = str.trim().split(' ');
57
92
  const value = rest.join(' ');
58
93
 
94
+ if(unaryOperators.includes(operator)) {
95
+ return {
96
+ operator,
97
+ value: ''
98
+ };
99
+ }
100
+
59
101
  if(operators.includes(operator)) {
60
102
  return {
61
103
  operator,
@@ -69,6 +111,45 @@ const parseAssertionOperator = (str = '') => {
69
111
  };
70
112
  };
71
113
 
114
+ const isUnaryOperator = (operator) => {
115
+ const unaryOperators = [
116
+ 'isEmpty', 'isNull', 'isUndefined', 'isDefined', 'isTruthy', 'isFalsy', 'isJson', 'isNumber', 'isString', 'isBoolean'
117
+ ];
118
+
119
+ return unaryOperators.includes(operator);
120
+ };
121
+
122
+ const evaluateRhsOperand = (rhsOperand, operator, context) => {
123
+ if(isUnaryOperator(operator)) {
124
+ return;
125
+ }
126
+
127
+ // gracefully allow both a,b as well as [a, b]
128
+ if(operator === 'in' || operator === 'notIn') {
129
+ if(rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) {
130
+ rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
131
+ }
132
+
133
+ return rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
134
+ }
135
+
136
+ if(operator === 'between') {
137
+ const [lhs, rhs] = rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
138
+ return [lhs, rhs];
139
+ }
140
+
141
+ // gracefully allow both ^[a-Z] as well as /^[a-Z]/
142
+ if(operator === 'matches' || operator === 'notMatches') {
143
+ if(rhsOperand.startsWith('/') && rhsOperand.endsWith('/')) {
144
+ rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
145
+ }
146
+
147
+ return rhsOperand;
148
+ }
149
+
150
+ return evaluateJsTemplateLiteral(rhsOperand, context);
151
+ };
152
+
72
153
  class AssertRuntime {
73
154
  runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath) {
74
155
  const enabledAssertions = _.filter(assertions, (a) => a.enabled);
@@ -105,7 +186,7 @@ class AssertRuntime {
105
186
 
106
187
  try {
107
188
  const lhs = evaluateJsExpression(lhsExpr, context);
108
- const rhs = evaluateJsExpression(rhsOperand, context);
189
+ const rhs = evaluateRhsOperand(rhsOperand, operator, context);
109
190
 
110
191
  switch(operator) {
111
192
  case 'eq':
@@ -114,9 +195,6 @@ class AssertRuntime {
114
195
  case 'neq':
115
196
  expect(lhs).to.not.equal(rhs);
116
197
  break;
117
- case 'like':
118
- expect(lhs).to.match(new RegExp(rhs));
119
- break;
120
198
  case 'gt':
121
199
  expect(lhs).to.be.greaterThan(rhs);
122
200
  break;
@@ -141,9 +219,6 @@ class AssertRuntime {
141
219
  case 'notContains':
142
220
  expect(lhs).to.not.include(rhs);
143
221
  break;
144
- case 'count':
145
- expect(lhs).to.have.lengthOf(rhs);
146
- break;
147
222
  case 'length':
148
223
  expect(lhs).to.have.lengthOf(rhs);
149
224
  break;
@@ -160,7 +235,7 @@ class AssertRuntime {
160
235
  expect(lhs).to.endWith(rhs);
161
236
  break;
162
237
  case 'between':
163
- const [min, max] = value.split(' ');
238
+ const [min, max] = value.split(',');
164
239
  expect(lhs).to.be.within(min, max);
165
240
  break;
166
241
  case 'isEmpty':
@@ -199,6 +274,7 @@ class AssertRuntime {
199
274
  }
200
275
 
201
276
  assertionResults.push({
277
+ uid: nanoid(),
202
278
  lhsExpr,
203
279
  rhsExpr,
204
280
  rhsOperand,
@@ -208,6 +284,7 @@ class AssertRuntime {
208
284
  }
209
285
  catch (err) {
210
286
  assertionResults.push({
287
+ uid: nanoid(),
211
288
  lhsExpr,
212
289
  rhsExpr,
213
290
  rhsOperand,
@@ -222,4 +299,4 @@ class AssertRuntime {
222
299
  }
223
300
  }
224
301
 
225
- module.exports = AssertRuntime;
302
+ module.exports = AssertRuntime;
@@ -1,5 +1,12 @@
1
1
  const { NodeVM } = require('vm2');
2
2
  const path = require('path');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const stream = require('stream');
6
+ const util = require('util');
7
+ const zlib = require('zlib');
8
+ const url = require('url');
9
+ const punycode = require('punycode');
3
10
  const Bru = require('../bru');
4
11
  const BrunoRequest = require('../bruno-request');
5
12
  const BrunoResponse = require('../bruno-response');
@@ -11,16 +18,17 @@ const lodash = require('lodash');
11
18
  const moment = require('moment');
12
19
  const uuid = require('uuid');
13
20
  const nanoid = require('nanoid');
21
+ const axios = require('axios');
22
+ const fetch = require('node-fetch');
14
23
  const CryptoJS = require('crypto-js');
15
24
 
16
25
  class ScriptRuntime {
17
26
  constructor() {
18
27
  }
19
28
 
20
- runRequestScript(script, request, environment, collectionVariables, collectionPath) {
21
- const bru = new Bru(environment, collectionVariables);
29
+ async runRequestScript(script, request, envVariables, collectionVariables, collectionPath){
30
+ const bru = new Bru(envVariables, collectionVariables);
22
31
  const req = new BrunoRequest(request);
23
-
24
32
  const context = {
25
33
  bru,
26
34
  req
@@ -32,28 +40,39 @@ class ScriptRuntime {
32
40
  external: true,
33
41
  root: [collectionPath],
34
42
  mock: {
43
+ // node libs
44
+ path,
45
+ stream,
46
+ util,
47
+ url,
48
+ http,
49
+ https,
50
+ punycode,
51
+ zlib,
52
+ // 3rd party libs
35
53
  atob,
36
54
  btoa,
37
55
  lodash,
38
56
  moment,
39
57
  uuid,
40
58
  nanoid,
59
+ axios,
60
+ 'node-fetch': fetch,
41
61
  'crypto-js': CryptoJS
42
62
  }
43
63
  }
44
64
  });
45
-
46
- vm.run(script, path.join(collectionPath, 'vm.js'));
47
-
65
+ const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
66
+ await asyncVM();
48
67
  return {
49
68
  request,
50
- environment,
69
+ envVariables,
51
70
  collectionVariables
52
71
  };
53
72
  }
54
73
 
55
- runResponseScript(script, request, response, environment, collectionVariables, collectionPath) {
56
- const bru = new Bru(environment, collectionVariables);
74
+ async runResponseScript(script, request, response, envVariables, collectionVariables, collectionPath) {
75
+ const bru = new Bru(envVariables, collectionVariables);
57
76
  const req = new BrunoRequest(request);
58
77
  const res = new BrunoResponse(response);
59
78
 
@@ -75,16 +94,19 @@ class ScriptRuntime {
75
94
  moment,
76
95
  uuid,
77
96
  nanoid,
97
+ axios,
98
+ 'node-fetch': fetch,
78
99
  'crypto-js': CryptoJS
79
100
  }
80
101
  }
81
102
  });
82
103
 
83
- vm.run(script, path.join(collectionPath, 'vm.js'));
104
+ const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
105
+ await asyncVM();
84
106
 
85
107
  return {
86
108
  response,
87
- environment,
109
+ envVariables,
88
110
  collectionVariables
89
111
  };
90
112
  }
@@ -20,14 +20,23 @@ class TestRuntime {
20
20
  constructor() {
21
21
  }
22
22
 
23
- runTests(testsFile, request, response, environment, collectionVariables, collectionPath) {
24
- const bru = new Bru(environment, collectionVariables);
23
+ runTests(testsFile, request, response, envVariables, collectionVariables, collectionPath) {
24
+ const bru = new Bru(envVariables, collectionVariables);
25
25
  const req = new BrunoRequest(request);
26
26
  const res = new BrunoResponse(response);
27
27
 
28
28
  const __brunoTestResults = new TestResults();
29
29
  const test = Test(__brunoTestResults, chai);
30
30
 
31
+ if(!testsFile || !testsFile.length) {
32
+ return {
33
+ request,
34
+ envVariables,
35
+ collectionVariables,
36
+ results: __brunoTestResults.getResults()
37
+ };
38
+ }
39
+
31
40
  const context = {
32
41
  test,
33
42
  bru,
@@ -60,7 +69,7 @@ class TestRuntime {
60
69
 
61
70
  return {
62
71
  request,
63
- environment,
72
+ envVariables,
64
73
  collectionVariables,
65
74
  results: __brunoTestResults.getResults()
66
75
  };
@@ -1,7 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const Bru = require('../bru');
3
3
  const BrunoRequest = require('../bruno-request');
4
- const { evaluateJsExpression, createResponseParser } = require('../utils');
4
+ const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
5
5
 
6
6
  class VarsRuntime {
7
7
  runPreRequestVars(vars, request, envVariables, collectionVariables, collectionPath) {
@@ -25,9 +25,13 @@ class VarsRuntime {
25
25
  }
26
26
 
27
27
  _.each(enabledVars, (v) => {
28
- const value = evaluateJsExpression(v.value, context);
28
+ const value = evaluateJsTemplateLiteral(v.value, context);
29
29
  bru.setVar(v.name, value);
30
30
  });
31
+
32
+ return {
33
+ collectionVariables
34
+ };
31
35
  }
32
36
 
33
37
  runPostResponseVars(vars, request, response, envVariables, collectionVariables, collectionPath) {
@@ -56,6 +60,11 @@ class VarsRuntime {
56
60
  const value = evaluateJsExpression(v.value, context);
57
61
  bru.setVar(v.name, value);
58
62
  });
63
+
64
+ return {
65
+ envVariables,
66
+ collectionVariables
67
+ };
59
68
  }
60
69
  }
61
70
 
package/src/utils.js CHANGED
@@ -60,6 +60,46 @@ const evaluateJsExpression = (expression, context) => {
60
60
  return fn(context);
61
61
  };
62
62
 
63
+ const evaluateJsTemplateLiteral = (templateLiteral, context) => {
64
+ if(!templateLiteral || !templateLiteral.length || typeof templateLiteral !== 'string') {
65
+ return templateLiteral;
66
+ }
67
+
68
+ templateLiteral = templateLiteral.trim();
69
+
70
+ if(templateLiteral === 'true') {
71
+ return true;
72
+ }
73
+
74
+ if(templateLiteral === 'false') {
75
+ return false;
76
+ }
77
+
78
+ if(templateLiteral === 'null') {
79
+ return null;
80
+ }
81
+
82
+ if(templateLiteral === 'undefined') {
83
+ return undefined;
84
+ }
85
+
86
+ if(templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
87
+ return templateLiteral.slice(1, -1);
88
+ }
89
+
90
+ if(templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
91
+ return templateLiteral.slice(1, -1);
92
+ }
93
+
94
+ if(!isNaN(templateLiteral)) {
95
+ return Number(templateLiteral);
96
+ }
97
+
98
+ templateLiteral = "`" + templateLiteral + "`";
99
+
100
+ return evaluateJsExpression(templateLiteral, context);
101
+ };
102
+
63
103
  const createResponseParser = (response = {}) => {
64
104
  const res = (expr, ...fns) => {
65
105
  return get(response.data, expr, ...fns);
@@ -80,6 +120,7 @@ const createResponseParser = (response = {}) => {
80
120
 
81
121
  module.exports = {
82
122
  evaluateJsExpression,
123
+ evaluateJsTemplateLiteral,
83
124
  createResponseParser,
84
125
  internalExpressionCache
85
126
  };