ng-talkback 21.0.16 → 21.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/package.json +1 -1
- package/browser-prod/package.json +1 -1
- package/lib/build-info._auto-generated_.d.ts +1 -1
- package/lib/build-info._auto-generated_.js +1 -1
- package/lib/package.json +1 -1
- package/lib-prod/{build-info._auto-generated_.ts → build-info._auto-generated_.js} +1 -2
- package/lib-prod/env/{env.angular-node-app.ts → env.angular-node-app.js} +1 -1
- package/lib-prod/env/{env.docs-webapp.ts → env.docs-webapp.js} +1 -1
- package/lib-prod/env/{env.electron-app.ts → env.electron-app.js} +1 -1
- package/lib-prod/env/{env.mobile-app.ts → env.mobile-app.js} +1 -1
- package/lib-prod/env/{env.npm-lib-and-cli-tool.ts → env.npm-lib-and-cli-tool.js} +1 -1
- package/lib-prod/env/{env.vscode-plugin.ts → env.vscode-plugin.js} +1 -1
- package/lib-prod/es6.backend.js +2 -0
- package/lib-prod/features/error-rate.backend.js +21 -0
- package/lib-prod/features/latency.backend.js +28 -0
- package/lib-prod/{index._auto-generated_.ts → index._auto-generated_.js} +1 -1
- package/lib-prod/index.js +19 -0
- package/lib-prod/logger.backend.js +21 -0
- package/lib-prod/migrations/index.js +2 -0
- package/lib-prod/migrations/{migrations_index._auto-generated_.ts → migrations_index._auto-generated_.js} +0 -2
- package/lib-prod/options.backend.js +85 -0
- package/lib-prod/package.json +1 -1
- package/lib-prod/request-handler.backend.js +137 -0
- package/lib-prod/server.backend.js +79 -0
- package/lib-prod/summary.backend.js +19 -0
- package/lib-prod/talkback-factory.backend.js +16 -0
- package/lib-prod/tape-matcher.backend.js +91 -0
- package/lib-prod/tape-renderer.backend.js +94 -0
- package/lib-prod/tape-store.backend.js +92 -0
- package/lib-prod/tape.backend.js +69 -0
- package/lib-prod/types.backend.js +1 -0
- package/lib-prod/utils/content-encoding.backend.js +36 -0
- package/lib-prod/utils/headers.backend.js +14 -0
- package/lib-prod/utils/media-type.backend.js +49 -0
- package/package.json +1 -1
- package/websql/package.json +1 -1
- package/websql-prod/package.json +1 -1
- package/lib-prod/es6.backend.ts +0 -3
- package/lib-prod/features/error-rate.backend.ts +0 -31
- package/lib-prod/features/latency.backend.ts +0 -38
- package/lib-prod/index.ts +0 -27
- package/lib-prod/lib-info.md +0 -8
- package/lib-prod/logger.backend.ts +0 -26
- package/lib-prod/migrations/index.ts +0 -2
- package/lib-prod/migrations/migrations-info.md +0 -6
- package/lib-prod/options.backend.ts +0 -140
- package/lib-prod/request-handler.backend.ts +0 -165
- package/lib-prod/server.backend.ts +0 -100
- package/lib-prod/summary.backend.ts +0 -26
- package/lib-prod/talkback-factory.backend.ts +0 -18
- package/lib-prod/tape-matcher.backend.ts +0 -113
- package/lib-prod/tape-renderer.backend.ts +0 -118
- package/lib-prod/tape-store.backend.ts +0 -111
- package/lib-prod/tape.backend.ts +0 -93
- package/lib-prod/types.backend.ts +0 -55
- package/lib-prod/utils/content-encoding.backend.ts +0 -53
- package/lib-prod/utils/headers.backend.ts +0 -14
- package/lib-prod/utils/media-type.backend.ts +0 -62
- /package/lib-prod/env/{index.ts → index.js} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Headers from './utils/headers.backend';
|
|
2
|
+
import MediaType from './utils/media-type.backend';
|
|
3
|
+
import Tape from './tape.backend';
|
|
4
|
+
import ContentEncoding from './utils/content-encoding.backend';
|
|
5
|
+
import * as bufferShim from 'buffer-shims';
|
|
6
|
+
// const bufferShim = require('buffer-shims')/
|
|
7
|
+
export default class TapeRenderer {
|
|
8
|
+
constructor(tape) {
|
|
9
|
+
this.tape = tape;
|
|
10
|
+
}
|
|
11
|
+
static async fromStore(raw, options) {
|
|
12
|
+
const req = { ...raw.req };
|
|
13
|
+
req.body = await this.prepareBody(raw, req, req.body, "req");
|
|
14
|
+
const tape = new Tape(req, options);
|
|
15
|
+
tape.meta = { ...raw.meta };
|
|
16
|
+
const baseRes = { ...raw.res };
|
|
17
|
+
const resBody = await this.prepareBody(tape, baseRes, baseRes.body, "res");
|
|
18
|
+
tape.res = {
|
|
19
|
+
...baseRes,
|
|
20
|
+
body: resBody
|
|
21
|
+
};
|
|
22
|
+
return tape;
|
|
23
|
+
}
|
|
24
|
+
static async prepareBody(tape, reqResObj, rawBody, metaPrefix) {
|
|
25
|
+
const contentEncoding = new ContentEncoding(reqResObj);
|
|
26
|
+
const isTapeUncompressed = tape.meta[metaPrefix + "Uncompressed"];
|
|
27
|
+
const isTapeHumanReadable = tape.meta[metaPrefix + "HumanReadable"];
|
|
28
|
+
const isTapeInPlainText = isTapeUncompressed || contentEncoding.isUncompressed();
|
|
29
|
+
if (isTapeHumanReadable && isTapeInPlainText) {
|
|
30
|
+
const mediaType = new MediaType(reqResObj);
|
|
31
|
+
let bufferContent = rawBody;
|
|
32
|
+
const isResAnObject = typeof (bufferContent) === "object";
|
|
33
|
+
if (isResAnObject && mediaType.isJSON()) {
|
|
34
|
+
bufferContent = JSON.stringify(bufferContent, null, 2);
|
|
35
|
+
}
|
|
36
|
+
if (Headers.read(reqResObj.headers, "content-length")) {
|
|
37
|
+
Headers.write(reqResObj.headers, "content-length", Buffer.byteLength(bufferContent).toString(), metaPrefix);
|
|
38
|
+
}
|
|
39
|
+
if (isTapeUncompressed) {
|
|
40
|
+
return await contentEncoding.compressedBody(bufferContent);
|
|
41
|
+
}
|
|
42
|
+
return bufferShim.from(bufferContent);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return bufferShim.from(rawBody, "base64");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async render() {
|
|
49
|
+
const reqBody = await this.bodyFor(this.tape.req, "req");
|
|
50
|
+
const resBody = await this.bodyFor(this.tape.res, "res");
|
|
51
|
+
return {
|
|
52
|
+
meta: this.tape.meta,
|
|
53
|
+
req: {
|
|
54
|
+
...this.tape.req,
|
|
55
|
+
body: reqBody
|
|
56
|
+
},
|
|
57
|
+
res: {
|
|
58
|
+
...this.tape.res,
|
|
59
|
+
body: resBody
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async bodyFor(reqResObj, metaPrefix) {
|
|
64
|
+
const mediaType = new MediaType(reqResObj);
|
|
65
|
+
const contentEncoding = new ContentEncoding(reqResObj);
|
|
66
|
+
const bodyLength = reqResObj.body.length;
|
|
67
|
+
const isUncompressed = contentEncoding.isUncompressed();
|
|
68
|
+
const contentEncodingSupported = isUncompressed || contentEncoding.supportedAlgorithm();
|
|
69
|
+
if (mediaType.isHumanReadable() && contentEncodingSupported && bodyLength > 0) {
|
|
70
|
+
this.tape.meta[metaPrefix + "HumanReadable"] = true;
|
|
71
|
+
let body = reqResObj.body;
|
|
72
|
+
if (!isUncompressed) {
|
|
73
|
+
this.tape.meta[metaPrefix + "Uncompressed"] = true;
|
|
74
|
+
body = await contentEncoding.uncompressedBody(body);
|
|
75
|
+
}
|
|
76
|
+
const rawBody = body.toString("utf8");
|
|
77
|
+
if (mediaType.isJSON()) {
|
|
78
|
+
try { // TODO handle this better in future not based on mediaType.isJSON
|
|
79
|
+
const parsed = JSON.parse(rawBody);
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return rawBody;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
return rawBody;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
return reqResObj.body.toString("base64");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fse, path, json5, mkdirp } from 'tnp-core/lib-prod';
|
|
2
|
+
import Tape from './tape.backend';
|
|
3
|
+
import TapeMatcher from './tape-matcher.backend';
|
|
4
|
+
import TapeRenderer from './tape-renderer.backend';
|
|
5
|
+
export default class TapeStore {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.path = path.normalize(options.path + "/");
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.tapes = [];
|
|
10
|
+
}
|
|
11
|
+
async load() {
|
|
12
|
+
mkdirp.sync(this.path);
|
|
13
|
+
await this.loadTapesAtDir(this.path);
|
|
14
|
+
console.log(`Loaded ${this.tapes.length} tapes`);
|
|
15
|
+
}
|
|
16
|
+
async loadTapesAtDir(directory) {
|
|
17
|
+
const items = fse.readdirSync(directory);
|
|
18
|
+
for (let i = 0; i < items.length; i++) {
|
|
19
|
+
const filename = items[i];
|
|
20
|
+
const fullPath = `${directory}${filename}`;
|
|
21
|
+
const stat = fse.statSync(fullPath);
|
|
22
|
+
if (!stat.isDirectory()) {
|
|
23
|
+
try {
|
|
24
|
+
const data = fse.readFileSync(fullPath, "utf8");
|
|
25
|
+
const raw = json5.parse(data);
|
|
26
|
+
const tape = await Tape.fromStore(raw, this.options);
|
|
27
|
+
tape.path = filename;
|
|
28
|
+
this.tapes.push(tape);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.log(`Error reading tape ${fullPath}`, e.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.loadTapesAtDir(fullPath + "/");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
find(newTape) {
|
|
40
|
+
const foundTape = this.tapes.find(t => {
|
|
41
|
+
this.options.logger.debug(`Comparing against tape ${t.path}`);
|
|
42
|
+
return new TapeMatcher(t, this.options).sameAs(newTape);
|
|
43
|
+
});
|
|
44
|
+
if (foundTape) {
|
|
45
|
+
foundTape.used = true;
|
|
46
|
+
this.options.logger.log(`Found matching tape for ${newTape.req.url} at ${foundTape.path}`);
|
|
47
|
+
return foundTape;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async save(tape) {
|
|
51
|
+
tape.new = true;
|
|
52
|
+
tape.used = true;
|
|
53
|
+
const tapePath = tape.path;
|
|
54
|
+
let fullFilename;
|
|
55
|
+
if (tapePath) {
|
|
56
|
+
fullFilename = path.join(this.path, tapePath);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// If the tape doesn't have a path then it's new
|
|
60
|
+
this.tapes.push(tape);
|
|
61
|
+
fullFilename = this.createTapePath(tape);
|
|
62
|
+
tape.path = path.relative(this.path, fullFilename);
|
|
63
|
+
}
|
|
64
|
+
this.options.logger.log(`Saving request ${tape.req.url} at ${tape.path}`);
|
|
65
|
+
const tapeRenderer = new TapeRenderer(tape);
|
|
66
|
+
const toSave = await tapeRenderer.render();
|
|
67
|
+
fse.writeFileSync(fullFilename, json5.stringify(toSave, null, 4));
|
|
68
|
+
}
|
|
69
|
+
currentTapeId() {
|
|
70
|
+
return this.tapes.length;
|
|
71
|
+
}
|
|
72
|
+
hasTapeBeenUsed(tapeName) {
|
|
73
|
+
return this.tapes.some(t => t.used && t.path === tapeName);
|
|
74
|
+
}
|
|
75
|
+
resetTapeUsage() {
|
|
76
|
+
return this.tapes.forEach(t => t.used = false);
|
|
77
|
+
}
|
|
78
|
+
createTapePath(tape) {
|
|
79
|
+
const currentTapeId = this.currentTapeId();
|
|
80
|
+
let tapePath = `unnamed-${currentTapeId}.json5`;
|
|
81
|
+
if (this.options.tapeNameGenerator) {
|
|
82
|
+
tapePath = this.options.tapeNameGenerator(currentTapeId, tape);
|
|
83
|
+
}
|
|
84
|
+
let result = path.normalize(path.join(this.options.path, tapePath));
|
|
85
|
+
if (!result.endsWith(".json5")) {
|
|
86
|
+
result = `${result}.json5`;
|
|
87
|
+
}
|
|
88
|
+
const dir = path.dirname(result);
|
|
89
|
+
mkdirp.sync(dir);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import MediaType from './utils/media-type.backend';
|
|
2
|
+
import TapeRenderer from './tape-renderer.backend';
|
|
3
|
+
import ContentEncoding from './utils/content-encoding.backend';
|
|
4
|
+
import * as URL from 'url';
|
|
5
|
+
import * as querystring from 'querystring';
|
|
6
|
+
// const URL = require('url')
|
|
7
|
+
// const querystring = require('querystring')
|
|
8
|
+
export class Tape {
|
|
9
|
+
constructor(req, options) {
|
|
10
|
+
this.new = false;
|
|
11
|
+
this.used = false;
|
|
12
|
+
this.req = { ...req };
|
|
13
|
+
this.options = options;
|
|
14
|
+
// This needs to happen before we erase headers since we could lose information
|
|
15
|
+
this.normalizeBody();
|
|
16
|
+
this.cleanupHeaders();
|
|
17
|
+
this.queryParamsToIgnore = this.options.ignoreQueryParams;
|
|
18
|
+
this.cleanupQueryParams();
|
|
19
|
+
this.meta = {
|
|
20
|
+
createdAt: new Date(),
|
|
21
|
+
host: this.options.host
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
static async fromStore(raw, options) {
|
|
25
|
+
return TapeRenderer.fromStore(raw, options);
|
|
26
|
+
}
|
|
27
|
+
cleanupHeaders() {
|
|
28
|
+
const newHeaders = { ...this.req.headers };
|
|
29
|
+
this.options.ignoreHeaders.forEach(h => delete newHeaders[h]);
|
|
30
|
+
this.req = {
|
|
31
|
+
...this.req,
|
|
32
|
+
headers: newHeaders
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
cleanupQueryParams() {
|
|
36
|
+
if (this.queryParamsToIgnore.length === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const url = URL.parse(this.req.url, true);
|
|
40
|
+
if (!url.search) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const query = { ...url.query };
|
|
44
|
+
this.queryParamsToIgnore.forEach(q => delete query[q]);
|
|
45
|
+
const newQuery = querystring.stringify(query);
|
|
46
|
+
if (newQuery) {
|
|
47
|
+
url.query = query;
|
|
48
|
+
url.search = "?" + newQuery;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
url.query = null;
|
|
52
|
+
url.search = null;
|
|
53
|
+
}
|
|
54
|
+
this.req.url = URL.format(url);
|
|
55
|
+
}
|
|
56
|
+
normalizeBody() {
|
|
57
|
+
const mediaType = new MediaType(this.req);
|
|
58
|
+
const contentEncoding = new ContentEncoding(this.req);
|
|
59
|
+
if (contentEncoding.isUncompressed() && mediaType.isJSON() && this.req.body.length > 0) {
|
|
60
|
+
this.req.body = Buffer.from(JSON.stringify(JSON.parse(this.req.body.toString()), null, 2));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async clone() {
|
|
64
|
+
const tapeRenderer = new TapeRenderer(this);
|
|
65
|
+
const raw = await tapeRenderer.render();
|
|
66
|
+
return Tape.fromStore(raw, this.options);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export default Tape;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
import Headers from './headers.backend';
|
|
3
|
+
const ALGORITHMS = {
|
|
4
|
+
gzip: { compress: zlib.gzipSync, uncompress: zlib.gunzipSync },
|
|
5
|
+
deflate: { compress: zlib.deflateSync, uncompress: zlib.inflateSync }
|
|
6
|
+
};
|
|
7
|
+
export default class ContentEncoding {
|
|
8
|
+
constructor(reqRes) {
|
|
9
|
+
this.reqRes = reqRes;
|
|
10
|
+
}
|
|
11
|
+
isUncompressed() {
|
|
12
|
+
const contentEncoding = this.contentEncoding();
|
|
13
|
+
return !contentEncoding || contentEncoding === "identity";
|
|
14
|
+
}
|
|
15
|
+
supportedAlgorithm() {
|
|
16
|
+
const contentEncoding = this.contentEncoding();
|
|
17
|
+
return Object.keys(ALGORITHMS).includes(contentEncoding);
|
|
18
|
+
}
|
|
19
|
+
contentEncoding() {
|
|
20
|
+
return Headers.read(this.reqRes.headers, "content-encoding");
|
|
21
|
+
}
|
|
22
|
+
async uncompressedBody(body) {
|
|
23
|
+
const contentEncoding = this.contentEncoding();
|
|
24
|
+
if (!this.supportedAlgorithm()) {
|
|
25
|
+
throw new Error(`Unsupported content-encoding ${contentEncoding}`);
|
|
26
|
+
}
|
|
27
|
+
return ALGORITHMS[contentEncoding].uncompress(body);
|
|
28
|
+
}
|
|
29
|
+
async compressedBody(body) {
|
|
30
|
+
const contentEncoding = this.contentEncoding();
|
|
31
|
+
if (!this.supportedAlgorithm()) {
|
|
32
|
+
throw new Error(`Unsupported content-encoding ${contentEncoding}`);
|
|
33
|
+
}
|
|
34
|
+
return ALGORITHMS[contentEncoding].compress(body);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default class Headers {
|
|
2
|
+
static read(headers, headerName) {
|
|
3
|
+
const value = headers[headerName];
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value[0];
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
static write(headers, headerName, value, type) {
|
|
12
|
+
headers[headerName] = type === "req" ? value : [value];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ___NS__merge } from 'tnp-core/lib-prod';
|
|
2
|
+
import Headers from './headers.backend';
|
|
3
|
+
const contentTypeParser = require('content-type');
|
|
4
|
+
const equals = (to) => (contentType) => to == contentType;
|
|
5
|
+
export const jsonTypes = [
|
|
6
|
+
equals("application/json"),
|
|
7
|
+
(contentType) => contentType.startsWith("application/") && contentType.endsWith("+json")
|
|
8
|
+
];
|
|
9
|
+
const humanReadableContentTypes = [
|
|
10
|
+
equals("application/javascript"),
|
|
11
|
+
equals("text/css"),
|
|
12
|
+
equals("text/html"),
|
|
13
|
+
equals("text/javascript"),
|
|
14
|
+
equals("text/plain"),
|
|
15
|
+
...jsonTypes
|
|
16
|
+
];
|
|
17
|
+
export default class MediaType {
|
|
18
|
+
constructor(htmlReqRes) {
|
|
19
|
+
this.htmlReqRes = htmlReqRes;
|
|
20
|
+
}
|
|
21
|
+
isHumanReadable() {
|
|
22
|
+
const contentType = this.contentType();
|
|
23
|
+
if (!contentType) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return humanReadableContentTypes.some(comparator => comparator(contentType));
|
|
27
|
+
}
|
|
28
|
+
isJSON() {
|
|
29
|
+
const contentType = this.contentType();
|
|
30
|
+
if (!contentType) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const result = jsonTypes.some(comparator => comparator('application/json'));
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
contentType() {
|
|
37
|
+
const contentTypeHeader = Headers.read(this.headers(), "content-type");
|
|
38
|
+
if (!contentTypeHeader) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const parsedContentType = contentTypeParser.parse(contentTypeHeader);
|
|
42
|
+
return parsedContentType.type;
|
|
43
|
+
}
|
|
44
|
+
headers() {
|
|
45
|
+
return ___NS__merge(this.htmlReqRes.headers, {
|
|
46
|
+
'content-type': 'application/json'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
package/websql/package.json
CHANGED
package/websql-prod/package.json
CHANGED
package/lib-prod/es6.backend.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import OptionsFactory, {Options} from '../options.backend';
|
|
2
|
-
import Tape from '../tape.backend';
|
|
3
|
-
import {Req, Res} from '../types.backend';
|
|
4
|
-
|
|
5
|
-
export default class ErrorRate {
|
|
6
|
-
private options: Options
|
|
7
|
-
|
|
8
|
-
constructor(options: Options) {
|
|
9
|
-
this.options = options
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
shouldSimulate(req:Req, tape?: Tape) {
|
|
13
|
-
const globalErrorRate = typeof (this.options.errorRate) === 'number' ? this.options.errorRate : this.options.errorRate(req)
|
|
14
|
-
|
|
15
|
-
const errorRate = tape && tape.meta.errorRate !== undefined ? tape.meta.errorRate : globalErrorRate
|
|
16
|
-
|
|
17
|
-
OptionsFactory.validateErrorRate(errorRate)
|
|
18
|
-
|
|
19
|
-
const random = Math.random() * 100
|
|
20
|
-
return random < errorRate
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
simulate(req: Req) {
|
|
24
|
-
this.options.logger.log(`Simulating error for ${req.url}`)
|
|
25
|
-
return {
|
|
26
|
-
status: 503,
|
|
27
|
-
headers: {'content-type': ['text/plain']},
|
|
28
|
-
body: Buffer.from("talkback - failure injection")
|
|
29
|
-
} as Res
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import OptionsFactory, {Options} from '../options.backend';
|
|
2
|
-
import {Req} from '../types.backend';
|
|
3
|
-
import Tape from '../tape.backend';
|
|
4
|
-
|
|
5
|
-
export default class Latency {
|
|
6
|
-
private options
|
|
7
|
-
: Options
|
|
8
|
-
|
|
9
|
-
constructor(options: Options) {
|
|
10
|
-
this.options = options
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async simulate(req: Req, tape?: Tape) {
|
|
14
|
-
const resolved = Promise.resolve()
|
|
15
|
-
|
|
16
|
-
const latencyGenerator = tape && tape.meta.latency !== undefined ? tape.meta.latency : this.options.latency
|
|
17
|
-
if (!latencyGenerator) {
|
|
18
|
-
return resolved
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
OptionsFactory.validateLatency(latencyGenerator)
|
|
22
|
-
|
|
23
|
-
let latency = 0
|
|
24
|
-
|
|
25
|
-
const type = typeof latencyGenerator
|
|
26
|
-
if (type === "number") {
|
|
27
|
-
latency = latencyGenerator as number
|
|
28
|
-
} else if (Array.isArray(latencyGenerator)) {
|
|
29
|
-
const high = latencyGenerator[1]
|
|
30
|
-
const low = latencyGenerator[0]
|
|
31
|
-
latency = Math.random() * (high - low) + low
|
|
32
|
-
} else {
|
|
33
|
-
latency = (latencyGenerator as (_: Req) => number)(req)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return new Promise(r => setTimeout(r, latency))
|
|
37
|
-
}
|
|
38
|
-
}
|
package/lib-prod/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export type Dummy = 'a';
|
|
2
|
-
let a = 2;
|
|
3
|
-
a++;
|
|
4
|
-
//#region @backend
|
|
5
|
-
export * from './options.backend';
|
|
6
|
-
export * from './tape.backend';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import TalkbackFactory from './talkback-factory.backend'
|
|
10
|
-
import Options, { DefaultOptions, FallbackMode, RecordMode } from './options.backend'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const talkbackFn = (options: Partial<Options>) => {
|
|
14
|
-
return TalkbackFactory.server(options)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
talkbackFn.Options = {
|
|
18
|
-
Default: DefaultOptions,
|
|
19
|
-
FallbackMode,
|
|
20
|
-
RecordMode
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
talkbackFn.requestHandler = (options: Partial<Options>) => TalkbackFactory.requestHandler(options)
|
|
24
|
-
|
|
25
|
-
export const talkback = talkbackFn;
|
|
26
|
-
export default talkbackFn;
|
|
27
|
-
//#endregion
|
package/lib-prod/lib-info.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export default class Logger {
|
|
2
|
-
options: any;
|
|
3
|
-
|
|
4
|
-
constructor(options: any) {
|
|
5
|
-
this.options = options;
|
|
6
|
-
if(this.options.debug) {
|
|
7
|
-
console.debug("DEBUG mode active")
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
log(message: any) {
|
|
12
|
-
if(!this.options.silent || this.options.debug) {
|
|
13
|
-
console.log(message)
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
debug(message: any) {
|
|
18
|
-
if(this.options.debug) {
|
|
19
|
-
console.debug(message)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
error(message: any) {
|
|
24
|
-
console.error(message)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import Logger from './logger.backend';
|
|
2
|
-
import Tape from './tape.backend';
|
|
3
|
-
import {Req, MatchingContext} from './types.backend';
|
|
4
|
-
|
|
5
|
-
export const RecordMode = {
|
|
6
|
-
NEW: "NEW", // If no tape matches the request, proxy it and save the response to a tape
|
|
7
|
-
OVERWRITE: "OVERWRITE", // Always proxy the request and save the response to a tape, overwriting any existing one
|
|
8
|
-
DISABLED: "DISABLED", // If a matching tape exists, return it. Otherwise, don't proxy the request and use `fallbackMode` for the response
|
|
9
|
-
ALL: [] as string[]
|
|
10
|
-
}
|
|
11
|
-
RecordMode.ALL = [RecordMode.NEW, RecordMode.OVERWRITE, RecordMode.DISABLED]
|
|
12
|
-
|
|
13
|
-
export const FallbackMode = {
|
|
14
|
-
NOT_FOUND: "NOT_FOUND",
|
|
15
|
-
PROXY: "PROXY",
|
|
16
|
-
ALL: [] as string[]
|
|
17
|
-
}
|
|
18
|
-
FallbackMode.ALL = [FallbackMode.NOT_FOUND, FallbackMode.PROXY]
|
|
19
|
-
|
|
20
|
-
export interface Options {
|
|
21
|
-
host: string,
|
|
22
|
-
port: number,
|
|
23
|
-
path: string,
|
|
24
|
-
record: string | ((req: Req) => string),
|
|
25
|
-
fallbackMode: string | ((req: Req) => string),
|
|
26
|
-
name: string,
|
|
27
|
-
tapeNameGenerator?: (tapeNumber: number, tape: Tape) => string,
|
|
28
|
-
https: {
|
|
29
|
-
enabled: boolean,
|
|
30
|
-
keyPath?: string,
|
|
31
|
-
certPath?: string
|
|
32
|
-
},
|
|
33
|
-
ignoreHeaders: string[],
|
|
34
|
-
ignoreQueryParams: string[],
|
|
35
|
-
ignoreBody: boolean,
|
|
36
|
-
|
|
37
|
-
bodyMatcher?: (tape: Tape, req: Req) => boolean,
|
|
38
|
-
urlMatcher?: (tape: Tape, req: Req) => boolean,
|
|
39
|
-
|
|
40
|
-
requestDecorator?: (req: Req, context: MatchingContext) => Req,
|
|
41
|
-
responseDecorator?: (tape: Tape, req: Req, context: MatchingContext) => Tape,
|
|
42
|
-
tapeDecorator?: (tape: Tape, context: MatchingContext) => Tape,
|
|
43
|
-
|
|
44
|
-
latency: number | number[] | ((req: Req) => number),
|
|
45
|
-
errorRate: number | ((req: Req) => number),
|
|
46
|
-
|
|
47
|
-
silent: boolean,
|
|
48
|
-
summary: boolean,
|
|
49
|
-
debug: boolean,
|
|
50
|
-
logger: Logger
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const DefaultOptions: Options = {
|
|
54
|
-
host: "",
|
|
55
|
-
port: 8080,
|
|
56
|
-
path: "./tapes/",
|
|
57
|
-
record: RecordMode.NEW,
|
|
58
|
-
fallbackMode: FallbackMode.NOT_FOUND,
|
|
59
|
-
name: "unnamed",
|
|
60
|
-
tapeNameGenerator: undefined,
|
|
61
|
-
|
|
62
|
-
https: {
|
|
63
|
-
enabled: false,
|
|
64
|
-
keyPath: undefined,
|
|
65
|
-
certPath: undefined
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
ignoreHeaders: ["content-length", "host"],
|
|
69
|
-
ignoreQueryParams: [],
|
|
70
|
-
ignoreBody: false,
|
|
71
|
-
|
|
72
|
-
bodyMatcher: undefined,
|
|
73
|
-
urlMatcher: undefined,
|
|
74
|
-
|
|
75
|
-
requestDecorator: undefined,
|
|
76
|
-
responseDecorator: undefined,
|
|
77
|
-
tapeDecorator: undefined,
|
|
78
|
-
|
|
79
|
-
latency: 0,
|
|
80
|
-
errorRate: 0,
|
|
81
|
-
|
|
82
|
-
silent: false,
|
|
83
|
-
summary: true,
|
|
84
|
-
debug: false,
|
|
85
|
-
logger: new Logger({})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export default class OptionsFactory {
|
|
89
|
-
private static logger: Logger
|
|
90
|
-
|
|
91
|
-
static prepare(usrOpts: Partial<Options> = {}) {
|
|
92
|
-
const opts: typeof DefaultOptions = {
|
|
93
|
-
...DefaultOptions,
|
|
94
|
-
name: usrOpts.host!,
|
|
95
|
-
...usrOpts,
|
|
96
|
-
ignoreHeaders: [
|
|
97
|
-
...DefaultOptions.ignoreHeaders,
|
|
98
|
-
...(usrOpts.ignoreHeaders || [])
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.logger = new Logger(opts)
|
|
103
|
-
opts.logger = this.logger
|
|
104
|
-
|
|
105
|
-
this.validateOptions(opts)
|
|
106
|
-
|
|
107
|
-
return opts
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
static validateOptions(opts: Options) {
|
|
111
|
-
this.validateRecord(opts.record)
|
|
112
|
-
this.validateFallbackMode(opts.fallbackMode)
|
|
113
|
-
this.validateLatency(opts.latency)
|
|
114
|
-
this.validateErrorRate(opts.errorRate)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
static validateRecord(record: any) {
|
|
118
|
-
if (typeof (record) === "string" && !RecordMode.ALL.includes(record)) {
|
|
119
|
-
throw `INVALID OPTION: record has an invalid value of '${record}'`
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
static validateFallbackMode(fallbackMode: any) {
|
|
124
|
-
if (typeof (fallbackMode) === "string" && !FallbackMode.ALL.includes(fallbackMode)) {
|
|
125
|
-
throw `INVALID OPTION: fallbackMode has an invalid value of '${fallbackMode}'`
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
static validateLatency(latency: any) {
|
|
130
|
-
if (Array.isArray(latency) && latency.length !== 2) {
|
|
131
|
-
throw `Invalid LATENCY option. If using a range, the array should only have 2 values [min, max]. Current=[${latency}]`
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
static validateErrorRate(errorRate: any) {
|
|
136
|
-
if (typeof (errorRate) !== "function" && (errorRate < 0 || errorRate > 100)) {
|
|
137
|
-
throw `Invalid ERRORRATE option. Value should be between 0 and 100. Current=[${errorRate}]`
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|