You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1327 lines
32 KiB
1327 lines
32 KiB
import k6 from 'k6';
|
|
import http from 'k6/http';
|
|
|
|
/* Constants */
|
|
const undef = void 0; /* eslint-disable-line no-void */
|
|
|
|
/* Symbols */
|
|
const Assign = Symbol('assign');
|
|
const Clear = Symbol('clear');
|
|
const Define = Symbol.for('define');
|
|
const Extend = Symbol.for('extend');
|
|
const Has = Symbol('has');
|
|
const Initial = Symbol.for('initial');
|
|
const Iteration = Symbol.for('iteration');
|
|
const Post = Symbol.for('post');
|
|
const Pre = Symbol.for('pre');
|
|
const Request = Symbol.for('request');
|
|
const Reset = Symbol.for('reset');
|
|
const Var = Symbol.for('variable');
|
|
const Write = Symbol('write');
|
|
|
|
/* Expressions */
|
|
const expression = {
|
|
variableStart: /^{{.+}}/,
|
|
variables: /{{(.*?)}}/g,
|
|
};
|
|
|
|
String.prototype.has = function(v) {
|
|
return this.toString().indexOf(v) !== -1;
|
|
};
|
|
|
|
/*
|
|
* Functional manipulation of frozen dicts
|
|
*
|
|
* These objects are frozen for consistency with Postman readonly semantics.
|
|
* The constructor property is forced clear to keep it as clean as possible.
|
|
*
|
|
* Ideally these objects would have no prototype but it seems impossible.
|
|
* The following methods fail:
|
|
*
|
|
* - Object.create(null) - Throws a TypeError on access of created object:
|
|
* Could not convert &{Object 0xc420ba9980 <nil> true map[] []} to primitive
|
|
* - Object.setPrototypeOf() - Not implemented.
|
|
* - Reflect.setPrototypeOf() - Reports success but prototype persists.
|
|
* - obj.__proto__ - Not available. Access returns undefined.
|
|
* - Proxy intercepting property access - Proxies not implemented.
|
|
*/
|
|
class Dict {
|
|
constructor(values = {}) {
|
|
this.constructor = undef;
|
|
Object.assign(this, values);
|
|
Object.freeze(this);
|
|
}
|
|
|
|
[Assign](updates) {
|
|
const values = Object.assign({}, this);
|
|
Object.assign(values, updates);
|
|
return new Dict(values);
|
|
}
|
|
|
|
[Clear](key) {
|
|
const values = Object.assign({}, this);
|
|
delete values[key];
|
|
return new Dict(values);
|
|
}
|
|
|
|
[Has](key) {
|
|
return this[key] !== undef;
|
|
}
|
|
|
|
[Write](key, value) {
|
|
const values = Object.assign({}, this);
|
|
values[key] = value;
|
|
return new Dict(values);
|
|
}
|
|
}
|
|
|
|
/* State */
|
|
const state = {
|
|
initialized: false,
|
|
collection: false,
|
|
environment: false,
|
|
data: false,
|
|
request: false,
|
|
post: false,
|
|
test: false,
|
|
};
|
|
const scope = {
|
|
global: new Dict(),
|
|
collection: new Dict(),
|
|
environment: new Dict(),
|
|
data: new Dict(),
|
|
local: new Dict(),
|
|
};
|
|
const data = {
|
|
file: [],
|
|
};
|
|
const store = {
|
|
request: {
|
|
data: Object.freeze({}),
|
|
headers: Object.freeze({}),
|
|
name: null,
|
|
id: null,
|
|
method: null,
|
|
url: null,
|
|
},
|
|
response: {
|
|
body: {
|
|
text: null,
|
|
json: null,
|
|
},
|
|
code: null,
|
|
cookies: {
|
|
list: [],
|
|
dict: {},
|
|
simple: {},
|
|
},
|
|
headers: {
|
|
cased: {},
|
|
uncased: {},
|
|
},
|
|
time: null,
|
|
},
|
|
test: [],
|
|
};
|
|
const definition = {
|
|
request: {},
|
|
};
|
|
const setting = {
|
|
options: {},
|
|
};
|
|
const surface = {
|
|
pm: null,
|
|
response: null,
|
|
tests: {},
|
|
};
|
|
const standard = {
|
|
require: global.require,
|
|
};
|
|
|
|
/* State validation */
|
|
function requireEnvironment() {
|
|
if (!state.environment) {
|
|
throw new Error('Missing Postman environment');
|
|
}
|
|
}
|
|
function requireRequest() {
|
|
if (!state.request) {
|
|
throw new Error('May only be used in a request scope');
|
|
}
|
|
}
|
|
function requirePost() {
|
|
if (!state.post) {
|
|
throw new Error('May only be used in a postrequest script');
|
|
}
|
|
}
|
|
|
|
/* Dynamic variables */
|
|
const dynamic = {
|
|
/* Version 4 GUID */
|
|
guid() {
|
|
return guid();
|
|
},
|
|
|
|
/* Random integer [0,1000] */
|
|
randomInt() {
|
|
return Math.floor(Math.random() * 1001);
|
|
},
|
|
|
|
/* Current time as Unix timestamp */
|
|
timestamp() {
|
|
return Date.now();
|
|
},
|
|
};
|
|
function computeDynamic(name) {
|
|
if (dynamic[name]) {
|
|
return dynamic[name]();
|
|
} else {
|
|
throw new Error(`Unsupported dynamic variable: ${name}`);
|
|
}
|
|
}
|
|
|
|
/* postman.* */
|
|
const postman = Object.freeze({
|
|
clearEnvironmentVariable(name) {
|
|
requireEnvironment();
|
|
pm.environment.unset(name);
|
|
},
|
|
clearEnvironmentVariables() {
|
|
requireEnvironment();
|
|
pm.environment.clear();
|
|
},
|
|
clearGlobalVariable(name) {
|
|
pm.globals.unset(name);
|
|
},
|
|
clearGlobalVariables() {
|
|
pm.globals.clear();
|
|
},
|
|
getEnvironmentVariable(name) {
|
|
requireEnvironment();
|
|
return pm.environment.get(name);
|
|
},
|
|
getGlobalVariable(name) {
|
|
return pm.globals.get(name);
|
|
},
|
|
getResponseCookie(name) {
|
|
requirePost();
|
|
return store.response.cookies.dict[name];
|
|
},
|
|
getResponseHeader(name) {
|
|
requirePost();
|
|
return store.response.headers.uncased[name.toLowerCase()];
|
|
},
|
|
setEnvironmentVariable(name, value) {
|
|
requireEnvironment();
|
|
pm.environment.set(name, value);
|
|
},
|
|
setGlobalVariable(name, value) {
|
|
pm.globals.set(name, value);
|
|
},
|
|
setNextRequest() {
|
|
throw new Error('postman.setNextRequest not supported');
|
|
},
|
|
|
|
/*
|
|
* Reset shim
|
|
*
|
|
* Clears to preinitialized state. For use in tests.
|
|
*/
|
|
[Reset]() {
|
|
state.initialized = false;
|
|
state.collection = false;
|
|
state.environment = false;
|
|
state.data = false;
|
|
state.scope = false;
|
|
state.post = false;
|
|
scope.global = new Dict();
|
|
setting.options = {};
|
|
scope.collection = new Dict();
|
|
scope.environment = new Dict();
|
|
scope.data = new Dict();
|
|
scope.local = new Dict();
|
|
definition.request = {};
|
|
exposePm();
|
|
},
|
|
|
|
/*
|
|
* Initialize shim
|
|
*
|
|
* Only necessary if k6 options are defined or setting initial variable
|
|
* values. May only be called once. Must be called before other methods.
|
|
* Shim behavior is undefined if called after other methods.
|
|
*/
|
|
[Initial](initial = {}) {
|
|
if (state.initialized) {
|
|
throw new Error('Scope already initialized');
|
|
}
|
|
state.initialized = true;
|
|
if ('options' in initial) {
|
|
setting.options = initial.options;
|
|
}
|
|
if ('global' in initial) {
|
|
scope.global = scope.global[Assign](initial.global);
|
|
}
|
|
if ('collection' in initial) {
|
|
state.collection = true;
|
|
scope.collection = scope.collection[Assign](initial.collection);
|
|
}
|
|
if ('environment' in initial) {
|
|
state.environment = true;
|
|
scope.environment = scope.environment[Assign](initial.environment);
|
|
}
|
|
if ('data' in initial) {
|
|
state.data = true;
|
|
data.file = [...initial.data].reverse();
|
|
}
|
|
exposePm();
|
|
},
|
|
|
|
/*
|
|
* Initialize test iteration
|
|
*
|
|
* Advances to next row of data variables. Stays on final row after end.
|
|
*/
|
|
[Iteration]() {
|
|
if (data.file.length) {
|
|
const row = data.file.pop();
|
|
scope.data = new Dict(row);
|
|
}
|
|
},
|
|
|
|
/* Outer scope prerequest scripts */
|
|
[Pre]: [],
|
|
|
|
/* Outer scope postrequest scripts */
|
|
[Post]: [],
|
|
|
|
/**
|
|
* Execute named or specified request
|
|
*
|
|
* To execute a named request pass name and optionally a 1 based index.
|
|
* To execute an ad hoc specified request pass an options object.
|
|
*
|
|
* @param {string|RequestParams}
|
|
* @param {number} [index] - Named request index.
|
|
* @return k6 response.
|
|
*/
|
|
[Request](...args) {
|
|
switch (typeof args[0]) {
|
|
case 'object':
|
|
return executeRequest(...args);
|
|
case 'string':
|
|
return namedRequest(...args);
|
|
default:
|
|
throw new Error('Invalid postman[Request] params');
|
|
}
|
|
},
|
|
|
|
/* Define request */
|
|
[Define](spec) {
|
|
if (!spec.name) {
|
|
throw new Error('Attempted to define request without name');
|
|
}
|
|
if (!definition.request[spec.name]) {
|
|
definition.request[spec.name] = [];
|
|
}
|
|
definition.request[spec.name].push(spec);
|
|
},
|
|
|
|
/* Extension point. Enables selective loading of external libraries. */
|
|
[Extend]: {
|
|
AssertionError: null,
|
|
expect() {
|
|
throw new Error('To use pm.expect import "./libs/shim/expect.js"');
|
|
},
|
|
jsonSchema() {
|
|
throw new Error('To use JSON schema import "./libs/shim/jsonSchema.js"');
|
|
},
|
|
jsonSchemaNot() {
|
|
throw new Error('To use JSON schema impot "./libs/shim/jsonSchema.js"');
|
|
},
|
|
module: {},
|
|
},
|
|
});
|
|
|
|
/* pm.* */
|
|
const pm = Object.freeze({
|
|
/* Cookies */
|
|
cookies: Object.freeze({
|
|
get(name) {
|
|
const cookie = store.response.cookies.dict[name];
|
|
return cookie ? cookie.value : null;
|
|
},
|
|
has(name) {
|
|
return name in store.response.cookies.dict;
|
|
},
|
|
toObject() {
|
|
return store.response.cookies.simple;
|
|
},
|
|
}),
|
|
|
|
/* Environment variables */
|
|
environment: Object.freeze({
|
|
clear() {
|
|
scope.environment = new Dict();
|
|
},
|
|
get(name) {
|
|
return scope.environment[name];
|
|
},
|
|
has(name) {
|
|
return scope.environment[Has](name);
|
|
},
|
|
set(name, value) {
|
|
scope.environment = scope.environment[Write](name, value);
|
|
},
|
|
toObject() {
|
|
return Object.assign({}, scope.environment);
|
|
},
|
|
unset(name) {
|
|
scope.environment = scope.environment[Clear](name);
|
|
},
|
|
}),
|
|
|
|
/* Value assertions */
|
|
expect(...args) {
|
|
return postman[Extend].expect(...args);
|
|
},
|
|
|
|
/* Global variables */
|
|
globals: Object.freeze({
|
|
clear() {
|
|
scope.global = new Dict();
|
|
},
|
|
get(name) {
|
|
return scope.global[name];
|
|
},
|
|
has(name) {
|
|
return scope.global[Has](name);
|
|
},
|
|
set(name, value) {
|
|
scope.global = scope.global[Write](name, value);
|
|
},
|
|
toObject() {
|
|
return Object.assign({}, scope.global);
|
|
},
|
|
unset(name) {
|
|
scope.global = scope.global[Clear](name);
|
|
},
|
|
}),
|
|
|
|
/* Runtime information */
|
|
info: Object.freeze({
|
|
get eventName() {
|
|
if (state.post) {
|
|
return 'test';
|
|
} else {
|
|
return 'prerequest';
|
|
}
|
|
},
|
|
get iteration() {
|
|
return global.__ITER;
|
|
},
|
|
get iterationCount() {
|
|
const value = demarshal.integer(setting.options.iterations);
|
|
if (value === undef) {
|
|
return Number.POSITIVE_INFINITY;
|
|
} else {
|
|
return value;
|
|
}
|
|
},
|
|
get requestId() {
|
|
return request.id;
|
|
},
|
|
get requestName() {
|
|
return request.name;
|
|
},
|
|
}),
|
|
|
|
/* Data variables */
|
|
iterationData: Object.freeze({
|
|
get(name) {
|
|
return scope.data[name];
|
|
},
|
|
toObject() {
|
|
return Object.assign({}, scope.data);
|
|
},
|
|
}),
|
|
|
|
/* Request information */
|
|
request: Object.freeze({
|
|
get headers() {
|
|
throw new Error('pm.request.headers not supported');
|
|
},
|
|
get url() {
|
|
throw new Error('pm.request.url not supported');
|
|
},
|
|
}),
|
|
|
|
/* Response information */
|
|
response: Object.freeze({
|
|
get code() {
|
|
return store.response.code;
|
|
},
|
|
get headers() {
|
|
throw new Error('pm.response.headers not supported');
|
|
},
|
|
json() {
|
|
parseBodyJson();
|
|
const body = store.response.body;
|
|
if (body.json === false) {
|
|
throw new Error('JSON parsing of body failed');
|
|
}
|
|
return body.json;
|
|
},
|
|
reason() {
|
|
throw new Error('pm.response.reason: Response reason unavailable in k6');
|
|
},
|
|
get responseTime() {
|
|
return store.response.time;
|
|
},
|
|
text() {
|
|
return store.response.body.text;
|
|
},
|
|
get to() {
|
|
return state.test ? to : undef;
|
|
},
|
|
}),
|
|
|
|
/* Web transactions */
|
|
sendRequest() {
|
|
throw new Error('pm.sendRequest not supported');
|
|
},
|
|
|
|
/* Test definition */
|
|
test(name, logic) {
|
|
if (state.test) {
|
|
throw new Error('Nested pm.test calls not allowed');
|
|
}
|
|
try {
|
|
enterTest();
|
|
logic();
|
|
k6.check(store.response, {
|
|
[name]: response => {
|
|
for (const test of store.test) {
|
|
if (!test(response)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const AssertionError = postman[Extend].AssertionError;
|
|
if (AssertionError && error instanceof AssertionError) {
|
|
k6.check(null, { [name]: () => false });
|
|
} else {
|
|
throw error;
|
|
}
|
|
} finally {
|
|
exitTest();
|
|
}
|
|
},
|
|
|
|
/* General variable access */
|
|
variables: Object.freeze({
|
|
get(name) {
|
|
return (
|
|
(scope.local[Has](name) && scope.local[name]) ||
|
|
(scope.data[Has](name) && scope.data[name]) ||
|
|
(scope.environment[Has](name) && scope.environment[name]) ||
|
|
(scope.collection[Has](name) && scope.collection[name]) ||
|
|
(scope.global[Has](name) && scope.global[name]) ||
|
|
undef
|
|
);
|
|
},
|
|
set(name, value) {
|
|
requireRequest();
|
|
scope.local = scope.local[Write](name, value);
|
|
},
|
|
}),
|
|
|
|
collectionVariables: Object.freeze({
|
|
clear() {
|
|
scope.collection = new Dict();
|
|
},
|
|
get(name) {
|
|
return scope.collection[name];
|
|
},
|
|
set(name, value) {
|
|
requireRequest();
|
|
scope.collection = scope.collection[Write](name, value);
|
|
},
|
|
unset(name) {
|
|
scope.collection = scope.collection[Clear](name);
|
|
},
|
|
}),
|
|
|
|
/**
|
|
* Evaluate variable
|
|
*
|
|
* For use in string interpolation. Computes dynamic variables.
|
|
* Reads simple variables from full scope hierarchy.
|
|
*
|
|
* @example
|
|
* const Var = Symbol.for('variable')
|
|
* const address = `${pm[Var]('protocol')}://${pm[Var]('domain')}/index.html`
|
|
*/
|
|
[Var](name) {
|
|
if (name[0] === '$') {
|
|
return computeDynamic(name.substring(1));
|
|
} else {
|
|
return this.variables.get(name);
|
|
}
|
|
},
|
|
});
|
|
function exposePm() {
|
|
const exposed = {
|
|
environment: pm.environment,
|
|
globals: pm.globals,
|
|
info: pm.info,
|
|
request: pm.request,
|
|
sendRequest: pm.sendRequest,
|
|
variables: pm.variables,
|
|
collectionVariables: pm.collectionVariables,
|
|
[Var]: pm[Var],
|
|
};
|
|
if (state.data) {
|
|
Object.assign(exposed, {
|
|
iterationData: pm.iterationData,
|
|
});
|
|
}
|
|
if (state.post) {
|
|
Object.assign(exposed, {
|
|
cookies: pm.cookies,
|
|
response: pm.response,
|
|
test: pm.test,
|
|
});
|
|
}
|
|
if (state.test) {
|
|
Object.assign(exposed, {
|
|
expect: pm.expect,
|
|
});
|
|
}
|
|
Object.freeze(exposed);
|
|
surface.pm = exposed;
|
|
}
|
|
|
|
/* Test assertions */
|
|
const to = Object.freeze({
|
|
/* response.to.be */
|
|
be: Object.freeze({
|
|
get accepted() {
|
|
// Response code 202
|
|
store.test.push(response => response.code === 202);
|
|
},
|
|
get badRequest() {
|
|
// Response code 400
|
|
store.test.push(response => response.code === 400);
|
|
},
|
|
get clientError() {
|
|
// Response code 4xx
|
|
store.test.push(response => ((response.code / 100) | 0) === 4);
|
|
},
|
|
get error() {
|
|
// Response code 4xx|5xx
|
|
store.test.push(response => [4, 5].includes((response.code / 100) | 0));
|
|
},
|
|
get forbidden() {
|
|
// Response code 403
|
|
store.test.push(response => response.code === 403);
|
|
},
|
|
get info() {
|
|
// Response code 1xx
|
|
store.test.push(response => ((response.code / 100) | 0) === 1);
|
|
},
|
|
get notFound() {
|
|
// Response code 404
|
|
store.test.push(response => response.code === 404);
|
|
},
|
|
get ok() {
|
|
// Response code 200
|
|
store.test.push(response => response.code === 200);
|
|
},
|
|
get rateLimited() {
|
|
// Response code 429
|
|
store.test.push(response => response.code === 429);
|
|
},
|
|
get redirection() {
|
|
// Response code 3xx
|
|
store.test.push(response => ((response.code / 100) | 0) === 3);
|
|
},
|
|
get serverError() {
|
|
// Response code 5xx
|
|
store.test.push(response => ((response.code / 100) | 0) === 5);
|
|
},
|
|
get success() {
|
|
// Response code 2xx
|
|
store.test.push(response => ((response.code / 100) | 0) === 2);
|
|
},
|
|
get unauthorized() {
|
|
// Response code 401
|
|
store.test.push(response => response.code === 401);
|
|
},
|
|
}),
|
|
|
|
/* response.to.have */
|
|
have: Object.freeze({
|
|
body(value) {
|
|
if (!value) {
|
|
store.test.push(response => !!response.body.text);
|
|
} else if (typeof value === 'string') {
|
|
store.test.push(response => response.body.text === value);
|
|
} else if (value instanceof RegExp) {
|
|
store.test.push(response => value.test(response.body.text));
|
|
} else {
|
|
throw new Error('Unrecognized argument type');
|
|
}
|
|
},
|
|
header(key, value) {
|
|
if (arguments.length > 1) {
|
|
store.test.push(
|
|
response => response.headers.uncased[key.toLowerCase()] === value
|
|
);
|
|
} else {
|
|
store.test.push(
|
|
response => key.toLowerCase() in response.headers.uncased
|
|
);
|
|
}
|
|
},
|
|
jsonBody() {
|
|
parseBodyJson();
|
|
if (arguments.length === 0) {
|
|
store.test.push(response => !!store.response.body.json);
|
|
} else if (arguments.length === 1) {
|
|
if (typeof arguments[0] === 'object') {
|
|
const [expected] = arguments;
|
|
store.test.push(response => deepEqual(response.body.json, expected));
|
|
} else if (typeof arguments[0] === 'string') {
|
|
const [path] = arguments;
|
|
const value = jsonPath(store.response.body.json, path);
|
|
store.test.push(response => !!value);
|
|
} else {
|
|
throw new Error('Unrecognized argument type');
|
|
}
|
|
} else {
|
|
const [path, expected] = arguments;
|
|
const value = jsonPath(store.response.body.json, path);
|
|
store.test.push(response => value === expected);
|
|
}
|
|
},
|
|
jsonSchema(...args) {
|
|
parseBodyJson();
|
|
postman[Extend].jsonSchema(store, ...args);
|
|
},
|
|
status(value) {
|
|
switch (typeof value) {
|
|
case 'number':
|
|
store.test.push(response => response.code === value);
|
|
break;
|
|
case 'string':
|
|
throw new Error('Response reason unavailable in k6');
|
|
default:
|
|
throw new Error(`Unrecognized argument type: ${typeof value}`);
|
|
}
|
|
},
|
|
}),
|
|
|
|
not: Object.freeze({
|
|
/* response.to.not.be */
|
|
be: Object.freeze({
|
|
get accepted() {
|
|
// Response code not 202
|
|
store.test.push(response => response.code !== 202);
|
|
},
|
|
get badRequest() {
|
|
// Response code not 400
|
|
store.test.push(response => response.code !== 400);
|
|
},
|
|
get clientError() {
|
|
// Response code not 4xx
|
|
store.test.push(response => ((response.code / 100) | 0) !== 4);
|
|
},
|
|
get error() {
|
|
// Response code not 4xx|5xx
|
|
store.test.push(
|
|
response => ![4, 5].includes((response.code / 100) | 0)
|
|
);
|
|
},
|
|
get forbidden() {
|
|
// Response code not 403
|
|
store.test.push(response => response.code !== 403);
|
|
},
|
|
get info() {
|
|
// Response code not 1xx
|
|
store.test.push(response => ((response.code / 100) | 0) !== 1);
|
|
},
|
|
get notFound() {
|
|
// Response code not 404
|
|
store.test.push(response => response.code !== 404);
|
|
},
|
|
get ok() {
|
|
// Response code not 200
|
|
store.test.push(response => response.code !== 200);
|
|
},
|
|
get rateLimited() {
|
|
// Response code not 429
|
|
store.test.push(response => response.code !== 429);
|
|
},
|
|
get redirection() {
|
|
// Response code not 3xx
|
|
store.test.push(response => ((response.code / 100) | 0) !== 3);
|
|
},
|
|
get serverError() {
|
|
// Response code not 5xx
|
|
store.test.push(response => ((response.code / 100) | 0) !== 5);
|
|
},
|
|
get success() {
|
|
// Response code not 2xx
|
|
store.test.push(response => ((response.code / 100) | 0) !== 2);
|
|
},
|
|
get unauthorized() {
|
|
// Response code not 401
|
|
store.test.push(response => response.code !== 401);
|
|
},
|
|
}),
|
|
|
|
/* response.to.not.have */
|
|
have: Object.freeze({
|
|
body(value) {
|
|
if (!value) {
|
|
store.test.push(response => !response.body.text);
|
|
} else if (typeof value === 'string') {
|
|
store.test.push(response => response.body.text !== value);
|
|
} else if (value instanceof RegExp) {
|
|
store.test.push(response => !value.test(response.body.text));
|
|
} else {
|
|
throw new Error('Unrecognized argument type');
|
|
}
|
|
},
|
|
header(key, value) {
|
|
if (arguments.length > 1) {
|
|
store.test.push(
|
|
response => response.headers.uncased[key.toLowerCase()] !== value
|
|
);
|
|
} else {
|
|
store.test.push(
|
|
response => !(key.toLowerCase() in response.headers.uncased)
|
|
);
|
|
}
|
|
},
|
|
jsonBody() {
|
|
parseBodyJson();
|
|
if (arguments.length === 0) {
|
|
store.test.push(response => !response.body.json);
|
|
} else if (arguments.length === 1) {
|
|
if (typeof arguments[0] === 'object') {
|
|
const [expected] = arguments;
|
|
store.test.push(
|
|
response => !deepEqual(response.body.json, expected)
|
|
);
|
|
} else if (typeof arguments[0] === 'string') {
|
|
const [path] = arguments;
|
|
const value = jsonPath(store.response.body.json, path);
|
|
store.test.push(response => !value);
|
|
} else {
|
|
throw new Error('Unrecognized argument type');
|
|
}
|
|
} else {
|
|
const [path, expected] = arguments;
|
|
const value = jsonPath(store.response.body.json, path);
|
|
store.test.push(response => value !== expected);
|
|
}
|
|
},
|
|
jsonSchema(...args) {
|
|
parseBodyJson();
|
|
postman[Extend].jsonSchemaNot(store, ...args);
|
|
},
|
|
status(value) {
|
|
switch (typeof value) {
|
|
case 'number':
|
|
store.test.push(response => response.code !== value);
|
|
break;
|
|
case 'string':
|
|
throw new Error('Response reason unavailable in k6');
|
|
default:
|
|
throw new Error(`Unrecognized argument type: ${typeof value}`);
|
|
}
|
|
},
|
|
}),
|
|
}),
|
|
});
|
|
|
|
/* request */
|
|
const request = Object.freeze({
|
|
get data() {
|
|
return store.request.data;
|
|
},
|
|
get headers() {
|
|
return store.request.headers;
|
|
},
|
|
get id() {
|
|
return store.request.id;
|
|
},
|
|
get method() {
|
|
return store.request.method;
|
|
},
|
|
get name() {
|
|
return store.request.name;
|
|
},
|
|
get url() {
|
|
return store.request.url;
|
|
},
|
|
});
|
|
|
|
/* responseCode */
|
|
const responseCode = Object.freeze({
|
|
get code() {
|
|
return store.response.code;
|
|
},
|
|
get detail() {
|
|
throw new Error('responseCode.detail: Response message unavailable in k6');
|
|
},
|
|
get name() {
|
|
throw new Error('responseCode.name: Response message unavailable in k6');
|
|
},
|
|
});
|
|
|
|
/* Conversion */
|
|
function xml2Json(xml) {
|
|
throw new Error('To use xml2Json import ./libs/shim/xml2Json.js');
|
|
}
|
|
function xmlToJson(xml) {
|
|
throw new Error('Deprecated function xmlToJson not supported');
|
|
}
|
|
|
|
/* Internal utility */
|
|
function parseBodyJson() {
|
|
const body = store.response.body;
|
|
if (body.text === null || body.json === false || body.json) {
|
|
// No body or parsing failed or already parsed
|
|
return;
|
|
}
|
|
try {
|
|
body.json = JSON.parse(body.text);
|
|
} catch (e) {
|
|
body.json = false;
|
|
}
|
|
}
|
|
function deepEqual(x, y) {
|
|
// From https://stackoverflow.com/a/32922084/10086414
|
|
const ok = Object.keys;
|
|
const tx = typeof x;
|
|
const ty = typeof y;
|
|
return x && y && tx === 'object' && tx === ty
|
|
? ok(x).length === ok(y).length &&
|
|
ok(x).every(key => deepEqual(x[key], y[key]))
|
|
: x === y;
|
|
}
|
|
function jsonPath(value, path) {
|
|
try {
|
|
return eval(`value.${path}`); /* eslint-disable-line no-eval */
|
|
} catch (e) {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
/* Standard library */
|
|
function require(id) {
|
|
switch (id) {
|
|
case 'lodash':
|
|
case 'cheerio':
|
|
case 'crypto-js':
|
|
if (postman[Extend].module[id]) {
|
|
return postman[Extend].module[id];
|
|
} else {
|
|
throw new Error(`To use module ${id} import ./libs/shim/${id}.js`);
|
|
}
|
|
default:
|
|
throw new Error(
|
|
`Can't load module '${id}', Node.js modules aren't supported in k6`
|
|
);
|
|
}
|
|
}
|
|
|
|
/* Template strings */
|
|
function evaluate(text) {
|
|
return text.replace(expression.variables, (match, name) => pm[Var](name));
|
|
}
|
|
|
|
/* Request execution */
|
|
/**
|
|
* @typedef {object} RequestParams
|
|
* @param {string} name - Request name.
|
|
* @param {string} [id] - Request ID. Random ID generated if not provided.
|
|
* @param {string} method - Request method.
|
|
* @param {string} address - Request address.
|
|
* @param {string|object} [data] - Data for request body.
|
|
* @param {object} [headers] - Request headers.
|
|
* @param {object} [options] - k6 request options, except headers.
|
|
* @param {function} [pre] - Prerequest logic.
|
|
* @param {function} [auth] - Authentication logic.
|
|
* @param {function} [post] - Postrequest logic. Receives k6 response.
|
|
*/
|
|
/**
|
|
* Execute request with Postman semantics
|
|
*
|
|
* Executes a request and surrounding logic with a scoped set of local
|
|
* variables. Exposes request specific API surface during execution. Exposes
|
|
* test script specific API surface during test script execution. Executes
|
|
* provided logic synchronously.
|
|
*
|
|
* @param {RequestParams} params
|
|
* @return k6 response.
|
|
*/
|
|
function executeRequest({
|
|
name,
|
|
id = guid(),
|
|
method,
|
|
address,
|
|
data,
|
|
headers,
|
|
options,
|
|
tags,
|
|
pre,
|
|
auth,
|
|
post,
|
|
}) {
|
|
try {
|
|
enterRequest(name, id, method, address, data, headers);
|
|
executePrerequest(postman[Pre], pre);
|
|
const config = makeRequestConfig(
|
|
method,
|
|
address,
|
|
data,
|
|
headers,
|
|
options,
|
|
tags
|
|
);
|
|
if (auth) {
|
|
auth(config, Var);
|
|
}
|
|
const args = makeRequestArgs(config);
|
|
const response = http.request(...args);
|
|
if (post) {
|
|
enterPost(response);
|
|
executePostrequest(postman[Post], post, response);
|
|
executeTests();
|
|
}
|
|
return response;
|
|
} finally {
|
|
exitRequest();
|
|
}
|
|
}
|
|
function namedRequest(name, index = 1) {
|
|
const requests = definition.request[name];
|
|
if (!requests) {
|
|
throw new Error(`No request with name '${name}'`);
|
|
}
|
|
if (!(requests.length >= index)) {
|
|
throw new Error(`No request with name '${name}' and index ${index}`);
|
|
}
|
|
const config = requests[index - 1];
|
|
return executeRequest(config);
|
|
}
|
|
function makeRequestConfig(method, address, data, headers, options, tags) {
|
|
const config = {};
|
|
config.method = method || null;
|
|
config.address = address ? evaluateAddress(address) : null;
|
|
if (typeof data === 'string') {
|
|
config.data = evaluate(data);
|
|
} else if (data) {
|
|
evaluateDataObject(data);
|
|
config.data = data;
|
|
} else {
|
|
config.data = {};
|
|
}
|
|
if (headers) {
|
|
for (const key of Object.keys(headers)) {
|
|
headers[key] = evaluate(headers[key]);
|
|
}
|
|
config.headers = headers;
|
|
} else {
|
|
config.headers = {};
|
|
}
|
|
|
|
config.options = options || {};
|
|
if (tags && typeof tags === 'object') {
|
|
config.options.tags = tags;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
function evaluateAddress(address) {
|
|
if (expression.variableStart.test(address)) {
|
|
const evaluated = evaluate(address);
|
|
return defaultProtocol(evaluated);
|
|
} else {
|
|
return evaluate(address);
|
|
}
|
|
}
|
|
function defaultProtocol(addressText) {
|
|
if (!postman[Extend].module.urijs) {
|
|
throw new Error(
|
|
'To use a variable at address start import "./libs/shim/urijs.js"'
|
|
);
|
|
}
|
|
const URI = postman[Extend].module.urijs;
|
|
const address = new URI(addressText);
|
|
if (!address.protocol()) {
|
|
address.protocol('http');
|
|
}
|
|
return address.toString();
|
|
}
|
|
function evaluateDataObject(data) {
|
|
for (const key of Object.keys(data)) {
|
|
if (typeof data[key] === 'string') {
|
|
data[key] = evaluate(data[key]);
|
|
}
|
|
}
|
|
}
|
|
function makeRequestArgs({ method, address, data, headers, options }) {
|
|
// Set K6 request params from setting
|
|
if (setting.options) {
|
|
options = Object.assign(setting.options, options);
|
|
}
|
|
let requestHeaders = headers;
|
|
// Merge setting headers & request headers
|
|
if (setting.options && setting.options.headers) {
|
|
requestHeaders = Object.assign(setting.options.headers, headers);
|
|
}
|
|
options.headers = requestHeaders;
|
|
return [method, address, data, options];
|
|
}
|
|
function enterRequest(name, id, method, address, data, headers) {
|
|
state.request = true;
|
|
store.request.id = id;
|
|
if (name) {
|
|
store.request.name = name;
|
|
}
|
|
if (method) {
|
|
store.request.method = method.toUpperCase();
|
|
}
|
|
if (address) {
|
|
store.request.url = address;
|
|
}
|
|
if (data && typeof data === 'object') {
|
|
store.request.data = Object.freeze(Object.assign({}, data));
|
|
}
|
|
if (headers) {
|
|
store.request.headers = Object.freeze(Object.assign({}, headers));
|
|
}
|
|
Object.assign(global, {
|
|
require,
|
|
});
|
|
}
|
|
function enterPost(response = {}) {
|
|
state.post = true;
|
|
Object.assign(store.response, {
|
|
code: response.status,
|
|
time: response.timings ? response.timings.duration : undef,
|
|
});
|
|
if (typeof response.body === 'string') {
|
|
store.response.body.text = response.body;
|
|
}
|
|
if (response.headers) {
|
|
Object.assign(store.response.headers.cased, response.headers);
|
|
for (const name of Object.keys(response.headers)) {
|
|
const value = response.headers[name];
|
|
store.response.headers.uncased[name.toLowerCase()] = value;
|
|
}
|
|
}
|
|
if (response.cookies) {
|
|
translateCookies(response.cookies);
|
|
}
|
|
exposePm();
|
|
}
|
|
function enterTest() {
|
|
state.test = true;
|
|
exposePm();
|
|
}
|
|
function exitTest() {
|
|
state.test = false;
|
|
store.test = [];
|
|
exposePm();
|
|
}
|
|
function exitRequest() {
|
|
state.request = false;
|
|
state.post = false;
|
|
state.test = false;
|
|
scope.local = new Dict();
|
|
store.request = {
|
|
data: Object.freeze({}),
|
|
headers: Object.freeze({}),
|
|
method: null,
|
|
name: null,
|
|
id: null,
|
|
url: null,
|
|
};
|
|
store.response = {
|
|
body: {
|
|
text: null,
|
|
json: null,
|
|
},
|
|
code: null,
|
|
cookies: {
|
|
list: [],
|
|
dict: {},
|
|
simple: {},
|
|
},
|
|
headers: {
|
|
cased: {},
|
|
uncased: {},
|
|
},
|
|
time: null,
|
|
};
|
|
exposePm();
|
|
surface.tests = {};
|
|
Object.assign(global, {
|
|
require: standard.require,
|
|
});
|
|
}
|
|
function translateCookies(cookies) {
|
|
for (const key of Object.keys(cookies)) {
|
|
// Postman semantics have no duplicate cookie names. Duplicates discarded.
|
|
const cookie = translateCookie(cookies[key][0]);
|
|
store.response.cookies.list.push(cookie);
|
|
store.response.cookies.dict[cookie.name] = cookie;
|
|
store.response.cookies.simple[cookie.name] = cookie.value;
|
|
}
|
|
}
|
|
function translateCookie(cookie) {
|
|
return {
|
|
domain: cookie.domain,
|
|
get hostOnly() {
|
|
throw new Error('cookie.hostOnly not supported');
|
|
},
|
|
httpOnly: cookie.httpOnly,
|
|
name: cookie.name,
|
|
path: cookie.path,
|
|
secure: cookie.secure,
|
|
get session() {
|
|
throw new Error('cookie.session not supported');
|
|
},
|
|
get storeId() {
|
|
throw new Error('cookie.storeId not supported');
|
|
},
|
|
value: cookie.value,
|
|
};
|
|
}
|
|
function executePrerequest(outer, inner) {
|
|
const scripts = [...outer];
|
|
if (inner) {
|
|
scripts.push(inner);
|
|
}
|
|
for (const script of scripts) {
|
|
script();
|
|
}
|
|
}
|
|
function executePostrequest(outer, inner, response) {
|
|
const scripts = [...outer];
|
|
if (inner) {
|
|
scripts.push(inner);
|
|
}
|
|
for (const script of scripts) {
|
|
script(response);
|
|
}
|
|
}
|
|
|
|
/* Tests */
|
|
function executeTests() {
|
|
for (const description of Object.keys(surface.tests)) {
|
|
const value = surface.tests[description];
|
|
k6.check(null, { [description]: () => value });
|
|
}
|
|
}
|
|
|
|
/* Utility */
|
|
function guid() {
|
|
// Version 4 GUID
|
|
// From https://stackoverflow.com/a/2117523/10086414
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
const r = (Math.random() * 16) | 0;
|
|
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
/* Messaging with k6 */
|
|
const demarshal = Object.freeze({
|
|
integer(encoded) {
|
|
if (encoded === undef) {
|
|
return undef;
|
|
} else {
|
|
return parseInt(JSON.stringify(encoded));
|
|
}
|
|
},
|
|
});
|
|
|
|
/* Interface */
|
|
Object.defineProperties(global, {
|
|
data: {
|
|
get() {
|
|
return state.data ? scope.data : undef;
|
|
},
|
|
},
|
|
environment: {
|
|
get() {
|
|
return scope.environment;
|
|
},
|
|
},
|
|
globals: {
|
|
get() {
|
|
return scope.global;
|
|
},
|
|
},
|
|
iteration: {
|
|
get() {
|
|
return pm.info.iteration;
|
|
},
|
|
},
|
|
pm: {
|
|
get() {
|
|
return surface.pm;
|
|
},
|
|
},
|
|
request: {
|
|
get() {
|
|
return state.request ? request : undef;
|
|
},
|
|
},
|
|
responseBody: {
|
|
get() {
|
|
return state.post ? store.response.body.text : undef;
|
|
},
|
|
},
|
|
responseCode: {
|
|
get() {
|
|
return state.post ? responseCode : undef;
|
|
},
|
|
},
|
|
responseCookies: {
|
|
get() {
|
|
return state.post ? store.response.cookies.list : undef;
|
|
},
|
|
},
|
|
responseHeaders: {
|
|
get() {
|
|
return state.post ? store.response.headers.cased : undef;
|
|
},
|
|
},
|
|
responseTime: {
|
|
get() {
|
|
return state.post ? store.response.time : undef;
|
|
},
|
|
},
|
|
tests: {
|
|
get() {
|
|
return state.post ? surface.tests : undef;
|
|
},
|
|
},
|
|
});
|
|
Object.assign(global, {
|
|
postman,
|
|
xml2Json,
|
|
xmlToJson,
|
|
});
|
|
exposePm();
|