diff --git a/packages/testkit-backend/.gitignore b/packages/testkit-backend/.gitignore new file mode 100644 index 000000000..2173e574c --- /dev/null +++ b/packages/testkit-backend/.gitignore @@ -0,0 +1 @@ +public/index.js diff --git a/packages/testkit-backend/package-lock.json b/packages/testkit-backend/package-lock.json index d57669890..219d83d0c 100644 --- a/packages/testkit-backend/package-lock.json +++ b/packages/testkit-backend/package-lock.json @@ -4,11 +4,365 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@rollup/plugin-commonjs": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz", + "integrity": "sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + } + } + }, + "@rollup/plugin-inject": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-4.0.3.tgz", + "integrity": "sha512-lzMXmj0LZjd67MI+M8H9dk/oCxR0TYqYAdZ6ZOejWQLSUtud+FUPu4NCMAO8KyWWAalFo8ean7yFHCMvCNsCZw==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "estree-walker": "^2.0.1", + "magic-string": "^0.25.7" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + } + } + }, + "@rollup/plugin-node-resolve": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz", + "integrity": "sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "node-static": { + "version": "0.7.11", + "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", + "integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==", + "requires": { + "colors": ">=0.6.0", + "mime": "^1.2.9", + "optimist": ">=0.3.4" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.59.0.tgz", + "integrity": "sha512-l7s90JQhCQ6JyZjKgo7Lq1dKh2RxatOM+Jr6a9F7WbS9WgKbocyUSeLmZl8evAse7y96Ae98L2k1cBOwWD8nHw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-inject-process-env": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject-process-env/-/rollup-plugin-inject-process-env-1.3.1.tgz", + "integrity": "sha512-kKDoL30IZr0wxbNVJjq+OS92RJSKRbKV6B5eNW4q3mZTFqoWDh6lHy+mPDYuuGuERFNKXkG+AKxvYqC9+DRpKQ==", + "dev": true, + "requires": { + "magic-string": "^0.25.7" + } + }, + "rollup-plugin-polyfill-node": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.7.0.tgz", + "integrity": "sha512-iJLZDfvxcQh3SpC0OiYlZG9ik26aRM29hiC2sARbAPXYunB8rzW8GtVaWuJgiCtX1hNAo/OaYvVXfPp15fMs7g==", + "dev": true, + "requires": { + "@rollup/plugin-inject": "^4.0.0" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" } } } diff --git a/packages/testkit-backend/package.json b/packages/testkit-backend/package.json index 8d050630c..8e68ae0fd 100644 --- a/packages/testkit-backend/package.json +++ b/packages/testkit-backend/package.json @@ -4,10 +4,16 @@ "description": "Backend for the testkit test framework", "main": "src/main.js", "private": true, + "browser": { + "./src/controller/remote.js": "./src/controller/interface.js", + "./src/channel/socket.js": "./src/controller/interface.js" + }, "type": "module", "scripts": { - "start": "node -r esm src/main.js", - "clean": "rm -fr node_modules" + "build": "rollup src/index.js --config rollup.config.js", + "start": "node -r esm src/index.js", + "clean": "rm -fr node_modules public/index.js", + "prepare": "npm run build" }, "repository": { "type": "git", @@ -24,10 +30,17 @@ }, "homepage": "https://github.com/neo4j/neo4j-javascript-driver#readme", "dependencies": { + "neo4j-driver": "4.4.0-dev", "neo4j-driver-lite": "4.4.0-dev", - "neo4j-driver": "4.4.0-dev" + "node-static": "^0.7.11", + "ws": "^8.2.3" }, "devDependencies": { - "esm": "^3.2.25" + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-node-resolve": "^13.0.6", + "esm": "^3.2.25", + "rollup": "^2.59.0", + "rollup-plugin-inject-process-env": "^1.3.1", + "rollup-plugin-polyfill-node": "^0.7.0" } } diff --git a/packages/testkit-backend/public/index.html b/packages/testkit-backend/public/index.html new file mode 100644 index 000000000..275aaed53 --- /dev/null +++ b/packages/testkit-backend/public/index.html @@ -0,0 +1,14 @@ + + + + + + + + + +

Example

+ + + + \ No newline at end of file diff --git a/packages/testkit-backend/rollup.config.js b/packages/testkit-backend/rollup.config.js new file mode 100644 index 000000000..ae5295236 --- /dev/null +++ b/packages/testkit-backend/rollup.config.js @@ -0,0 +1,28 @@ +import nodeResolve from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' +import polyfillNode from 'rollup-plugin-polyfill-node' +import injectProcessEnv from 'rollup-plugin-inject-process-env' + +export default { + input: 'src/index.js', + output: { + dir: 'public', + format: 'umd', + name: 'testkitbackend' + }, + plugins: [ + nodeResolve({ + browser: true, + preferBuiltins: false + }), + commonjs(), + polyfillNode({ + }), + injectProcessEnv({ + ...process.env, + TEST_ENVIRONMENT: 'LOCAL', + CHANNEL_TYPE: 'WEBSOCKET', + BACKEND_PORT: process.env.WEB_SERVER_PORT || 8000 + }) + ] +} diff --git a/packages/testkit-backend/src/backend.js b/packages/testkit-backend/src/backend.js new file mode 100644 index 000000000..f1f8983cd --- /dev/null +++ b/packages/testkit-backend/src/backend.js @@ -0,0 +1,43 @@ +import Channel from './channel' +import Controller from './controller' + +/** + * Binds Channel and Controller + */ +export default class Backend { + /** + * + * @param {function():Controller} newController The controller factory function + * @param {function():Channel} newChannel The channel factory function + */ + constructor (newController, newChannel) { + this._channel = newChannel() + this._controller = newController() + + this._controller.on('response', ({ contextId, response }) => { + this._channel.writeResponse(contextId, response) + }) + + this._channel.on('contextOpen', ({ contextId }) => this._controller.openContext(contextId)) + this._channel.on('contextClose', ({ contextId }) => this._controller.closeContext(contextId)) + + this._channel.on('request', ({ contextId, request }) => { + try { + this._controller.handle(contextId, request) + } catch (e) { + this._channel.writeBackendError(contextId, e) + } + }) + } + + start () { + this._controller.start() + this._channel.start() + } + + stop () { + this._channel.stop() + this._controller.stop() + } + +} diff --git a/packages/testkit-backend/src/channel/index.js b/packages/testkit-backend/src/channel/index.js new file mode 100644 index 000000000..3f6350362 --- /dev/null +++ b/packages/testkit-backend/src/channel/index.js @@ -0,0 +1,17 @@ +import Channel from "./interface" +import SocketChannel from "./socket" +import WebSocketChannel from "./websocket" +/** + * Channels are the pieces of code responsible for communicating with testkit. + * + * {@link SocketChannel} is a server socket implementation meant to be used to talk directly to the + * testkit server. + * + * {@link WebSocketChannel} is a client implementation used for connection to other testkit-backend for receiving + * messages. + */ +export default Channel +export { + SocketChannel, + WebSocketChannel +} diff --git a/packages/testkit-backend/src/channel/interface.js b/packages/testkit-backend/src/channel/interface.js new file mode 100644 index 000000000..9cadfdea1 --- /dev/null +++ b/packages/testkit-backend/src/channel/interface.js @@ -0,0 +1,30 @@ +import { EventEmitter } from "events" + +/** + * Defines the interface used for receiving commands form teskit. + * + * This is a thin layer only responsible for receiving and sending messages from and to testkit. + * + * @event contextOpen This event is triggered when a new testkit client starts its work. + * @event contextClose This event is triggered when an existing client finishes it work + * @event request This event is triggered when the channel receives a request + */ +export default class Channel extends EventEmitter { + + start () { + throw Error('Not implemented') + } + + stop () { + throw Error('Not implemented') + } + + writeResponse (contextId, response) { + throw Error('Not implemented') + } + + writeBackendError (contextId, error) { + this.writeResponse(contextId, { name: 'BackendError', data: { msg: error } }) + } + +} diff --git a/packages/testkit-backend/src/channel/socket.js b/packages/testkit-backend/src/channel/socket.js new file mode 100644 index 000000000..37e440688 --- /dev/null +++ b/packages/testkit-backend/src/channel/socket.js @@ -0,0 +1,83 @@ +import Channel from './interface' +import net from 'net' +import { randomBytes } from 'crypto' +import Protocol from './testkit-protocol' + +function generateRandomId () { + return randomBytes(16).toString('hex') +} + +/** + * This is communication channel handles the direct communication with TestKit using its protocol. + * + * This implementation is meant to be run in NodeJS, it doesn't support Browser. + */ +export default class SocketChannel extends Channel { + constructor(port, newProtocol = stream => new Protocol(stream), newId = generateRandomId ) { + super() + this._newProtocol = newProtocol + this._server = null + this._newId = newId + this._clients = new Map() + this._port = port + } + + start () { + if (!this._server) { + this._server = net.createServer(this._handleConnection.bind(this)) + + this._server.listen(this._port, () => { + console.log('Listening') + }) + + this._server.on('close', () => this.emit('close')) + } + } + + _handleConnection(connection) { + console.log('Backend connected') + + const contextId = this._newId() + const protocol = this._newProtocol(connection) + + this._clients.set(contextId, { + protocol, + connection + }) + + this.emit('contextOpen', { contextId }) + protocol.on('request', request => this.emit('request', { contextId, request }) ) + protocol.on('error', e => this._writeBackendError(contextId, e)) + + connection.on('end', () => { + if (this._clients.has(contextId)) { + this._clients.get(contextId).protocol.stop() + } + this._clients.delete(contextId) + this.emit('contextClose', { contextId }) + }) + + protocol.start() + } + + writeResponse (contextId, response) { + if (this._clients.has(contextId)) { + const { protocol, connection } = this._clients.get(contextId) + const chunk = protocol.serializeResponse(response) + connection.write(chunk, 'utf8', () => {}) + } + } + + writeBackendError (contextId, error) { + this.writeResponse(contextId, { name: 'BackendError', data: { msg: error } }) + } + + stop () { + if (this._server) { + this._server.close() + this._server = null + this._clients.forEach(client => client.protocol.stop()) + this._clients = new Map() + } + } +} diff --git a/packages/testkit-backend/src/channel/testkit-protocol.js b/packages/testkit-backend/src/channel/testkit-protocol.js new file mode 100644 index 000000000..bacc41e64 --- /dev/null +++ b/packages/testkit-backend/src/channel/testkit-protocol.js @@ -0,0 +1,80 @@ +import readline from 'readline' +import EventEmitter from 'events' + +export default class Protocol extends EventEmitter { + constructor (stream) { + super() + this._inRequest = false + this._request = '' + this._stream = stream + this._readlineInterface = null + } + + start() { + if (!this._readlineInterface) { + this._readlineInterface = readline.createInterface(this._stream, null) + this._readlineInterface.on('line', this._processLine.bind(this)) + } + } + + stop () { + if (this._readlineInterface) { + this._readlineInterface.off('line', this._processLine.bind(this)) + this._readlineInterface = null + } + } + + // Called whenever a new line is received. + _processLine (line) { + switch (line) { + case '#request begin': + if (this._inRequest) { + throw new Error('Already in request') + } + this._inRequest = true + break + case '#request end': + if (!this._inRequest) { + throw new Error('End while not in request') + } + try { + this._emitRequest() + } catch (e) { + console.log('error', e) + this._emitError(e) + } + this._request = '' + this._inRequest = false + break + default: + if (!this._inRequest) { + this._emitError(new Error('Line while not in request')) + } + this._request += line + break + } + } + + _emitError(e) { + this.emit('error', e) + } + + serializeResponse (response) { + console.log('> writing response', response) + const responseStr = this._stringify(response) + return ['#response begin', responseStr, '#response end'].join('\n') + '\n' + } + + _emitRequest () { + const request = JSON.parse(this._request) + const { name, data } = request + console.log('> Got request ' + name, data) + this.emit('request', { name, data }) + } + + _stringify (val) { + return JSON.stringify(val, (_, value) => + typeof value === 'bigint' ? `${value}n` : value + ) + } +} diff --git a/packages/testkit-backend/src/channel/websocket.js b/packages/testkit-backend/src/channel/websocket.js new file mode 100644 index 000000000..4c8281dfa --- /dev/null +++ b/packages/testkit-backend/src/channel/websocket.js @@ -0,0 +1,62 @@ +import Channel from "./interface" + +/** + * This communication channel is meant to connect to other instances of the `testkit-backend` for receiving its events. + * + * This channel is only supported in browsers since it depends on WebSocket client to be available globally. + */ +export default class WebSocketChannel extends Channel { + + constructor(address) { + super() + this._adddress = address + this._ws = null + } + + start () { + if(!this._ws) { + this._ws = new WebSocket(this._adddress) + this._ws.onmessage = ({ data: message }) => { + console.log(message) + console.debug('[WebSocketChannel] Received messsage', message) + const { messageType, contextId, data } = JSON.parse(message) + + switch (messageType) { + case 'contextOpen': + case 'contextClose': + this.emit(messageType, data) + break + case 'request': + this.emit(messageType, { contextId, request: data }) + break + default: + console.error(`[WebSocketChannel] ${messageType} is not a valid message type`) + } + } + + this._ws.onclose = () => this.emit('close') + } + } + + stop () { + if(this._ws) { + this._ws.close() + this._ws = null + } + } + + writeResponse (contextId, response) { + if (this._ws) { + console.debug('[WebSocketChannel] Writing response', { contextId, response }) + return this._ws.send(this._serialize({ contextId, response })) + } + console.error('[WebSocketChannel] Websocket is not connected') + } + + _serialize (val) { + return JSON.stringify(val, (_, value) => + typeof value === 'bigint' ? `${value}n` : value + ) + } + +} diff --git a/packages/testkit-backend/src/controller/index.js b/packages/testkit-backend/src/controller/index.js new file mode 100644 index 000000000..6fc687335 --- /dev/null +++ b/packages/testkit-backend/src/controller/index.js @@ -0,0 +1,15 @@ +import Controller from './interface' +import LocalController from './local' +import RemoteController from './remote' + +/** + * Controllers are pieces of code responsible for redirecting requests to the correct handler. + * + * {@link LocalController} delegates the requests to be handled by local handlers. + * {@link RemoteController} delegates the requests to be handled by remote clients by forwarding the requests over websockets. + */ +export default Controller +export { + LocalController, + RemoteController +} diff --git a/packages/testkit-backend/src/controller/interface.js b/packages/testkit-backend/src/controller/interface.js new file mode 100644 index 000000000..3de0cb961 --- /dev/null +++ b/packages/testkit-backend/src/controller/interface.js @@ -0,0 +1,30 @@ +import { EventEmitter } from 'events' + +/** + * Controller is the unit responsible for redirecting the requests to the correct handler and managing the + * creation and destruction of the request contexts. + * + * @event response Event triggered whith response to the request handled. + */ +export default class Controller extends EventEmitter { + + start () { + + } + + stop () { + + } + + openContext (contextId) { + throw new Error('not implemented') + } + + closeContext (contextId) { + throw new Error('not implemented') + } + + handle(contextId, request) { + throw new Error('not implemented') + } +} diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js new file mode 100644 index 000000000..aed4a4a80 --- /dev/null +++ b/packages/testkit-backend/src/controller/local.js @@ -0,0 +1,70 @@ +import Context from '../context' +import Controller from './interface' + + +/** + * Local controller handles the requests locally by redirecting them to the correct request handler/service. + * + * This controller is used when testing browser and locally. + */ +export default class LocalController extends Controller { + + constructor(requestHandlers = {}) { + super() + this._requestHandlers = requestHandlers + this._contexts = new Map() + } + + openContext (contextId) { + this._contexts.set(contextId, new Context()) + } + + closeContext (contextId) { + this._contexts.delete(contextId) + } + + handle (contextId, { name, data }) { + if (!this._contexts.has(contextId)) { + throw new Error(`Context ${contextId} does not exist`) + } else if (!(name in this._requestHandlers)) { + console.log('Unknown request: ' + name) + console.log(stringify(data)) + throw new Error(`Unknown request: ${name}`) + } + + this._requestHandlers[name](this._contexts.get(contextId), data, { + writeResponse: (name, data) => this._writeResponse(contextId, name, data), + writeError: (e) => this._writeError(contextId, e), + writeBackendError: (msg) => this._writeBackendError(contextId, msg) + }) + + } + + _writeResponse (contextId, name, data) { + console.log('> writing response', name, data) + let response = { + name: name, + data: data + } + + this.emit('response', { contextId, response }) + } + + _writeBackendError (contextId, msg) { + this._writeResponse(contextId, 'BackendError', { msg: msg }) + } + + _writeError (contextId, e) { + if (e.name) { + const id = this._contexts.get(contextId).addError(e) + this._writeResponse(contextId, 'DriverError', { + id, + msg: e.message + ' (' + e.code + ')', + code: e.code + }) + return + } + this._writeBackendError(contextId, e) + } + +} diff --git a/packages/testkit-backend/src/controller/remote.js b/packages/testkit-backend/src/controller/remote.js new file mode 100644 index 000000000..fc13fedf1 --- /dev/null +++ b/packages/testkit-backend/src/controller/remote.js @@ -0,0 +1,138 @@ +import Controller from "./interface" +import { WebSocketServer } from "ws" +import { createServer } from "http" +import { Server } from "node-static" + +/** + * RemoteController handles the requests by sending them a remote client. + * + * This controller requires a client to be connected. Otherwise, it will reply with a BackendError + * to every incoming request. + * + * This controller can only be used in Node since it depends on {@link createServer}, {@link WebSocketServer} and {@link Server} + */ +export default class RemoteController extends Controller { + constructor(port) { + super() + this._staticServer = new Server('./public') + this._port = port + this._wss = null + this._ws = null + this._http = null + } + + start () { + if (!this._http) { + this._http = createServer(safeRun((request, response) => { + request.addListener('end', safeRun(() => { + this._staticServer.serve(request, response) + })).resume() + })) + + this._http.listen(this._port) + } + if (!this._wss) { + this._wss = new WebSocketServer({ server: this._http }) + this._wss.on('connection', safeRun( ws => this._handleClientConnection(ws))) + this._wss.on('error', safeRun(error => { + console.error('[RemoteController] Server error', error) + })) + } + + } + + stop () { + if (this._ws) { + this._ws.close() + this._ws = null + } + + if(this._wss) { + this._wss.close() + this._wss = null + } + + if (this._http) { + this._http.close() + this._http = null + } + } + + openContext (contextId) { + this._forwardToConnectedClient('contextOpen', contextId, { contextId }) + } + + closeContext (contextId) { + this._forwardToConnectedClient('contextClose', contextId, { contextId }) + } + + handle (contextId, request) { + this._forwardToConnectedClient('request', contextId, request) + } + + _handleClientConnection (ws) { + if (this._ws) { + console.warn('[RemoteController] Client socket already exists, new connection will be discarded') + return + } + console.log('[RemoteController] Registering client') + + this._ws = ws + this._ws.on('message', safeRun(buffer => { + const message = JSON.parse(buffer.toString()) + console.debug('[RemoteController] Received messsage', message) + const { contextId, response } = message + this._writeResponse(contextId, response) + })) + + this._ws.on('close', () => { + console.log('[RemoteController] Client connection closed') + this._ws = null + }) + + this._ws.on('error', safeRun(error => { + console.error('[RemoteController] Client connection error', error) + })) + + console.log('[RemoteController] Client registred') + } + + _forwardToConnectedClient (messageType, contextId, data) { + if (this._ws) { + const message = { + messageType, + contextId, + data + } + + console.info(`[RemoteController] Sending message`, message) + return this._ws.send(JSON.stringify(message)) + } + console.error('[RemoteController] There is no client connected') + this._writeBackendError(contextId, 'No testkit-backend client connected') + } + + _writeResponse (contextId, response) { + console.log('> writing response', response) + + this.emit('response', { contextId, response }) + } + + _writeBackendError (contextId, msg) { + this._writeResponse(contextId, { name: 'BackendError', data: { msg: msg } }) + } + +} + + +function safeRun (func) { + return function () { + const args = [...arguments] + try { + return func.apply(null, args) + } catch (error) { + console.error(`Error in function '${func.name}' called with arguments: ${JSON.stringify(args)}.`, error) + throw error + } + } +} diff --git a/packages/testkit-backend/src/index.js b/packages/testkit-backend/src/index.js new file mode 100644 index 000000000..e4f3928f7 --- /dev/null +++ b/packages/testkit-backend/src/index.js @@ -0,0 +1,46 @@ +import Backend from './backend' +import { SocketChannel, WebSocketChannel } from './channel' +import { LocalController, RemoteController } from './controller' +import * as REQUEST_HANDLERS from './request-handlers' + +/** + * Responsible for configure and run the backend server. + */ +function main( ) { + const testEnviroment = process.env.TEST_ENVIRONMENT || 'LOCAL' + const channelType = process.env.CHANNEL_TYPE || 'SOCKET' + const backendPort = process.env.BACKEND_PORT || 9876 + const webserverPort = process.env.WEB_SERVER_PORT || 8000 + + const newChannel = () => { + if ( channelType.toUpperCase() === 'WEBSOCKET' ) { + return new WebSocketChannel(new URL(`ws://localhost:${backendPort}`)) + + } + return new SocketChannel(backendPort) + } + + const newController = () => { + if ( testEnviroment.toUpperCase() === 'REMOTE' ) { + return new RemoteController(webserverPort) + } + return new LocalController(REQUEST_HANDLERS) + } + + const backend = new Backend(newController, newChannel) + + backend.start() + + if (process.on) { + // cleaning up + process.on('exit', backend.stop.bind(backend)); + + // Capturing signals + process.on('SIGINT', process.exit.bind(process)); + process.on('SIGUSR1', process.exit.bind(process)); + process.on('SIGUSR2', process.exit.bind(process)); + process.on('uncaughtException', process.exit.bind(process)); + } +} + +main() diff --git a/packages/testkit-backend/src/main.js b/packages/testkit-backend/src/main.js deleted file mode 100644 index abf0315bf..000000000 --- a/packages/testkit-backend/src/main.js +++ /dev/null @@ -1,117 +0,0 @@ -import net from 'net' -import readline from 'readline' -import Context from './context.js' -import * as requestHandlers from './request-handlers.js' - -class Backend { - constructor ({ writer }) { - console.log('Backend connected') - this._inRequest = false - this._request = '' - // Event handlers need to be bound to this instance - this.onLine = this.onLine.bind(this) - this._writer = writer - this._context = new Context() - } - - // Called whenever a new line is received. - onLine (line) { - switch (line) { - case '#request begin': - if (this._inRequest) { - throw new Error('Already in request') - } - this._inRequest = true - break - case '#request end': - if (!this._inRequest) { - throw new Error('End while not in request') - } - try { - this._handleRequest(this._request) - } catch (e) { - this._writeBackendError(e) - } - this._request = '' - this._inRequest = false - break - default: - if (!this._inRequest) { - throw new Error('Line while not in request') - } - this._request += line - break - } - } - - _handleRequest (request) { - request = JSON.parse(request) - const { name, data } = request - console.log('> Got request ' + name, data) - - if (name in requestHandlers) { - requestHandlers[name](this._context, data, { - writeResponse: this._writeResponse.bind(this), - writeError: this._writeError.bind(this), - writeBackendError: this._writeBackendError.bind(this) - }) - return - } - - this._writeBackendError('Unknown request: ' + name) - console.log('Unknown request: ' + name) - console.log(stringify(data)) - } - - _writeResponse (name, data) { - console.log('> writing response', name, data) - let response = { - name: name, - data: data - } - response = stringify(response) - const lines = ['#response begin', response, '#response end'] - this._writer(lines) - } - - _writeBackendError (msg) { - this._writeResponse('BackendError', { msg: msg }) - } - - _writeError (e) { - if (e.name) { - const id = this._context.addError(e) - this._writeResponse('DriverError', { - id, - msg: e.message + ' (' + e.code + ')', - code: e.code - }) - return - } - this._writeBackendError(e) - } -} - -function stringify (val) { - return JSON.stringify(val, (_, value) => - typeof value === 'bigint' ? `${value}n` : value - ) -} - -function server () { - const server = net.createServer(conn => { - const backend = new Backend({ - writer: lines => { - const chunk = lines.join('\n') + '\n' - conn.write(chunk, 'utf8', () => {}) - } - }) - conn.setEncoding('utf8') - const iface = readline.createInterface(conn, null) - iface.on('line', backend.onLine) - }) - server.listen(9876, () => { - console.log('Listening') - }) -} -server() diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 0b3064c7d..857bf4a5e 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -5,13 +5,16 @@ import { shouldRunTest } from './skipped-tests' import tls from 'tls' const SUPPORTED_TLS = (() => { - const min = Number(tls.DEFAULT_MIN_VERSION.split('TLSv')[1]) - const max = Number(tls.DEFAULT_MAX_VERSION.split('TLSv')[1]) - const result = []; - for (let version = min > 1 ? min : 1.1; version <= max; version = Number((version + 0.1).toFixed(1)) ) { - result.push(`Feature:TLS:${version.toFixed(1)}`) + if (tls.DEFAULT_MAX_VERSION) { + const min = Number(tls.DEFAULT_MIN_VERSION.split('TLSv')[1]) + const max = Number(tls.DEFAULT_MAX_VERSION.split('TLSv')[1]) + const result = []; + for (let version = min > 1 ? min : 1.1; version <= max; version = Number((version + 0.1).toFixed(1)) ) { + result.push(`Feature:TLS:${version.toFixed(1)}`) + } + return result; } - return result; + return []; })(); export function NewDriver (context, data, { writeResponse }) { diff --git a/testkit/backend.py b/testkit/backend.py index 949022814..304f08a57 100644 --- a/testkit/backend.py +++ b/testkit/backend.py @@ -3,8 +3,28 @@ Assumes driver and backend has been built. Responsible for starting the test backend. """ -from common import run_in_driver_repo +from common import ( + open_proccess_in_driver_repo, + is_browser, +) import os +import time if __name__ == "__main__": - run_in_driver_repo(["npm", "run", "start-testkit-backend"], env=os.environ) + print("starting backend") + if is_browser(): + print("Testkit should test browser") + os.environ["TEST_ENVIRONMENT"] = "REMOTE" + + print("npm run start-testkit-backend") + with open_proccess_in_driver_repo([ + "npm", "run", "start-testkit-backend" + ], env=os.environ) as backend: + if (is_browser()): + time.sleep(5) + print("openning firefox") + with open_proccess_in_driver_repo([ + "firefox", "-headless", "http://localhost:8000" + ]) as firefox: + firefox.wait() + backend.wait() diff --git a/testkit/common.py b/testkit/common.py index 611d84c11..a6e58adff 100644 --- a/testkit/common.py +++ b/testkit/common.py @@ -3,15 +3,22 @@ """ import subprocess import os +import sys DRIVER_REPO = "/home/driver/repo/" -def run(args, env=None, cwd=None): +def is_enabled(value): + return value.lower() in ( + "y", "yes", "t", "true", "1", "on" + ) + + +def run(args, env=None, cwd=None, check=True): subprocess.run( - args, universal_newlines=True, stderr=subprocess.STDOUT, - check=True, env=env, cwd=cwd) + args, universal_newlines=True, stderr=sys.stderr, stdout=sys.stdout, + check=check, env=env, cwd=cwd) def run_in(cwd): @@ -20,9 +27,18 @@ def _runIn(args, env=None): return _runIn -def run_in_driver_repo(args, env=None): - return run(args, env, DRIVER_REPO) +def run_in_driver_repo(args, env=None, check=True): + return run(args, env, DRIVER_REPO, check=check) + + +def open_proccess_in_driver_repo(args, env=None): + return subprocess.Popen(args, cwd=DRIVER_REPO, env=env, stderr=sys.stderr, + stdout=sys.stdout) def is_lite(): - return os.environ.get("TEST_DRIVER_LITE", "False").upper() in ["TRUE", "1"] + return is_enabled(os.environ.get("TEST_DRIVER_LITE", "false")) + + +def is_browser(): + return is_enabled(os.environ.get("TEST_DRIVER_BROWSER", "false")) diff --git a/testkit/integration.py b/testkit/integration.py index 3c1b3465e..1af24054e 100644 --- a/testkit/integration.py +++ b/testkit/integration.py @@ -1,13 +1,10 @@ import os -from common import run_in_driver_repo, is_lite - - -def should_test_browser(): - return os.environ.get("TEST_DRIVER_SKIP_BROWSER", "false").lower() not in ( - "y", "yes", "t", "true", "1", "on" - ) - +from common import ( + is_browser, + is_lite, + run_in_driver_repo, +) if __name__ == "__main__": os.environ["TEST_NEO4J_IPV6_ENABLED"] = "False" @@ -17,7 +14,7 @@ def should_test_browser(): else: ignore = "--ignore=neo4j-driver-lite" - run_in_driver_repo(["npm", "run", "test::integration", "--", ignore]) - - if should_test_browser(): + if is_browser(): run_in_driver_repo(["npm", "run", "test::browser", "--", ignore]) + else: + run_in_driver_repo(["npm", "run", "test::integration", "--", ignore]) diff --git a/testkit/stress.py b/testkit/stress.py index 77fac8dab..7481e6bae 100644 --- a/testkit/stress.py +++ b/testkit/stress.py @@ -1,5 +1,9 @@ import os -from common import run_in_driver_repo, is_lite +from common import ( + is_browser, + is_lite, + run_in_driver_repo, +) if __name__ == "__main__": @@ -7,9 +11,10 @@ os.environ["RUNNING_TIME_IN_SECONDS"] = \ os.environ.get("TEST_NEO4J_STRESS_DURATION", 0) - if is_lite(): - ignore = "--ignore=neo4j-driver" - else: - ignore = "--ignore=neo4j-driver-lite" + if not is_browser(): + if is_lite(): + ignore = "--ignore=neo4j-driver" + else: + ignore = "--ignore=neo4j-driver-lite" - run_in_driver_repo(["npm", "run", "test::stress", "--", ignore]) + run_in_driver_repo(["npm", "run", "test::stress", "--", ignore])