@usebruno/js 0.46.1 → 0.48.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 +9 -7
- package/src/bru.js +173 -75
- package/src/bruno-request.js +15 -7
- package/src/bruno-response.js +4 -0
- package/src/cookie-list.js +272 -0
- package/src/header-list.js +497 -0
- package/src/index.js +27 -2
- package/src/property-list.js +184 -0
- package/src/readonly-property-list.js +227 -0
- package/src/runtime/assert-runtime.js +164 -10
- package/src/runtime/script-runtime.js +55 -6
- package/src/runtime/test-runtime.js +19 -1
- package/src/runtime/vars-runtime.js +20 -3
- package/src/sandbox/bundle-browser-rollup.js +72 -66
- package/src/sandbox/bundle-libraries.js +10 -2
- package/src/sandbox/quickjs/index.js +8 -41
- package/src/sandbox/quickjs/shims/bru.js +31 -1
- package/src/sandbox/quickjs/shims/bruno-request.js +24 -3
- package/src/sandbox/quickjs/shims/bruno-response.js +57 -5
- package/src/sandbox/quickjs/shims/bruno-response.spec.js +91 -0
- package/src/sandbox/quickjs/shims/lib/uuid.spec.js +166 -0
- package/src/sandbox/quickjs/shims/require.js +56 -0
- package/src/sandbox/quickjs/shims/require.spec.js +154 -0
- package/src/sandbox/quickjs/shims/test.js +175 -2
- package/src/sandbox/quickjs/utils/property-list-bridge.js +190 -0
- package/src/sandbox/quickjs/utils/test-helpers.js +31 -0
- package/src/utils/error-formatter.js +345 -20
- package/src/utils/error-formatter.spec.js +683 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const ReadOnlyPropertyList = require('./readonly-property-list');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PropertyList - A mutable collection data structure.
|
|
5
|
+
*
|
|
6
|
+
* Extends ReadOnlyPropertyList with mutation methods that operate on the
|
|
7
|
+
* internal _items array in static mode. In dynamic mode, all mutations
|
|
8
|
+
* throw — subclasses (e.g. CookieList) override with async implementations.
|
|
9
|
+
*
|
|
10
|
+
* Class hierarchy:
|
|
11
|
+
* ReadOnlyPropertyList (read-only, both modes)
|
|
12
|
+
* └── PropertyList (sync mutations in static mode; throws in dynamic mode)
|
|
13
|
+
* └── CookieList (overrides add/upsert/remove/clear/delete with async jar ops)
|
|
14
|
+
*/
|
|
15
|
+
class PropertyList extends ReadOnlyPropertyList {
|
|
16
|
+
/**
|
|
17
|
+
* Guard that throws in dynamic mode. Called by all mutation methods.
|
|
18
|
+
* @param {string} method - Name of the calling method (for error message)
|
|
19
|
+
*/
|
|
20
|
+
#ensureStaticMode(method) {
|
|
21
|
+
if (this._dynamic) {
|
|
22
|
+
throw new Error(`${method}() is not supported in dynamic mode. Override in subclass.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Mutation methods ──────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Append an item to the end of the list.
|
|
30
|
+
* @param {object} item
|
|
31
|
+
*/
|
|
32
|
+
add(item) {
|
|
33
|
+
this.#ensureStaticMode('add');
|
|
34
|
+
this._items.push(item);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Alias for add().
|
|
39
|
+
* @param {object} item
|
|
40
|
+
*/
|
|
41
|
+
append(item) {
|
|
42
|
+
return this.add(item);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Insert an item at the beginning of the list.
|
|
47
|
+
* @param {object} item
|
|
48
|
+
*/
|
|
49
|
+
prepend(item) {
|
|
50
|
+
this.#ensureStaticMode('prepend');
|
|
51
|
+
this._items.unshift(item);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Insert an item before a reference item.
|
|
56
|
+
* @param {object} item - The item to insert
|
|
57
|
+
* @param {string|object} before - Key string or item object to insert before
|
|
58
|
+
*/
|
|
59
|
+
insert(item, before) {
|
|
60
|
+
this.#ensureStaticMode('insert');
|
|
61
|
+
const idx = this.#findIndex(before);
|
|
62
|
+
if (idx === -1) {
|
|
63
|
+
this._items.push(item);
|
|
64
|
+
} else {
|
|
65
|
+
this._items.splice(idx, 0, item);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Insert an item after a reference item.
|
|
71
|
+
* @param {object} item - The item to insert
|
|
72
|
+
* @param {string|object} after - Key string or item object to insert after
|
|
73
|
+
*/
|
|
74
|
+
insertAfter(item, after) {
|
|
75
|
+
this.#ensureStaticMode('insertAfter');
|
|
76
|
+
const idx = this.#findIndex(after);
|
|
77
|
+
if (idx === -1) {
|
|
78
|
+
this._items.push(item);
|
|
79
|
+
} else {
|
|
80
|
+
this._items.splice(idx + 1, 0, item);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove items matching a predicate, key string, or item reference.
|
|
86
|
+
* @param {Function|string|object} predicate
|
|
87
|
+
*/
|
|
88
|
+
remove(predicate) {
|
|
89
|
+
this.#ensureStaticMode('remove');
|
|
90
|
+
if (typeof predicate === 'function') {
|
|
91
|
+
this._items = this._items.filter((item) => !predicate(item));
|
|
92
|
+
} else if (typeof predicate === 'string') {
|
|
93
|
+
this._items = this._items.filter((item) => item[this._keyProperty] !== predicate);
|
|
94
|
+
} else if (predicate && typeof predicate === 'object') {
|
|
95
|
+
const idx = this.indexOf(predicate);
|
|
96
|
+
if (idx !== -1) {
|
|
97
|
+
this._items.splice(idx, 1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove all items from the list.
|
|
104
|
+
*/
|
|
105
|
+
clear() {
|
|
106
|
+
this.#ensureStaticMode('clear');
|
|
107
|
+
this._items = [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Update an existing item by key, or append if not found.
|
|
112
|
+
* @param {object} item
|
|
113
|
+
*/
|
|
114
|
+
upsert(item) {
|
|
115
|
+
this.#ensureStaticMode('upsert');
|
|
116
|
+
const key = item[this._keyProperty];
|
|
117
|
+
const idx = this._items.findIndex((i) => i[this._keyProperty] === key);
|
|
118
|
+
if (idx !== -1) {
|
|
119
|
+
this._items[idx] = item;
|
|
120
|
+
} else {
|
|
121
|
+
this._items.push(item);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Replace all items with a new array.
|
|
127
|
+
* @param {Array} items
|
|
128
|
+
*/
|
|
129
|
+
populate(items) {
|
|
130
|
+
this.#ensureStaticMode('populate');
|
|
131
|
+
this._items = Array.isArray(items) ? [...items] : [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clear and repopulate with new items.
|
|
136
|
+
* @param {Array} items
|
|
137
|
+
*/
|
|
138
|
+
repopulate(items) {
|
|
139
|
+
this.#ensureStaticMode('repopulate');
|
|
140
|
+
this.populate(items);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Merge items from another PropertyList or array.
|
|
145
|
+
* @param {PropertyList|Array} source - Source of items to merge
|
|
146
|
+
* @param {boolean} [prune=false] - If true, clear existing items first
|
|
147
|
+
*/
|
|
148
|
+
assimilate(source, prune) {
|
|
149
|
+
this.#ensureStaticMode('assimilate');
|
|
150
|
+
if (prune) {
|
|
151
|
+
this._items = [];
|
|
152
|
+
}
|
|
153
|
+
let items;
|
|
154
|
+
if (ReadOnlyPropertyList.isPropertyList(source)) {
|
|
155
|
+
items = source.all();
|
|
156
|
+
} else if (Array.isArray(source)) {
|
|
157
|
+
items = source;
|
|
158
|
+
} else {
|
|
159
|
+
items = [];
|
|
160
|
+
}
|
|
161
|
+
for (const item of items) {
|
|
162
|
+
this._items.push(item);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Find the index of a reference (key string or item object).
|
|
170
|
+
* @param {string|object} ref
|
|
171
|
+
* @returns {number}
|
|
172
|
+
*/
|
|
173
|
+
#findIndex(ref) {
|
|
174
|
+
if (typeof ref === 'string') {
|
|
175
|
+
return this._items.findIndex((i) => i[this._keyProperty] === ref);
|
|
176
|
+
}
|
|
177
|
+
if (ref && typeof ref === 'object') {
|
|
178
|
+
return this.indexOf(ref);
|
|
179
|
+
}
|
|
180
|
+
return -1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = PropertyList;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReadOnlyPropertyList - A read-only collection data structure.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - Static mode: items stored internally in an array (for headers, query params, etc.)
|
|
6
|
+
* - Dynamic mode: a dataSource function returns fresh items on every read (for cookies)
|
|
7
|
+
*
|
|
8
|
+
* Items are plain objects with a configurable key property (keyProperty) and value property (valueProperty).
|
|
9
|
+
*
|
|
10
|
+
* This base class provides only read/search/iteration/transform methods.
|
|
11
|
+
* See PropertyList for static-mode mutation methods.
|
|
12
|
+
* See CookieList for async cookie-jar write methods.
|
|
13
|
+
*
|
|
14
|
+
* Convention:
|
|
15
|
+
* #field / #method – truly private, inaccessible to subclasses
|
|
16
|
+
* _field / _method – protected, intended for subclass access only
|
|
17
|
+
*/
|
|
18
|
+
class ReadOnlyPropertyList {
|
|
19
|
+
// ── Private fields (not accessible by subclasses) ────────────────────
|
|
20
|
+
#valueProperty;
|
|
21
|
+
#dataSource;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} options
|
|
25
|
+
* @param {string} [options.keyProperty='key'] - The property name used as the unique key
|
|
26
|
+
* @param {string} [options.valueProperty='value'] - The property name used as the value
|
|
27
|
+
* @param {Function} [options.dataSource] - Dynamic data source function (returns array of items)
|
|
28
|
+
* @param {Array} [options.items] - Initial items for static mode
|
|
29
|
+
*/
|
|
30
|
+
// Items are stored in an array (not a Map) to support positional access (idx, indexOf),
|
|
31
|
+
// ordered insertion (insert, insertAfter, prepend in PropertyList), and duplicate keys.
|
|
32
|
+
// At typical list sizes (cookies, headers) the O(n) key lookup is negligible.
|
|
33
|
+
constructor({ keyProperty = 'key', valueProperty = 'value', dataSource, items } = {}) {
|
|
34
|
+
this._keyProperty = keyProperty;
|
|
35
|
+
this.#valueProperty = valueProperty;
|
|
36
|
+
this._dynamic = typeof dataSource === 'function';
|
|
37
|
+
if (this._dynamic) {
|
|
38
|
+
this.#dataSource = dataSource;
|
|
39
|
+
} else {
|
|
40
|
+
this._items = Array.isArray(items) ? [...items] : [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the current list of items.
|
|
46
|
+
* In dynamic mode, calls the dataSource function.
|
|
47
|
+
* In static mode, returns the internal array.
|
|
48
|
+
*/
|
|
49
|
+
#getItems() {
|
|
50
|
+
return this._dynamic ? this.#dataSource() : this._items;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Retrieval ──────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the value of an item by its key.
|
|
57
|
+
* @param {string} name
|
|
58
|
+
* @returns {*} The value property of the matching item, or undefined
|
|
59
|
+
*/
|
|
60
|
+
get(name) {
|
|
61
|
+
const items = this.#getItems();
|
|
62
|
+
// Use findLast so that duplicate keys resolve to the last entry,
|
|
63
|
+
// consistent with toObject() which also gives last-wins semantics.
|
|
64
|
+
const item = items.findLast((i) => i[this._keyProperty] === name);
|
|
65
|
+
return item ? item[this.#valueProperty] : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the full item object by its key.
|
|
70
|
+
* @param {string} name
|
|
71
|
+
* @returns {object|undefined}
|
|
72
|
+
*/
|
|
73
|
+
one(name) {
|
|
74
|
+
const items = this.#getItems();
|
|
75
|
+
// Use findLast so that duplicate keys resolve to the last entry,
|
|
76
|
+
// consistent with get() and toObject() which also give last-wins semantics.
|
|
77
|
+
return items.findLast((i) => i[this._keyProperty] === name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a cloned array of all items.
|
|
82
|
+
* @returns {Array}
|
|
83
|
+
*/
|
|
84
|
+
all() {
|
|
85
|
+
return [...this.#getItems()];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get an item by its positional index.
|
|
90
|
+
* @param {number} index
|
|
91
|
+
* @returns {object|undefined}
|
|
92
|
+
*/
|
|
93
|
+
idx(index) {
|
|
94
|
+
return this.#getItems()[index];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the number of items.
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
count() {
|
|
102
|
+
return this.#getItems().length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the index of an item.
|
|
107
|
+
* Uses structural equality (matching by key and value) so it works
|
|
108
|
+
* even when the item is a copy rather than the same reference.
|
|
109
|
+
* @param {object} item
|
|
110
|
+
* @returns {number} -1 if not found
|
|
111
|
+
*/
|
|
112
|
+
indexOf(item) {
|
|
113
|
+
if (!item || typeof item !== 'object') return -1;
|
|
114
|
+
const items = this.#getItems();
|
|
115
|
+
const keyProp = this._keyProperty;
|
|
116
|
+
return items.findIndex(
|
|
117
|
+
(i) => i[keyProp] === item[keyProp] && i[this.#valueProperty] === item[this.#valueProperty]
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Search ─────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if an item with the given key exists.
|
|
125
|
+
* If value is provided, also checks that the item's value matches.
|
|
126
|
+
* @param {string} name
|
|
127
|
+
* @param {*} [value]
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
has(name, value) {
|
|
131
|
+
const items = this.#getItems();
|
|
132
|
+
if (value !== undefined) {
|
|
133
|
+
return items.some((i) => i[this._keyProperty] === name && i[this.#valueProperty] === value);
|
|
134
|
+
}
|
|
135
|
+
return items.some((i) => i[this._keyProperty] === name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find the first item matching a predicate.
|
|
140
|
+
* @param {Function} predicate
|
|
141
|
+
* @returns {object|undefined}
|
|
142
|
+
*/
|
|
143
|
+
find(predicate) {
|
|
144
|
+
return this.#getItems().find(predicate);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Filter items by a predicate.
|
|
149
|
+
* @param {Function} predicate
|
|
150
|
+
* @returns {Array}
|
|
151
|
+
*/
|
|
152
|
+
filter(predicate) {
|
|
153
|
+
return this.#getItems().filter(predicate);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Iteration ──────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Iterate over each item.
|
|
160
|
+
* @param {Function} fn - Called with (item, index)
|
|
161
|
+
*/
|
|
162
|
+
each(fn) {
|
|
163
|
+
this.#getItems().forEach(fn);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Map over items.
|
|
168
|
+
* @param {Function} fn
|
|
169
|
+
* @returns {Array}
|
|
170
|
+
*/
|
|
171
|
+
map(fn) {
|
|
172
|
+
return this.#getItems().map(fn);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Reduce items.
|
|
177
|
+
* @param {Function} fn
|
|
178
|
+
* @param {*} [initialValue] - Optional initial accumulator value
|
|
179
|
+
* @returns {*}
|
|
180
|
+
*/
|
|
181
|
+
reduce(fn, ...rest) {
|
|
182
|
+
return rest.length ? this.#getItems().reduce(fn, rest[0]) : this.#getItems().reduce(fn);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Transformation ─────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convert to a plain object { key: value }.
|
|
189
|
+
* @returns {object}
|
|
190
|
+
*/
|
|
191
|
+
toObject() {
|
|
192
|
+
const result = {};
|
|
193
|
+
for (const item of this.#getItems()) {
|
|
194
|
+
result[item[this._keyProperty]] = item[this.#valueProperty];
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert to a string "key=value; key2=value2".
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
toString() {
|
|
204
|
+
return this.#getItems()
|
|
205
|
+
.map((i) => `${i[this._keyProperty]}=${i[this.#valueProperty]}`)
|
|
206
|
+
.join('; ');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Convert to JSON (returns the same as all()).
|
|
211
|
+
* @returns {Array}
|
|
212
|
+
*/
|
|
213
|
+
toJSON() {
|
|
214
|
+
return this.all();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if an object is an instance of ReadOnlyPropertyList.
|
|
219
|
+
* @param {*} obj
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
static isPropertyList(obj) {
|
|
223
|
+
return obj instanceof ReadOnlyPropertyList;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = ReadOnlyPropertyList;
|
|
@@ -7,6 +7,8 @@ const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser, u
|
|
|
7
7
|
const { interpolateString } = require('../interpolate-string');
|
|
8
8
|
const { executeQuickJsVm } = require('../sandbox/quickjs');
|
|
9
9
|
|
|
10
|
+
const Ajv = require('ajv');
|
|
11
|
+
const addFormats = require('ajv-formats');
|
|
10
12
|
const { expect } = chai;
|
|
11
13
|
chai.use(require('chai-string'));
|
|
12
14
|
chai.use(function (chai, utils) {
|
|
@@ -17,13 +19,55 @@ chai.use(function (chai, utils) {
|
|
|
17
19
|
// Objects created inside Node's vm.createContext() have a different Object constructor,
|
|
18
20
|
// so obj.constructor === Object fails for objects passed via res.setBody() from scripts.
|
|
19
21
|
// Note: toString check is more permissive than constructor check — custom class instances
|
|
20
|
-
const isJson = typeof obj === 'object' && obj !== null
|
|
21
|
-
&& Object.prototype.toString.call(obj) === '[object Object]';
|
|
22
|
+
const isJson = typeof obj === 'object' && obj !== null
|
|
23
|
+
&& (Array.isArray(obj) || Object.prototype.toString.call(obj) === '[object Object]');
|
|
22
24
|
|
|
23
25
|
this.assert(isJson, `expected ${utils.inspect(obj)} to be JSON`, `expected ${utils.inspect(obj)} not to be JSON`);
|
|
24
26
|
});
|
|
25
27
|
});
|
|
26
28
|
|
|
29
|
+
// Custom assertion for JSON Schema validation
|
|
30
|
+
const defaultAjv = new Ajv({ allErrors: true });
|
|
31
|
+
addFormats(defaultAjv);
|
|
32
|
+
|
|
33
|
+
const SUPPORTED_SCHEMA_VERSIONS = [
|
|
34
|
+
'http://json-schema.org/draft-07/schema#',
|
|
35
|
+
'http://json-schema.org/draft-07/schema'
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
chai.use(function (chai) {
|
|
39
|
+
chai.Assertion.addMethod('jsonSchema', function (schema, ajvOptions) {
|
|
40
|
+
if (schema && schema.$schema && !SUPPORTED_SCHEMA_VERSIONS.includes(schema.$schema)) {
|
|
41
|
+
this.assert(
|
|
42
|
+
false,
|
|
43
|
+
`Unsupported JSON Schema version: "${schema.$schema}". Bruno currently only supports Draft-07 (http://json-schema.org/draft-07/schema#). Please update your schema to be Draft-07 compatible and remove the $schema property.`,
|
|
44
|
+
`Unsupported JSON Schema version: "${schema.$schema}".`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
let ajv;
|
|
48
|
+
if (ajvOptions) {
|
|
49
|
+
ajv = new Ajv({ allErrors: true, ...ajvOptions });
|
|
50
|
+
addFormats(ajv);
|
|
51
|
+
} else {
|
|
52
|
+
ajv = defaultAjv;
|
|
53
|
+
}
|
|
54
|
+
let validate;
|
|
55
|
+
try {
|
|
56
|
+
validate = ajv.compile(schema);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
this.assert(false, 'JSON schema compile error: ' + e.message, 'JSON schema compile error: ' + e.message);
|
|
59
|
+
}
|
|
60
|
+
const data = this._obj;
|
|
61
|
+
const isValid = validate(data);
|
|
62
|
+
|
|
63
|
+
this.assert(
|
|
64
|
+
isValid,
|
|
65
|
+
'expected #{this} to match JSON schema, validation errors: ' + (validate.errors ? JSON.stringify(validate.errors) : 'none'),
|
|
66
|
+
'expected #{this} to not match JSON schema'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
27
71
|
// Custom assertion for matching regex
|
|
28
72
|
chai.use(function (chai, utils) {
|
|
29
73
|
chai.Assertion.addMethod('match', function (regex) {
|
|
@@ -43,6 +87,118 @@ chai.use(function (chai, utils) {
|
|
|
43
87
|
});
|
|
44
88
|
});
|
|
45
89
|
|
|
90
|
+
// Custom assertion for jsonBody (Postman parity)
|
|
91
|
+
chai.use(function (chai, utils) {
|
|
92
|
+
// Parse a property path into an array of keys.
|
|
93
|
+
// Handles: dot notation (a.b), numeric brackets (a[0]), quoted brackets (a["b.c"], a['key']),
|
|
94
|
+
// and combinations like data[0]["a.b"].name
|
|
95
|
+
//
|
|
96
|
+
// Examples:
|
|
97
|
+
// "a.b.c" -> ["a", "b", "c"]
|
|
98
|
+
// "items[0].name" -> ["items", "0", "name"]
|
|
99
|
+
// 'data["a.b"]' -> ["data", "a.b"]
|
|
100
|
+
// "matrix[0][1]" -> ["matrix", "0", "1"]
|
|
101
|
+
// 'nested["x.y"].z' -> ["nested", "x.y", "z"]
|
|
102
|
+
// '["say \\"hi\\""]' -> ["say \"hi\""]
|
|
103
|
+
function parsePath(path) {
|
|
104
|
+
const keys = [];
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < path.length) {
|
|
107
|
+
if (path[i] === '.') {
|
|
108
|
+
// Skip dot separator
|
|
109
|
+
i++;
|
|
110
|
+
} else if (path[i] === '[') {
|
|
111
|
+
i++; // skip '['
|
|
112
|
+
if (i < path.length && (path[i] === '\'' || path[i] === '"')) {
|
|
113
|
+
// Quoted key — collect until matching unescaped quote + ']'
|
|
114
|
+
const quote = path[i];
|
|
115
|
+
i++; // skip opening quote
|
|
116
|
+
let key = '';
|
|
117
|
+
while (i < path.length && path[i] !== quote) {
|
|
118
|
+
if (path[i] === '\\' && i + 1 < path.length && path[i + 1] === quote) {
|
|
119
|
+
key += quote;
|
|
120
|
+
i += 2; // skip backslash + escaped quote
|
|
121
|
+
} else {
|
|
122
|
+
key += path[i];
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
i++; // skip closing quote
|
|
127
|
+
i++; // skip ']'
|
|
128
|
+
keys.push(key);
|
|
129
|
+
} else {
|
|
130
|
+
// Unquoted (numeric) key — collect until ']'
|
|
131
|
+
let key = '';
|
|
132
|
+
while (i < path.length && path[i] !== ']') {
|
|
133
|
+
key += path[i];
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
i++; // skip ']'
|
|
137
|
+
keys.push(key);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Bare key — collect until '.', '[', or end
|
|
141
|
+
let key = '';
|
|
142
|
+
while (i < path.length && path[i] !== '.' && path[i] !== '[') {
|
|
143
|
+
key += path[i];
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
keys.push(key);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return keys;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getNestedValue(obj, path) {
|
|
153
|
+
const keys = parsePath(path);
|
|
154
|
+
let current = obj;
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
if (current === null || current === undefined || !Object.prototype.hasOwnProperty.call(Object(current), key)) {
|
|
157
|
+
return { found: false };
|
|
158
|
+
}
|
|
159
|
+
current = current[key];
|
|
160
|
+
}
|
|
161
|
+
return { found: true, value: current };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
chai.Assertion.addMethod('jsonBody', function () {
|
|
165
|
+
const obj = this._obj;
|
|
166
|
+
const args = Array.prototype.slice.call(arguments);
|
|
167
|
+
|
|
168
|
+
if (args.length === 0) {
|
|
169
|
+
// No args: check body is valid JSON (object or array)
|
|
170
|
+
this.assert(
|
|
171
|
+
typeof obj === 'object' && obj !== null,
|
|
172
|
+
`expected ${utils.inspect(obj)} to be a JSON body (object or array)`,
|
|
173
|
+
`expected ${utils.inspect(obj)} not to be a JSON body`
|
|
174
|
+
);
|
|
175
|
+
} else if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
176
|
+
// Object arg: deep equality
|
|
177
|
+
this.assert(
|
|
178
|
+
utils.eql(obj, args[0]),
|
|
179
|
+
`expected body to deeply equal ${utils.inspect(args[0])}`,
|
|
180
|
+
`expected body to not deeply equal ${utils.inspect(args[0])}`
|
|
181
|
+
);
|
|
182
|
+
} else if (args.length === 1) {
|
|
183
|
+
// String path: check nested property exists
|
|
184
|
+
const result = getNestedValue(obj, String(args[0]));
|
|
185
|
+
this.assert(
|
|
186
|
+
result.found,
|
|
187
|
+
`expected body to have nested property '${args[0]}'`,
|
|
188
|
+
`expected body to not have nested property '${args[0]}'`
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
// Path + value: check nested property equals value
|
|
192
|
+
const result = getNestedValue(obj, String(args[0]));
|
|
193
|
+
this.assert(
|
|
194
|
+
result.found && utils.eql(result.value, args[1]),
|
|
195
|
+
`expected body to have nested property '${args[0]}' equal to ${utils.inspect(args[1])}`,
|
|
196
|
+
`expected body to not have nested property '${args[0]}' equal to ${utils.inspect(args[1])}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
46
202
|
/**
|
|
47
203
|
* Assertion operators
|
|
48
204
|
*
|
|
@@ -263,14 +419,12 @@ class AssertRuntime {
|
|
|
263
419
|
}
|
|
264
420
|
|
|
265
421
|
const certsAndProxyConfig = request?.certsAndProxyConfig;
|
|
266
|
-
const bru = new Bru(
|
|
267
|
-
this.runtime,
|
|
422
|
+
const bru = new Bru({
|
|
423
|
+
runtime: this.runtime,
|
|
268
424
|
envVariables,
|
|
269
425
|
runtimeVariables,
|
|
270
426
|
processEnvVars,
|
|
271
|
-
|
|
272
|
-
undefined, // historyLogger,
|
|
273
|
-
undefined, // setVisualizations,
|
|
427
|
+
historyLogger,
|
|
274
428
|
secretVariables,
|
|
275
429
|
collectionVariables,
|
|
276
430
|
folderVariables,
|
|
@@ -278,10 +432,10 @@ class AssertRuntime {
|
|
|
278
432
|
globalEnvironmentVariables,
|
|
279
433
|
oauth2CredentialVariables,
|
|
280
434
|
iterationDetails,
|
|
281
|
-
undefined,
|
|
282
435
|
promptVariables,
|
|
283
|
-
certsAndProxyConfig
|
|
284
|
-
|
|
436
|
+
certsAndProxyConfig,
|
|
437
|
+
requestUrl: request?.url
|
|
438
|
+
});
|
|
285
439
|
const req = new BrunoRequest(request, historyLogger);
|
|
286
440
|
const res = createResponseParser(response);
|
|
287
441
|
|