diff --git a/package.json b/package.json index 5e1a6db33..a0cb4ac09 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,11 @@ "packages/neo4j-driver/**/*.ts": [ "npm run ts-standard::neo4j-driver", "git add" + ], + "packages/testkit-backend/deno/**/*.{ts,js}": [ + "deno fmt", + "deno lint", + "git add" ] }, "scripts": { @@ -46,6 +51,7 @@ "start-neo4j": "lerna run start-neo4j --scope neo4j-driver", "stop-neo4j": "lerna run stop-neo4j --scope neo4j-driver", "start-testkit-backend": "lerna run start --scope testkit-backend --stream", + "start-testkit-backend::deno": "lerna run start::deno --scope testkit-backend --stream", "lerna": "lerna", "prepare": "husky install", "lint-staged": "lint-staged", diff --git a/packages/bolt-connection/src/channel/deno/deno-channel.js b/packages/bolt-connection/src/channel/deno/deno-channel.js new file mode 100644 index 000000000..acb5365bb --- /dev/null +++ b/packages/bolt-connection/src/channel/deno/deno-channel.js @@ -0,0 +1,336 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable */ +import ChannelBuffer from '../channel-buf' +import { newError, internal } from 'neo4j-driver-core' +import { iterateReader } from 'https://deno.land/std@0.157.0/streams/conversion.ts'; + +const { + util: { ENCRYPTION_OFF, ENCRYPTION_ON } +} = internal + +let _CONNECTION_IDGEN = 0 +/** + * Create a new DenoChannel to be used in Deno runtime. + * @access private + */ +export default class DenoChannel { + /** + * Create new instance + * @param {ChannelConfig} config - configuration for this channel. + */ + constructor ( + config, + connect = _connect + ) { + this.id = _CONNECTION_IDGEN++ + this._conn = null + this._pending = [] + this._open = true + this._error = null + this._handleConnectionError = this._handleConnectionError.bind(this) + this._handleConnectionTerminated = this._handleConnectionTerminated.bind( + this + ) + this._connectionErrorCode = config.connectionErrorCode + this._receiveTimeout = null + this._receiveTimeoutStarted = false + this._receiveTimeoutId = null + + this._config = config + + connect(config) + .then(conn => { + this._clearConnectionTimeout() + if (!this._open) { + return conn.close() + } + this._conn = conn + + setupReader(this) + .catch(this._handleConnectionError) + + const pending = this._pending + this._pending = null + for (let i = 0; i < pending.length; i++) { + this.write(pending[i]) + } + }) + .catch(this._handleConnectionError) + + this._connectionTimeoutFired = false + this._connectionTimeoutId = this._setupConnectionTimeout() + } + + _setupConnectionTimeout () { + const timeout = this._config.connectionTimeout + if (timeout) { + return setTimeout(() => { + this._connectionTimeoutFired = true + this.close() + .then(e => this._handleConnectionError(newError(`Connection timeout after ${timeout} ms`))) + .catch(this._handleConnectionError) + }, timeout) + } + return null + } + + /** + * Remove active connection timeout, if any. + * @private + */ + _clearConnectionTimeout () { + const timeoutId = this._connectionTimeoutId + if (timeoutId !== null) { + this._connectionTimeoutFired = false + this._connectionTimeoutId = null + clearTimeout(timeoutId) + } + } + + _handleConnectionError (err) { + let msg = + 'Failed to connect to server. ' + + 'Please ensure that your database is listening on the correct host and port ' + + 'and that you have compatible encryption settings both on Neo4j server and driver. ' + + 'Note that the default encryption setting has changed in Neo4j 4.0.' + if (err.message) msg += ' Caused by: ' + err.message + this._error = newError(msg, this._connectionErrorCode) + if (this.onerror) { + this.onerror(this._error) + } + } + + _handleConnectionTerminated () { + this._open = false + this._error = newError( + 'Connection was closed by server', + this._connectionErrorCode + ) + if (this.onerror) { + this.onerror(this._error) + } + } + + + /** + * Write the passed in buffer to connection + * @param {ChannelBuffer} buffer - Buffer to write + */ + write (buffer) { + if (this._pending !== null) { + this._pending.push(buffer) + } else if (buffer instanceof ChannelBuffer) { + this._conn.write(buffer._buffer).catch(this._handleConnectionError) + } else { + throw newError("Don't know how to send buffer: " + buffer) + } + } + + /** + * Close the connection + * @returns {Promise} A promise that will be resolved after channel is closed + */ + async close () { + if (this._open) { + this._open = false + if (this._conn != null) { + await this._conn.close() + } + } + } + + /** + * Setup the receive timeout for the channel. + * + * Not supported for the browser channel. + * + * @param {number} receiveTimeout The amount of time the channel will keep without receive any data before timeout (ms) + * @returns {void} + */ + setupReceiveTimeout (receiveTimeout) { + this._receiveTimeout = receiveTimeout + } + + /** + * Stops the receive timeout for the channel. + */ + stopReceiveTimeout () { + if (this._receiveTimeout !== null && this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = false + if (this._receiveTimeoutId != null) { + clearTimeout(this._receiveTimeoutId) + } + this._receiveTimeoutId = null + } + } + + /** + * Start the receive timeout for the channel. + */ + startReceiveTimeout () { + if (this._receiveTimeout !== null && !this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = true + this._resetTimeout() + } + } + + _resetTimeout () { + if (!this._receiveTimeoutStarted) { + return + } + + if (this._receiveTimeoutId !== null) { + clearTimeout(this._receiveTimeoutId) + } + + this._receiveTimeoutId = setTimeout(() => { + this._receiveTimeoutId = null + this.stopReceiveTimeout() + this._error = newError( + `Connection lost. Server didn't respond in ${this._receiveTimeout}ms`, + this._config.connectionErrorCode + ) + + this.close() + .catch(() => { + // ignoring error during the close timeout connections since they + // not valid + }) + .finally(() => { + if (this.onerror) { + this.onerror(this._error) + } + }) + }, this._receiveTimeout) + } +} + +const TrustStrategy = { + TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: async function (config) { + if ( + !config.trustedCertificates || + config.trustedCertificates.length === 0 + ) { + throw newError( + 'You are using TRUST_CUSTOM_CA_SIGNED_CERTIFICATES as the method ' + + 'to verify trust for encrypted connections, but have not configured any ' + + 'trustedCertificates. You must specify the path to at least one trusted ' + + 'X.509 certificate for this to work. Two other alternatives is to use ' + + 'TRUST_ALL_CERTIFICATES or to disable encryption by setting encrypted="' + + ENCRYPTION_OFF + + '"' + + 'in your driver configuration.' + ); + } + + const caCerts = await Promise.all( + config.trustedCertificates.map(f => Deno.readTextFile(f)) + ) + + return Deno.connectTls({ + hostname: config.address.resolvedHost(), + port: config.address.port(), + caCerts + }) + }, + TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config) { + return Deno.connectTls({ + hostname: config.address.resolvedHost(), + port: config.address.port() + }) + }, + TRUST_ALL_CERTIFICATES: function (config) { + throw newError( + `"${config.trust}" is not available in DenoJS. ` + + 'For trust in any certificates, you should use the DenoJS flag ' + + '"--unsafely-ignore-certificate-errors". '+ + 'See, https://deno.com/blog/v1.13#disable-tls-verification' + ) + } +} + +async function _connect (config) { + if (!isEncrypted(config)) { + return Deno.connect({ + hostname: config.address.resolvedHost(), + port: config.address.port() + }) + } + const trustStrategyName = getTrustStrategyName(config) + const trustStrategy = TrustStrategy[trustStrategyName] + + if (trustStrategy != null) { + return await trustStrategy(config) + } + + throw newError( + 'Unknown trust strategy: ' + + config.trust + + '. Please use either ' + + "trust:'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' configuration " + + 'or the System CA. ' + + 'Alternatively, you can disable encryption by setting ' + + '`encrypted:"' + + ENCRYPTION_OFF + + '"`. There is no mechanism to use encryption without trust verification, ' + + 'because this incurs the overhead of encryption without improving security. If ' + + 'the driver does not verify that the peer it is connected to is really Neo4j, it ' + + 'is very easy for an attacker to bypass the encryption by pretending to be Neo4j.' + + ) +} + +function isEncrypted (config) { + const encryptionNotConfigured = + config.encrypted == null || config.encrypted === undefined + if (encryptionNotConfigured) { + // default to using encryption if trust-all-certificates is available + return false + } + return config.encrypted === true || config.encrypted === ENCRYPTION_ON +} + +function getTrustStrategyName (config) { + if (config.trust) { + return config.trust + } + return 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' +} + +async function setupReader (channel) { + try { + for await (const message of iterateReader(channel._conn)) { + channel._resetTimeout() + + if (!channel._open) { + return + } + if (channel.onmessage) { + channel.onmessage(new ChannelBuffer(message)) + } + } + channel._handleConnectionTerminated() + } catch (error) { + if (channel._open) { + channel._handleConnectionError(error) + } + } +} + diff --git a/packages/bolt-connection/src/channel/deno/deno-host-name-resolver.js b/packages/bolt-connection/src/channel/deno/deno-host-name-resolver.js new file mode 100644 index 000000000..99fb8174a --- /dev/null +++ b/packages/bolt-connection/src/channel/deno/deno-host-name-resolver.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { internal } from 'neo4j-driver-core' + +const { + resolver: { BaseHostNameResolver } +} = internal + +export default class DenoHostNameResolver extends BaseHostNameResolver { + resolve (address) { + return this._resolveToItself(address) + } +} diff --git a/packages/bolt-connection/src/channel/deno/index.js b/packages/bolt-connection/src/channel/deno/index.js new file mode 100644 index 000000000..7ed3a270b --- /dev/null +++ b/packages/bolt-connection/src/channel/deno/index.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import DenoChannel from './deno-channel' +import DenoHostNameResolver from './deno-host-name-resolver' + +/* + + This module exports a set of components to be used in deno environment. + They are not compatible with NodeJS environment. + All files import/require APIs from `node/index.js` by default. + Such imports are replaced at build time with `deno/index.js` when building a deno bundle. + + NOTE: exports in this module should have exactly the same names/structure as exports in `node/index.js`. + + */ +export const Channel = DenoChannel +export const HostNameResolver = DenoHostNameResolver diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index 45d3c9ba9..57b775332 100644 --- a/packages/core/src/transaction.ts +++ b/packages/core/src/transaction.ts @@ -109,6 +109,7 @@ class Transaction { this._lowRecordWatermak = lowRecordWatermark this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() + this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve }) diff --git a/packages/neo4j-driver-deno/README.md b/packages/neo4j-driver-deno/README.md index f7a97c58e..26eaa3cec 100644 --- a/packages/neo4j-driver-deno/README.md +++ b/packages/neo4j-driver-deno/README.md @@ -21,7 +21,7 @@ The script will: 1. Copy `neo4j-driver-lite` and the Neo4j packages it uses into a subfolder here called `lib`. 1. Rewrite all imports to Deno-compatible versions -1. Replace the "node channel" with the "browser channel" +1. Replace the "node channel" with the "deno channel" 1. Test that the resulting driver can be imported by Deno and passes type checks It is not necessary to do any other setup first; in particular, you don't need @@ -53,7 +53,42 @@ you don't have a running Neo4j instance, you can use `docker run --rm -p 7687:7687 -e NEO4J_AUTH=neo4j/driverdemo neo4j:4.4` to quickly spin one up. +## TLS + +For using system certificates, the `DENO_TLS_CA_STORE` should be set to `"system"`. +`TRUST_ALL_CERTIFICATES` should be handle by `--unsafely-ignore-certificate-errors` and not by driver configuration. See, https://deno.com/blog/v1.13#disable-tls-verification; + ## Tests -It is not yet possible to run the test suite with this driver. Contributions to -make that possible would be welcome. +Tests **require** latest [Testkit 5.0](https://github.com/neo4j-drivers/testkit/tree/5.0), Python3 and Docker. + +Testkit is needed to be cloned and configured to run against the Javascript Lite Driver. Use the following steps to configure Testkit. + +1. Clone the Testkit repository + +``` +git clone https://github.com/neo4j-drivers/testkit.git +``` + +2. Under the Testkit folder, install the requirements. + +``` +pip3 install -r requirements.txt +``` + +3. Define some enviroment variables to configure Testkit + +``` +export TEST_DRIVER_NAME=javascript +export TEST_DRIVER_REPO= +export TEST_DRIVER_DENO=1 +``` + +To run test against against some Neo4j version: + +``` +python3 main.py +``` + +More details about how to use Teskit could be found on [its repository](https://github.com/neo4j-drivers/testkit/tree/5.0) + diff --git a/packages/neo4j-driver-deno/copyright.txt b/packages/neo4j-driver-deno/copyright.txt new file mode 100644 index 000000000..9ee3abe74 --- /dev/null +++ b/packages/neo4j-driver-deno/copyright.txt @@ -0,0 +1,18 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/packages/neo4j-driver-deno/generate.ts b/packages/neo4j-driver-deno/generate.ts index a323a62a1..ed5741c62 100644 --- a/packages/neo4j-driver-deno/generate.ts +++ b/packages/neo4j-driver-deno/generate.ts @@ -130,11 +130,11 @@ async function copyAndTransform(inDir: string, outDir: string) { } // Special fix for bolt-connection/channel/index.js - // Replace the "node channel" with the "browser channel", since Deno supports browser APIs + // Replace the "node channel" with the "deno channel", since Deno supports different APIs if (inPath.endsWith("channel/index.js")) { contents = contents.replace( `export * from './node/index.js'`, - `export * from './browser/index.js'`, + `export * from './deno/index.js'`, ); } @@ -159,9 +159,10 @@ await copyAndTransform( await copyAndTransform("../neo4j-driver-lite/src", rootOutDir); // Deno convention is to use "mod.ts" not "index.ts", so let's do that at least for the main/root import: await Deno.rename(join(rootOutDir, "index.ts"), join(rootOutDir, "mod.ts")) +const copyright = await Deno.readTextFile("./copyright.txt"); await Deno.writeTextFile( join(rootOutDir, "version.ts"), - `export default "${version}" // Specified using --version when running generate.ts\n`, + [copyright, `export default "${version}" // Specified using --version when running generate.ts\n`].join('\n'), ); //////////////////////////////////////////////////////////////////////////////// diff --git a/packages/testkit-backend/deno/README.md b/packages/testkit-backend/deno/README.md new file mode 100644 index 000000000..783ab655a --- /dev/null +++ b/packages/testkit-backend/deno/README.md @@ -0,0 +1,33 @@ +# Deno-Specific Testkit Backend Implementations + +This directory contains Deno specific implementations which depend on having +`Deno` global variable available or being able to load `Deno` specific libraries +such as the `Neo4j Deno Module`. Files like `../feature/deno.js` and +`../skipped-tests/deno.js` are outside this directory since they are pure +javascript configuration files, and they don't depend on the environment. + +## Starting Backend + +### Pre-Requisites + +First, you need to build the `Neo4j Deno Module` by running +`npm run build::deno` in the repository root folder. + +### The Start Command + +For starting this backend, you should run the following command in this +directory: + +``` +deno run --allow-read --allow-write --allow-net --allow-env --allow-run index.ts +``` + +Alternatively, you could run `'npm run start::deno'` in the root package of the +`testkit-backend` or `npm run start-testkit-backend::deno` in the repository +root folder. + +## Project Structure + +- `index.ts` is responsible for configuring the backend to run. +- `channel.ts` is responsible for the communication with testkit. +- `controller.ts` is responsible for routing the request to the service. diff --git a/packages/testkit-backend/deno/channel.ts b/packages/testkit-backend/deno/channel.ts new file mode 100644 index 000000000..8fa08a4d1 --- /dev/null +++ b/packages/testkit-backend/deno/channel.ts @@ -0,0 +1,105 @@ +import { TestkitRequest, TestkitResponse } from "./domain.ts"; +import { iterateReader } from "./deps.ts"; +export interface TestkitClient { + id: number; + requests: () => AsyncIterable; + reply: (response: TestkitResponse) => Promise; +} + +export async function* listen(port: number): AsyncIterable { + let clientId = 0; + const listener = Deno.listen({ port }); + + for await (const conn of listener) { + const id = clientId++; + const { requests, reply } = setupRequestsAndReply(conn) + yield { id, requests, reply }; + } +} + +interface State { + finishedReading: boolean +} + +function setupRequestsAndReply (conn: Deno.Conn) { + const state = { finishedReading: false } + const requests = () => readRequests(conn, state); + const reply = createReply(conn, state); + + return { requests, reply } +} + +async function* readRequests(conn: Deno.Conn, state: State ): AsyncIterable { + let inRequest = false; + let requestString = ""; + try { + for await (const message of iterateReader(conn)) { + const rawTxtMessage = new TextDecoder().decode(message); + const lines = rawTxtMessage.split("\n"); + for (const line of lines) { + switch (line) { + case "#request begin": + if (inRequest) { + throw new Error("Already in request"); + } + inRequest = true; + break; + case "#request end": + if (!inRequest) { + throw new Error("Not in request"); + } + yield JSON.parse(requestString); + inRequest = false; + requestString = ""; + break; + case "": + // ignore empty lines + break; + default: + if (!inRequest) { + throw new Error("Not in request"); + } + requestString += line; + break; + } + } + } + } finally { + state.finishedReading = true + } +} + +function createReply(conn: Deno.Conn, state: State) { + const textEncoder = new TextEncoder() + return async function (response: TestkitResponse): Promise { + if (state.finishedReading) { + console.warn('Discarded response:', response) + return + } + const responseStr = JSON.stringify( + response, + (_, value) => typeof value === "bigint" ? `${value}n` : value, + ); + + const responseArr = + ["#response begin", responseStr, "#response end"].join("\n") + "\n"; + const buffer = textEncoder.encode(responseArr) + + async function writeBuffer(buff: Uint8Array, size: number) { + try { + let bytesWritten = 0; + while (bytesWritten < size) { + const writtenInSep = await conn.write(buff.slice(bytesWritten)); + bytesWritten += writtenInSep; + } + } catch (error) { + console.error(error); + } + } + await writeBuffer(buffer, buffer.byteLength); + }; +} + +export default { + listen, +}; diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts new file mode 100644 index 000000000..eafd454c5 --- /dev/null +++ b/packages/testkit-backend/deno/controller.ts @@ -0,0 +1,84 @@ +import Context from "../src/context.js"; +import { + RequestHandlerMap, + TestkitRequest, + TestkitResponse, +} from "./domain.ts"; + +interface Reply { + (response: TestkitResponse): Promise; +} + +interface Wire { + writeResponse(response: TestkitRequest): Promise; + writeError(e: Error): Promise; + writeBackendError(msg: string): Promise; +} + +function newWire(context: Context, reply: Reply): Wire { + return { + writeResponse: (response: TestkitResponse) => reply(response), + writeError: (e: Error) => { + if (e.name) { + if (e.message === "TestKit FrontendError") { + return reply({ + name: "FrontendError", + data: { + msg: "Simulating the client code throwing some error.", + }, + }); + } else { + const id = context.addError(e); + return reply({ + name: "DriverError", + data: { + id, + msg: e.message, + // @ts-ignore Code Neo4jError does have code + code: e.code, + }, + }); + } + } + const msg = e.message; + return reply({ name: "BackendError", data: { msg } }); + }, + writeBackendError: (msg: string) => + reply({ name: "BackendError", data: { msg } }), + }; +} + +export function createHandler( + // deno-lint-ignore no-explicit-any + neo4j: any, + newContext: () => Context, + requestHandlers: RequestHandlerMap, +) { + return async function ( + reply: Reply, + requests: () => AsyncIterable, + ) { + const context = newContext(); + const wire = newWire(context, (response) => { + console.log("response:", response); + return reply(response); + }); + + for await (const request of requests()) { + console.log("request:", request); + const { data, name } = request; + if (!(name in requestHandlers)) { + console.log("Unknown request: " + name); + wire.writeBackendError("Unknown request: " + name); + } + + const handleRequest = requestHandlers[name]; + + handleRequest(neo4j, context, data, wire); + } + }; +} + +export default { + createHandler, +}; diff --git a/packages/testkit-backend/deno/deps.ts b/packages/testkit-backend/deno/deps.ts new file mode 100644 index 000000000..898990acd --- /dev/null +++ b/packages/testkit-backend/deno/deps.ts @@ -0,0 +1,7 @@ +export { iterateReader } from "https://deno.land/std@0.119.0/streams/conversion.ts"; +export { default as Context } from "../src/context.js"; +export { getShouldRunTest } from "../src/skipped-tests/index.js"; +export { default as neo4j } from "../../neo4j-driver-deno/lib/mod.ts"; +export { createGetFeatures } from "../src/feature/index.js"; +export * as handlers from "../src/request-handlers.js"; +export { default as CypherNativeBinders } from "../src/cypher-native-binders.js"; diff --git a/packages/testkit-backend/deno/domain.ts b/packages/testkit-backend/deno/domain.ts new file mode 100644 index 000000000..74d5e817b --- /dev/null +++ b/packages/testkit-backend/deno/domain.ts @@ -0,0 +1,20 @@ +// deno-lint-ignore-file no-explicit-any +import Context from "../src/context.js"; + +export interface TestkitRequest { + name: string; + data?: any; +} + +export interface TestkitResponse { + name: string; + data?: any; +} + +export interface RequestHandler { + (neo4j: any, c: Context, data: any, wire: any): void; +} + +export interface RequestHandlerMap { + [key: string]: RequestHandler; +} diff --git a/packages/testkit-backend/deno/index.ts b/packages/testkit-backend/deno/index.ts new file mode 100644 index 000000000..60efbff5d --- /dev/null +++ b/packages/testkit-backend/deno/index.ts @@ -0,0 +1,37 @@ +import { + Context, + createGetFeatures, + CypherNativeBinders, + getShouldRunTest, + handlers, + neo4j, +} from "./deps.ts"; +import channel from "./channel.ts"; +import controller from "./controller.ts"; +import { RequestHandlerMap } from "./domain.ts"; + +const requestHandlers: RequestHandlerMap = handlers as RequestHandlerMap; + +addEventListener("events.errorMonitor", (event) => { + console.log("something here ========================", event); +}); + +addEventListener("unhandledrejection", (event: Event) => { + // @ts-ignore PromiseRejectionEvent has reason property + console.error("unhandledrejection", event.reason); +}); + +const binder = new CypherNativeBinders(neo4j); +const descriptor = ["async", "deno"]; +const shouldRunTest = getShouldRunTest(descriptor); +const getFeatures = createGetFeatures(descriptor); +const logLevel = Deno.env.get("LOG_LEVEL"); +const createContext = () => + new Context(shouldRunTest, getFeatures, binder, logLevel); + +const listener = channel.listen(9876); +const handle = controller.createHandler(neo4j, createContext, requestHandlers); + +for await (const client of listener) { + handle(client.reply, client.requests); +} diff --git a/packages/testkit-backend/deno/tsconfig.json b/packages/testkit-backend/deno/tsconfig.json new file mode 100644 index 000000000..b18e5bc35 --- /dev/null +++ b/packages/testkit-backend/deno/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "allowJs": true + } +} diff --git a/packages/testkit-backend/package.json b/packages/testkit-backend/package.json index c00e95f9d..4feed4551 100644 --- a/packages/testkit-backend/package.json +++ b/packages/testkit-backend/package.json @@ -12,6 +12,7 @@ "scripts": { "build": "rollup src/index.js --config rollup.config.js", "start": "node --version | grep -q v10. && node -r esm src/index.js || node --experimental-specifier-resolution=node src/index.js", + "start::deno": "deno run --allow-read --allow-write --allow-net --allow-env --allow-run deno/index.ts", "clean": "rm -fr node_modules public/index.js", "prepare": "npm run build", "node": "node" diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index fad3a5329..ab4733c0b 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -1,5 +1,5 @@ export default class Context { - constructor (shouldRunTest, getFeatures) { + constructor (shouldRunTest, getFeatures, binder, environmentLogLevel) { this._id = 0 this._drivers = {} this._sessions = {} @@ -12,6 +12,16 @@ export default class Context { this._bookmarkSupplierRequests = {} this._notifyBookmarksRequests = {} this._bookmarksManagers = {} + this._binder = binder + this._environmentLogLevel = environmentLogLevel + } + + get binder () { + return this._binder + } + + get environmentLogLevel () { + return this._environmentLogLevel } addDriver (driver) { diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index e8a838563..2d5c80af0 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -2,6 +2,7 @@ import Context from '../context' import Controller from './interface' import stringify from '../stringify' import { isFrontendError } from '../request-handlers' +import CypherNativeBinders from '../cypher-native-binders' /** * Local controller handles the requests locally by redirecting them to the correct request handler/service. @@ -9,16 +10,18 @@ import { isFrontendError } from '../request-handlers' * This controller is used when testing browser and locally. */ export default class LocalController extends Controller { - constructor (requestHandlers = {}, shouldRunTest = () => {}, getFeatures = () => []) { + constructor (requestHandlers = {}, shouldRunTest = () => {}, getFeatures = () => [], neo4j) { super() this._requestHandlers = requestHandlers this._shouldRunTest = shouldRunTest this._getFeatures = getFeatures this._contexts = new Map() + this._neo4j = neo4j + this._binder = new CypherNativeBinders(neo4j) } openContext (contextId) { - this._contexts.set(contextId, new Context(this._shouldRunTest, this._getFeatures)) + this._contexts.set(contextId, new Context(this._shouldRunTest, this._getFeatures, this._binder, process.env.LOG_LEVEL)) } closeContext (contextId) { @@ -34,7 +37,7 @@ export default class LocalController extends Controller { throw new Error(`Unknown request: ${name}`) } - return await this._requestHandlers[name](this._contexts.get(contextId), data, { + return await this._requestHandlers[name](this._neo4j, this._contexts.get(contextId), data, { writeResponse: (response) => this._writeResponse(contextId, response), writeError: (e) => this._writeError(contextId, e), writeBackendError: (msg) => this._writeBackendError(contextId, msg) diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index f7de792d9..712685b5b 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -1,252 +1,258 @@ -import neo4j from './neo4j' -export function valueResponse (name, value) { - return { name: name, data: { value: value } } -} - -export function objectToCypher (obj) { - return objectMapper(obj, nativeToCypher) -} - -export function objectMemberBitIntToNumber (obj, recursive = false) { - return objectMapper(obj, val => { - if (typeof val === 'bigint') { - return Number(val) - } else if (recursive && typeof val === 'object') { - return objectMemberBitIntToNumber(val) - } else if (recursive && Array.isArray(val)) { - return val.map(item => objectMemberBitIntToNumber(item, true)) - } - return val - }) -} - -function objectMapper (obj, mapper) { - if (obj === null || obj === undefined) { - return obj +export default function CypherNativeBinders (neo4j) { + function valueResponse (name, value) { + return { name: name, data: { value: value } } } - return Object.keys(obj).reduce((acc, key) => { - return { ...acc, [key]: mapper(obj[key]) } - }, {}) -} - -export function nativeToCypher (x) { - if (x == null) { - return valueResponse('CypherNull', null) + function objectToCypher (obj) { + return objectMapper(obj, nativeToCypher) } - switch (typeof x) { - case 'number': - if (Number.isInteger(x)) { - return valueResponse('CypherInt', x) + + function objectMemberBitIntToNumber (obj, recursive = false) { + return objectMapper(obj, val => { + if (typeof val === 'bigint') { + return Number(val) + } else if (recursive && typeof val === 'object') { + return objectMemberBitIntToNumber(val) + } else if (recursive && Array.isArray(val)) { + return val.map(item => objectMemberBitIntToNumber(item, true)) } - return valueResponse('CypherFloat', x) - case 'bigint': - return valueResponse('CypherInt', neo4j.int(x).toNumber()) - case 'string': - return valueResponse('CypherString', x) - case 'boolean': - return valueResponse('CypherBool', x) - case 'object': - return valueResponseOfObject(x) - } - console.log(`type of ${x} is ${typeof x}`) - const err = 'Unable to convert ' + x + ' to cypher type' - console.log(err) - throw Error(err) -} - -function valueResponseOfObject (x) { - if (neo4j.isInt(x)) { - // TODO: Broken!!! - return valueResponse('CypherInt', x.toInt()) - } - if (Array.isArray(x)) { - const values = x.map(nativeToCypher) - return valueResponse('CypherList', values) + return val + }) } - if (x instanceof neo4j.types.Node) { - const node = { - id: nativeToCypher(x.identity), - labels: nativeToCypher(x.labels), - props: nativeToCypher(x.properties), - elementId: nativeToCypher(x.elementId) + + function objectMapper (obj, mapper) { + if (obj === null || obj === undefined) { + return obj } - return { name: 'CypherNode', data: node } + return Object.keys(obj).reduce((acc, key) => { + return { ...acc, [key]: mapper(obj[key]) } + }, {}) } - if (x instanceof neo4j.types.Relationship) { - const relationship = { - id: nativeToCypher(x.identity), - startNodeId: nativeToCypher(x.start), - endNodeId: nativeToCypher(x.end), - type: nativeToCypher(x.type), - props: nativeToCypher(x.properties), - elementId: nativeToCypher(x.elementId), - startNodeElementId: nativeToCypher(x.startNodeElementId), - endNodeElementId: nativeToCypher(x.endNodeElementId) + + function nativeToCypher (x) { + if (x == null) { + return valueResponse('CypherNull', null) } - return { name: 'CypherRelationship', data: relationship } - } - if (x instanceof neo4j.types.Path) { - const path = x.segments - .map(segment => { - return { - nodes: [segment.end], - relationships: [segment.relationship] + switch (typeof x) { + case 'number': + if (Number.isInteger(x)) { + return valueResponse('CypherInt', x) } - }) - .reduce( - (previous, current) => { + return valueResponse('CypherFloat', x) + case 'bigint': + return valueResponse('CypherInt', neo4j.int(x).toNumber()) + case 'string': + return valueResponse('CypherString', x) + case 'boolean': + return valueResponse('CypherBool', x) + case 'object': + return valueResponseOfObject(x) + } + console.log(`type of ${x} is ${typeof x}`) + const err = 'Unable to convert ' + x + ' to cypher type' + console.log(err) + throw Error(err) + } + + function valueResponseOfObject (x) { + if (neo4j.isInt(x)) { + // TODO: Broken!!! + return valueResponse('CypherInt', x.toInt()) + } + if (Array.isArray(x)) { + const values = x.map(nativeToCypher) + return valueResponse('CypherList', values) + } + if (x instanceof neo4j.types.Node) { + const node = { + id: nativeToCypher(x.identity), + labels: nativeToCypher(x.labels), + props: nativeToCypher(x.properties), + elementId: nativeToCypher(x.elementId) + } + return { name: 'CypherNode', data: node } + } + if (x instanceof neo4j.types.Relationship) { + const relationship = { + id: nativeToCypher(x.identity), + startNodeId: nativeToCypher(x.start), + endNodeId: nativeToCypher(x.end), + type: nativeToCypher(x.type), + props: nativeToCypher(x.properties), + elementId: nativeToCypher(x.elementId), + startNodeElementId: nativeToCypher(x.startNodeElementId), + endNodeElementId: nativeToCypher(x.endNodeElementId) + } + return { name: 'CypherRelationship', data: relationship } + } + if (x instanceof neo4j.types.Path) { + const path = x.segments + .map(segment => { return { - nodes: [...previous.nodes, ...current.nodes], - relationships: [ - ...previous.relationships, - ...current.relationships - ] + nodes: [segment.end], + relationships: [segment.relationship] } - }, - { nodes: [x.start], relationships: [] } - ) - - return { - name: 'CypherPath', - data: { - nodes: nativeToCypher(path.nodes), - relationships: nativeToCypher(path.relationships) + }) + .reduce( + (previous, current) => { + return { + nodes: [...previous.nodes, ...current.nodes], + relationships: [ + ...previous.relationships, + ...current.relationships + ] + } + }, + { nodes: [x.start], relationships: [] } + ) + + return { + name: 'CypherPath', + data: { + nodes: nativeToCypher(path.nodes), + relationships: nativeToCypher(path.relationships) + } } } + + if (neo4j.isDate(x)) { + return structResponse('CypherDate', { + year: x.year, + month: x.month, + day: x.day + }) + } else if (neo4j.isDateTime(x) || neo4j.isLocalDateTime(x)) { + return structResponse('CypherDateTime', { + year: x.year, + month: x.month, + day: x.day, + hour: x.hour, + minute: x.minute, + second: x.second, + nanosecond: x.nanosecond, + utc_offset_s: x.timeZoneOffsetSeconds || (x.timeZoneId == null ? undefined : 0), + timezone_id: x.timeZoneId + }) + } else if (neo4j.isTime(x) || neo4j.isLocalTime(x)) { + return structResponse('CypherTime', { + hour: x.hour, + minute: x.minute, + second: x.second, + nanosecond: x.nanosecond, + utc_offset_s: x.timeZoneOffsetSeconds + }) + } else if (neo4j.isDuration(x)) { + return structResponse('CypherDuration', { + months: x.months, + days: x.days, + seconds: x.seconds, + nanoseconds: x.nanoseconds + }) + } + + // If all failed, interpret as a map + const map = {} + for (const [key, value] of Object.entries(x)) { + map[key] = nativeToCypher(value) + } + return valueResponse('CypherMap', map) } - - if (neo4j.isDate(x)) { - return structResponse('CypherDate', { - year: x.year, - month: x.month, - day: x.day - }) - } else if (neo4j.isDateTime(x) || neo4j.isLocalDateTime(x)) { - return structResponse('CypherDateTime', { - year: x.year, - month: x.month, - day: x.day, - hour: x.hour, - minute: x.minute, - second: x.second, - nanosecond: x.nanosecond, - utc_offset_s: x.timeZoneOffsetSeconds || (x.timeZoneId == null ? undefined : 0), - timezone_id: x.timeZoneId - }) - } else if (neo4j.isTime(x) || neo4j.isLocalTime(x)) { - return structResponse('CypherTime', { - hour: x.hour, - minute: x.minute, - second: x.second, - nanosecond: x.nanosecond, - utc_offset_s: x.timeZoneOffsetSeconds - }) - } else if (neo4j.isDuration(x)) { - return structResponse('CypherDuration', { - months: x.months, - days: x.days, - seconds: x.seconds, - nanoseconds: x.nanoseconds - }) - } - - // If all failed, interpret as a map - const map = {} - for (const [key, value] of Object.entries(x)) { - map[key] = nativeToCypher(value) - } - return valueResponse('CypherMap', map) -} - -function structResponse (name, data) { - const map = {} - for (const [key, value] of Object.entries(data)) { - map[key] = typeof value === 'bigint' || neo4j.isInt(value) - ? neo4j.int(value).toNumber() - : value + + function structResponse (name, data) { + const map = {} + for (const [key, value] of Object.entries(data)) { + map[key] = typeof value === 'bigint' || neo4j.isInt(value) + ? neo4j.int(value).toNumber() + : value + } + return { name, data: map } } - return { name, data: map } -} - -export function cypherToNative (c) { - const { - name, - data - } = c - switch (name) { - case 'CypherString': - return data.value - case 'CypherInt': - return BigInt(data.value) - case 'CypherFloat': - return data.value - case 'CypherNull': - return data.value - case 'CypherBool': - return data.value - case 'CypherList': - return data.value.map(cypherToNative) - case 'CypherDateTime': - if (data.utc_offset_s == null && data.timezone_id == null) { - return new neo4j.LocalDateTime( + + function cypherToNative (c) { + const { + name, + data + } = c + switch (name) { + case 'CypherString': + return data.value + case 'CypherInt': + return BigInt(data.value) + case 'CypherFloat': + return data.value + case 'CypherNull': + return data.value + case 'CypherBool': + return data.value + case 'CypherList': + return data.value.map(cypherToNative) + case 'CypherDateTime': + if (data.utc_offset_s == null && data.timezone_id == null) { + return new neo4j.LocalDateTime( + data.year, + data.month, + data.day, + data.hour, + data.minute, + data.second, + data.nanosecond + ) + } + return new neo4j.DateTime( data.year, data.month, data.day, data.hour, data.minute, data.second, - data.nanosecond + data.nanosecond, + data.utc_offset_s, + data.timezone_id ) - } - return new neo4j.DateTime( - data.year, - data.month, - data.day, - data.hour, - data.minute, - data.second, - data.nanosecond, - data.utc_offset_s, - data.timezone_id - ) - case 'CypherTime': - if (data.utc_offset_s == null) { - return new neo4j.LocalTime( + case 'CypherTime': + if (data.utc_offset_s == null) { + return new neo4j.LocalTime( + data.hour, + data.minute, + data.second, + data.nanosecond + ) + } + return new neo4j.Time( data.hour, data.minute, data.second, - data.nanosecond + data.nanosecond, + data.utc_offset_s ) - } - return new neo4j.Time( - data.hour, - data.minute, - data.second, - data.nanosecond, - data.utc_offset_s - ) - case 'CypherDate': - return new neo4j.Date( - data.year, - data.month, - data.day - ) - case 'CypherDuration': - return new neo4j.Duration( - data.months, - data.days, - data.seconds, - data.nanoseconds - ) - case 'CypherMap': - return Object.entries(data.value).reduce((acc, [key, val]) => { - acc[key] = cypherToNative(val) - return acc - }, {}) + case 'CypherDate': + return new neo4j.Date( + data.year, + data.month, + data.day + ) + case 'CypherDuration': + return new neo4j.Duration( + data.months, + data.days, + data.seconds, + data.nanoseconds + ) + case 'CypherMap': + return Object.entries(data.value).reduce((acc, [key, val]) => { + acc[key] = cypherToNative(val) + return acc + }, {}) + } + console.log(`Type ${name} is not handle by cypherToNative`, c) + const err = 'Unable to convert ' + c + ' to native type' + console.log(err) + throw Error(err) } - console.log(`Type ${name} is not handle by cypherToNative`, c) - const err = 'Unable to convert ' + c + ' to native type' - console.log(err) - throw Error(err) + + this.valueResponse = valueResponse + this.objectToCypher = objectToCypher + this.objectMemberBitIntToNumber = objectMemberBitIntToNumber + this.nativeToCypher = nativeToCypher + this.cypherToNative = cypherToNative } diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index d941af6e2..e7c8c5a2a 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -1,17 +1,5 @@ -import tls from 'tls' -const SUPPORTED_TLS = (() => { - 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 [] -})() + const features = [ 'Feature:Auth:Custom', @@ -37,8 +25,7 @@ const features = [ 'Optimization:EagerTransactionBegin', 'Optimization:ImplicitDefaultArguments', 'Optimization:MinimalBookmarksSet', - 'Optimization:MinimalResets', - ...SUPPORTED_TLS + 'Optimization:MinimalResets' ] export default features diff --git a/packages/testkit-backend/src/feature/deno.js b/packages/testkit-backend/src/feature/deno.js new file mode 100644 index 000000000..5b662da9a --- /dev/null +++ b/packages/testkit-backend/src/feature/deno.js @@ -0,0 +1,7 @@ + +const features = [ + 'Feature:TLS:1.2', + 'Feature:TLS:1.3' +] + +export default features diff --git a/packages/testkit-backend/src/feature/index.js b/packages/testkit-backend/src/feature/index.js index 1e95fa4ec..4d9639af1 100644 --- a/packages/testkit-backend/src/feature/index.js +++ b/packages/testkit-backend/src/feature/index.js @@ -1,17 +1,19 @@ -import commonFeatures from './common' -import rxFeatures from './rx' -import asyncFeatures from './async' +import commonFeatures from './common.js' +import rxFeatures from './rx.js' +import asyncFeatures from './async.js' +import denoFeatures from './deno.js' const featuresByContext = new Map([ ['async', asyncFeatures], - ['rx', rxFeatures] + ['rx', rxFeatures], + ['deno', denoFeatures] ]) -export function createGetFeatures (contexts) { +export function createGetFeatures (contexts, extraFeatures = []) { const features = contexts .filter(context => featuresByContext.has(context)) .map(context => featuresByContext.get(context)) - .reduce((previous, current) => [...previous, ...current], commonFeatures) + .reduce((previous, current) => [...previous, ...current], [...commonFeatures, ...extraFeatures]) return () => features } diff --git a/packages/testkit-backend/src/index.js b/packages/testkit-backend/src/index.js index c9a4b16eb..feaae6274 100644 --- a/packages/testkit-backend/src/index.js +++ b/packages/testkit-backend/src/index.js @@ -1,4 +1,6 @@ import Backend from './backend' +import tls from 'tls' +import neo4j from './neo4j.js' import { SocketChannel, WebSocketChannel } from './channel' import { LocalController, RemoteController } from './controller' import { getShouldRunTest } from './skipped-tests' @@ -6,6 +8,19 @@ import { createGetFeatures } from './feature' import * as REQUEST_HANDLERS from './request-handlers.js' import * as RX_REQUEST_HANDLERS from './request-handlers-rx.js' +const SUPPORTED_TLS = (() => { + 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 [] +})() + /** * Responsible for configure and run the backend server. */ @@ -21,7 +36,7 @@ function main () { .split(',').map(s => s.trim().toLowerCase()) const shouldRunTest = getShouldRunTest([...driverDescriptorList, sessionTypeDescriptor]) - const getFeatures = createGetFeatures([sessionTypeDescriptor]) + const getFeatures = createGetFeatures([sessionTypeDescriptor], SUPPORTED_TLS) const newChannel = () => { if (channelType.toUpperCase() === 'WEBSOCKET') { @@ -34,7 +49,7 @@ function main () { if (testEnviroment.toUpperCase() === 'REMOTE') { return new RemoteController(webserverPort) } - return new LocalController(getRequestHandlers(sessionType), shouldRunTest, getFeatures) + return new LocalController(getRequestHandlers(sessionType), shouldRunTest, getFeatures, neo4j) } const backend = new Backend(newController, newChannel) diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 088b89aff..8a882dfa4 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -1,8 +1,4 @@ import * as responses from './responses.js' -import neo4j from './neo4j.js' -import { - cypherToNative -} from './cypher-native-binders.js' import { from } from 'rxjs' // Handlers which didn't change depending @@ -28,7 +24,7 @@ export { StartSubTest } from './request-handlers.js' -export function NewSession (context, data, wire) { +export function NewSession (neo4j, context, data, wire) { let { driverId, accessMode, bookmarks, database, fetchSize, impersonatedUser, bookmarkManagerId } = data switch (accessMode) { case 'r': @@ -62,7 +58,7 @@ export function NewSession (context, data, wire) { wire.writeResponse(responses.Session({ id })) } -export function SessionClose (context, data, wire) { +export function SessionClose (_, context, data, wire) { const { sessionId } = data const session = context.getSession(sessionId) return session @@ -72,12 +68,12 @@ export function SessionClose (context, data, wire) { .catch(err => wire.writeError(err)) } -export function SessionRun (context, data, wire) { +export function SessionRun (_, context, data, wire) { const { sessionId, cypher, params, txMeta: metadata, timeout } = data const session = context.getSession(sessionId) if (params) { for (const [key, value] of Object.entries(params)) { - params[key] = cypherToNative(value) + params[key] = context.binder.cypherToNative(value) } } @@ -104,18 +100,18 @@ export function SessionRun (context, data, wire) { }) } -export function ResultConsume (context, data, wire) { +export function ResultConsume (_, context, data, wire) { const { resultId } = data const result = context.getResult(resultId) return result.consume() .toPromise() .then(summary => { - wire.writeResponse(responses.Summary({ summary })) + wire.writeResponse(responses.Summary({ summary }, { binder: context.binder })) }).catch(e => wire.writeError(e)) } -export function SessionBeginTransaction (context, data, wire) { +export function SessionBeginTransaction (_, context, data, wire) { const { sessionId, txMeta: metadata, timeout } = data const session = context.getSession(sessionId) @@ -135,12 +131,12 @@ export function SessionBeginTransaction (context, data, wire) { } } -export function TransactionRun (context, data, wire) { +export function TransactionRun (_, context, data, wire) { const { txId, cypher, params } = data const tx = context.getTx(txId) if (params) { for (const [key, value] of Object.entries(params)) { - params[key] = cypherToNative(value) + params[key] = context.binder.cypherToNative(value) } } @@ -158,7 +154,7 @@ export function TransactionRun (context, data, wire) { }) } -export function TransactionRollback (context, data, wire) { +export function TransactionRollback (_, context, data, wire) { const { txId: id } = data const { tx } = context.getTx(id) return tx.rollback() @@ -170,7 +166,7 @@ export function TransactionRollback (context, data, wire) { }) } -export function TransactionCommit (context, data, wire) { +export function TransactionCommit (_, context, data, wire) { const { txId: id } = data const { tx } = context.getTx(id) return tx.commit() @@ -182,7 +178,7 @@ export function TransactionCommit (context, data, wire) { }) } -export function TransactionClose (context, data, wire) { +export function TransactionClose (_, context, data, wire) { const { txId: id } = data const { tx } = context.getTx(id) return tx.close() @@ -191,7 +187,7 @@ export function TransactionClose (context, data, wire) { .catch(e => wire.writeError(e)) } -export function SessionReadTransaction (context, data, wire) { +export function SessionReadTransaction (_, context, data, wire) { const { sessionId, txMeta: metadata } = data const session = context.getSession(sessionId) @@ -210,7 +206,7 @@ export function SessionReadTransaction (context, data, wire) { } } -export function SessionWriteTransaction (context, data, wire) { +export function SessionWriteTransaction (_, context, data, wire) { const { sessionId, txMeta: metadata } = data const session = context.getSession(sessionId) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index b845c42b1..49b8fb0a2 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -1,5 +1,3 @@ -import neo4j from './neo4j' -import { cypherToNative } from './cypher-native-binders.js' import * as responses from './responses.js' export function throwFrontendError () { @@ -10,7 +8,7 @@ export function isFrontendError (error) { return error.message === 'TestKit FrontendError' } -export function NewDriver (context, data, wire) { +export function NewDriver (neo4j, context, data, wire) { const { uri, authorizationToken: { data: authToken }, @@ -43,16 +41,17 @@ export function NewDriver (context, data, wire) { } const resolver = resolverRegistered ? address => - new Promise((resolve, reject) => { - const id = context.addResolverRequest(resolve, reject) - wire.writeResponse(responses.ResolverResolutionRequired({ id, address })) - }) + new Promise((resolve, reject) => { + const id = context.addResolverRequest(resolve, reject) + wire.writeResponse(responses.ResolverResolutionRequired({ id, address })) + }) : undefined + const config = { userAgent, resolver, useBigInt: true, - logging: neo4j.logging.console(process.env.LOG_LEVEL || context.logLevel) + logging: neo4j.logging.console(context.logLevel || context.environmentLogLevel) } if ('encrypted' in data) { config.encrypted = data.encrypted ? 'ENCRYPTION_ON' : 'ENCRYPTION_OFF' @@ -95,7 +94,7 @@ export function NewDriver (context, data, wire) { wire.writeResponse(responses.Driver({ id })) } -export function DriverClose (context, data, wire) { +export function DriverClose (_, context, data, wire) { const { driverId } = data const driver = context.getDriver(driverId) return driver @@ -106,7 +105,7 @@ export function DriverClose (context, data, wire) { .catch(err => wire.writeError(err)) } -export function NewSession (context, data, wire) { +export function NewSession (neo4j, context, data, wire) { let { driverId, accessMode, bookmarks, database, fetchSize, impersonatedUser, bookmarkManagerId } = data switch (accessMode) { case 'r': @@ -140,7 +139,7 @@ export function NewSession (context, data, wire) { wire.writeResponse(responses.Session({ id })) } -export function SessionClose (context, data, wire) { +export function SessionClose (_, context, data, wire) { const { sessionId } = data const session = context.getSession(sessionId) return session @@ -151,12 +150,12 @@ export function SessionClose (context, data, wire) { .catch(err => wire.writeError(err)) } -export function SessionRun (context, data, wire) { +export function SessionRun (_, context, data, wire) { const { sessionId, cypher, params, txMeta: metadata, timeout } = data const session = context.getSession(sessionId) if (params) { for (const [key, value] of Object.entries(params)) { - params[key] = cypherToNative(value) + params[key] = context.binder.cypherToNative(value) } } @@ -174,7 +173,7 @@ export function SessionRun (context, data, wire) { wire.writeResponse(responses.Result({ id })) } -export function ResultNext (context, data, wire) { +export function ResultNext (_, context, data, wire) { const { resultId } = data const result = context.getResult(resultId) if (!('recordIt' in result)) { @@ -184,7 +183,7 @@ export function ResultNext (context, data, wire) { if (done) { wire.writeResponse(responses.NullRecord()) } else { - wire.writeResponse(responses.Record({ record: value })) + wire.writeResponse(responses.Record({ record: value }, { binder: context.binder })) } }).catch(e => { console.log('got some err: ' + JSON.stringify(e)) @@ -192,7 +191,7 @@ export function ResultNext (context, data, wire) { }) } -export function ResultPeek (context, data, wire) { +export function ResultPeek (_, context, data, wire) { const { resultId } = data const result = context.getResult(resultId) if (!('recordIt' in result)) { @@ -202,7 +201,7 @@ export function ResultPeek (context, data, wire) { if (done) { wire.writeResponse(responses.NullRecord()) } else { - wire.writeResponse(responses.Record({ record: value })) + wire.writeResponse(responses.Record({ record: value }, { binder: context.binder })) } }).catch(e => { console.log('got some err: ' + JSON.stringify(e)) @@ -210,28 +209,27 @@ export function ResultPeek (context, data, wire) { }) } -export function ResultConsume (context, data, wire) { +export function ResultConsume (_, context, data, wire) { const { resultId } = data const result = context.getResult(resultId) return result.summary().then(summary => { - wire.writeResponse(responses.Summary({ summary })) + wire.writeResponse(responses.Summary({ summary }, { binder: context.binder })) }).catch(e => wire.writeError(e)) } -export function ResultList (context, data, wire) { +export function ResultList (_, context, data, wire) { const { resultId } = data - const result = context.getResult(resultId) return result .then(({ records }) => { - wire.writeResponse(responses.RecordList({ records })) + wire.writeResponse(responses.RecordList({ records }, { binder: context.binder })) }) .catch(error => wire.writeError(error)) } -export function SessionReadTransaction (context, data, wire) { +export function SessionReadTransaction (_, context, data, wire) { const { sessionId, txMeta: metadata } = data const session = context.getSession(sessionId) return session @@ -246,12 +244,12 @@ export function SessionReadTransaction (context, data, wire) { .catch(error => wire.writeError(error)) } -export function TransactionRun (context, data, wire) { +export function TransactionRun (_, context, data, wire) { const { txId, cypher, params } = data const tx = context.getTx(txId) if (params) { for (const [key, value] of Object.entries(params)) { - params[key] = cypherToNative(value) + params[key] = context.binder.cypherToNative(value) } } const result = tx.tx.run(cypher, params) @@ -260,14 +258,14 @@ export function TransactionRun (context, data, wire) { wire.writeResponse(responses.Result({ id })) } -export function RetryablePositive (context, data, wire) { +export function RetryablePositive (_, context, data, wire) { const { sessionId } = data context.getTxsBySessionId(sessionId).forEach(tx => { tx.resolve() }) } -export function RetryableNegative (context, data, wire) { +export function RetryableNegative (_, context, data, wire) { const { sessionId, errorId } = data const error = context.getError(errorId) || new Error('TestKit FrontendError') context.getTxsBySessionId(sessionId).forEach(tx => { @@ -275,7 +273,7 @@ export function RetryableNegative (context, data, wire) { }) } -export function SessionBeginTransaction (context, data, wire) { +export function SessionBeginTransaction (_, context, data, wire) { const { sessionId, txMeta: metadata, timeout } = data const session = context.getSession(sessionId) @@ -294,7 +292,7 @@ export function SessionBeginTransaction (context, data, wire) { } } -export function TransactionCommit (context, data, wire) { +export function TransactionCommit (_, context, data, wire) { const { txId: id } = data const { tx } = context.getTx(id) return tx.commit() @@ -305,7 +303,7 @@ export function TransactionCommit (context, data, wire) { }) } -export function TransactionRollback (context, data, wire) { +export function TransactionRollback (_, context, data, wire) { const { txId: id } = data const { tx } = context.getTx(id) return tx.rollback() @@ -313,7 +311,7 @@ export function TransactionRollback (context, data, wire) { .catch(e => wire.writeError(e)) } -export function TransactionClose (context, data, wire) { +export function TransactionClose (_, context, data, wire) { const { txId: id } = data const { tx } = context.getTx(id) return tx.close() @@ -321,14 +319,14 @@ export function TransactionClose (context, data, wire) { .catch(e => wire.writeError(e)) } -export function SessionLastBookmarks (context, data, wire) { +export function SessionLastBookmarks (_, context, data, wire) { const { sessionId } = data const session = context.getSession(sessionId) const bookmarks = session.lastBookmarks() wire.writeResponse(responses.Bookmarks({ bookmarks })) } -export function SessionWriteTransaction (context, data, wire) { +export function SessionWriteTransaction (_, context, data, wire) { const { sessionId, txMeta: metadata } = data const session = context.getSession(sessionId) return session @@ -343,7 +341,7 @@ export function SessionWriteTransaction (context, data, wire) { .catch(error => wire.writeError(error)) } -export function StartTest (context, { testName }, wire) { +export function StartTest (_, context, { testName }, wire) { if (testName.endsWith('.test_disconnect_session_on_tx_pull_after_record') || testName.endsWith('test_no_reset_on_clean_connection')) { context.logLevel = 'debug' } else { @@ -361,7 +359,7 @@ export function StartTest (context, { testName }, wire) { }) } -export function StartSubTest (context, { testName, subtestArguments }, wire) { +export function StartSubTest (_, context, { testName, subtestArguments }, wire) { if (testName === 'neo4j.datatypes.test_temporal_types.TestDataTypes.test_date_time_cypher_created_tz_id') { try { Intl.DateTimeFormat(undefined, { timeZone: subtestArguments.tz_id }) @@ -374,13 +372,13 @@ export function StartSubTest (context, { testName, subtestArguments }, wire) { } } -export function GetFeatures (context, _params, wire) { +export function GetFeatures (_, context, _params, wire) { wire.writeResponse(responses.FeatureList({ features: context.getFeatures() })) } -export function VerifyConnectivity (context, { driverId }, wire) { +export function VerifyConnectivity (_, context, { driverId }, wire) { const driver = context.getDriver(driverId) return driver .verifyConnectivity() @@ -388,7 +386,7 @@ export function VerifyConnectivity (context, { driverId }, wire) { .catch(error => wire.writeError(error)) } -export function GetServerInfo (context, { driverId }, wire) { +export function GetServerInfo (_, context, { driverId }, wire) { const driver = context.getDriver(driverId) return driver .getServerInfo() @@ -396,7 +394,7 @@ export function GetServerInfo (context, { driverId }, wire) { .catch(error => wire.writeError(error)) } -export function CheckMultiDBSupport (context, { driverId }, wire) { +export function CheckMultiDBSupport (_, context, { driverId }, wire) { const driver = context.getDriver(driverId) return driver .supportsMultiDb() @@ -407,6 +405,7 @@ export function CheckMultiDBSupport (context, { driverId }, wire) { } export function ResolverResolutionCompleted ( + _, context, { requestId, addresses }, wire @@ -416,6 +415,7 @@ export function ResolverResolutionCompleted ( } export function NewBookmarkManager ( + neo4j, context, { initialBookmarks, @@ -458,6 +458,7 @@ export function NewBookmarkManager ( } export function BookmarkManagerClose ( + _, context, { id @@ -469,6 +470,7 @@ export function BookmarkManagerClose ( } export function BookmarksSupplierCompleted ( + _, context, { requestId, @@ -480,6 +482,7 @@ export function BookmarksSupplierCompleted ( } export function BookmarksConsumerCompleted ( + _, context, { requestId @@ -489,7 +492,7 @@ export function BookmarksConsumerCompleted ( notifyBookmarksRequest.resolve() } -export function GetRoutingTable (context, { driverId, database }, wire) { +export function GetRoutingTable (_, context, { driverId, database }, wire) { const driver = context.getDriver(driverId) const routingTable = driver && @@ -512,7 +515,7 @@ export function GetRoutingTable (context, { driverId, database }, wire) { } } -export function ForcedRoutingTableUpdate (context, { driverId, database, bookmarks }, wire) { +export function ForcedRoutingTableUpdate (_, context, { driverId, database, bookmarks }, wire) { const driver = context.getDriver(driverId) const provider = driver._getOrCreateConnectionProvider() diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 000d906e4..044f02e34 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -1,7 +1,3 @@ -import { - nativeToCypher -} from './cypher-native-binders.js' - import { nativeToTestkitSummary } from './summary-binder.js' @@ -50,20 +46,20 @@ export function NullRecord () { return response('NullRecord', null) } -export function Record ({ record }) { - const values = Array.from(record.values()).map(nativeToCypher) +export function Record ({ record }, { binder }) { + const values = Array.from(record.values()).map(binder.nativeToCypher) return response('Record', { values }) } -export function RecordList ({ records }) { +export function RecordList ({ records }, { binder }) { const cypherRecords = records.map(rec => { - return { values: Array.from(rec.values()).map(nativeToCypher) } + return { values: Array.from(rec.values()).map(binder.nativeToCypher) } }) return response('RecordList', { records: cypherRecords }) } -export function Summary ({ summary }) { - return response('Summary', nativeToTestkitSummary(summary)) +export function Summary ({ summary }, { binder }) { + return response('Summary', nativeToTestkitSummary(summary, binder)) } export function Bookmarks ({ bookmarks }) { diff --git a/packages/testkit-backend/src/skipped-tests/browser.js b/packages/testkit-backend/src/skipped-tests/browser.js index 788e1657a..b8313e21d 100644 --- a/packages/testkit-backend/src/skipped-tests/browser.js +++ b/packages/testkit-backend/src/skipped-tests/browser.js @@ -1,4 +1,4 @@ -import skip, { ifEndsWith, ifEquals, ifStartsWith } from './skip' +import skip, { ifEndsWith, ifEquals, ifStartsWith } from './skip.js' const skippedTests = [ skip( "Browser doesn't support socket timeouts", diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index bbe75f9be..7eb1f1e3e 100644 --- a/packages/testkit-backend/src/skipped-tests/common.js +++ b/packages/testkit-backend/src/skipped-tests/common.js @@ -1,4 +1,4 @@ -import skip, { ifEquals, ifEndsWith, endsWith, ifStartsWith, startsWith, not } from './skip' +import skip, { ifEquals, ifEndsWith, endsWith, ifStartsWith, startsWith, not } from './skip.js' const skippedTests = [ skip( diff --git a/packages/testkit-backend/src/skipped-tests/deno.js b/packages/testkit-backend/src/skipped-tests/deno.js new file mode 100644 index 000000000..e2dd2eec2 --- /dev/null +++ b/packages/testkit-backend/src/skipped-tests/deno.js @@ -0,0 +1,20 @@ +import skip, { ifEndsWith, ifStartsWith, ifEquals } from './skip.js' + +const skippedTests = [ + skip('DenoJS fails hard on certificate error', + ifEndsWith('test_trusted_ca_expired_server_correct_hostname'), + ifEndsWith('test_trusted_ca_wrong_hostname'), + ifEndsWith('test_unencrypted'), + ifEndsWith('test_untrusted_ca_correct_hostname'), + ifEndsWith('test_1_1') + ), + skip('Trust All is not available as configuration', + ifStartsWith('tls.test_self_signed_scheme.TestTrustAllCertsConfig.'), + ifStartsWith('tls.test_self_signed_scheme.TestSelfSignedScheme.') + ), + skip('Takes a bit longer to complete in TeamCity', + ifEquals('neo4j.test_session_run.TestSessionRun.test_long_string') + ) +] + +export default skippedTests diff --git a/packages/testkit-backend/src/skipped-tests/index.js b/packages/testkit-backend/src/skipped-tests/index.js index e1ee901be..324d07706 100644 --- a/packages/testkit-backend/src/skipped-tests/index.js +++ b/packages/testkit-backend/src/skipped-tests/index.js @@ -1,10 +1,12 @@ -import commonSkippedTests from './common' -import browserSkippedTests from './browser' -import rxSessionSkippedTests from './rx' +import commonSkippedTests from './common.js' +import browserSkippedTests from './browser.js' +import rxSessionSkippedTests from './rx.js' +import denoSkippedTests from './deno.js' const skippedTestsByContext = new Map([ ['browser', browserSkippedTests], - ['rx', rxSessionSkippedTests] + ['rx', rxSessionSkippedTests], + ['deno', denoSkippedTests] ]) export function getShouldRunTest (contexts) { diff --git a/packages/testkit-backend/src/skipped-tests/rx.js b/packages/testkit-backend/src/skipped-tests/rx.js index 0d0be9514..558e17f4e 100644 --- a/packages/testkit-backend/src/skipped-tests/rx.js +++ b/packages/testkit-backend/src/skipped-tests/rx.js @@ -1,4 +1,4 @@ -import { skip, ifEquals } from './skip' +import { skip, ifEquals } from './skip.js' const skippedTests = [ skip( diff --git a/packages/testkit-backend/src/summary-binder.js b/packages/testkit-backend/src/summary-binder.js index 24e5a46ef..d7221a3ea 100644 --- a/packages/testkit-backend/src/summary-binder.js +++ b/packages/testkit-backend/src/summary-binder.js @@ -1,5 +1,3 @@ -import { objectToCypher, objectMemberBitIntToNumber } from './cypher-native-binders.js' - function mapPlan (plan) { return { operatorType: plan.operatorType, @@ -18,10 +16,10 @@ function mapCounters (stats) { } } -function mapProfile (profile, child = false) { - const mapChild = (child) => mapProfile(child, true) +function mapProfile (profile, child = false, binder) { + const mapChild = (child) => mapProfile(child, true, binder) const obj = { - args: objectMemberBitIntToNumber(profile.arguments), + args: binder.objectMemberBitIntToNumber(profile.arguments), dbHits: Number(profile.dbHits), identifiers: profile.identifiers, operatorType: profile.operatorType, @@ -48,13 +46,13 @@ function mapNotification (notification) { } } -export function nativeToTestkitSummary (summary) { +export function nativeToTestkitSummary (summary, binder) { return { - ...objectMemberBitIntToNumber(summary), + ...binder.objectMemberBitIntToNumber(summary), database: summary.database.name, query: { text: summary.query.text, - parameters: objectToCypher(summary.query.parameters) + parameters: binder.objectToCypher(summary.query.parameters) }, serverInfo: { agent: summary.server.agent, @@ -62,7 +60,7 @@ export function nativeToTestkitSummary (summary) { }, counters: mapCounters(summary.counters), plan: mapPlan(summary.plan), - profile: mapProfile(summary.profile), + profile: mapProfile(summary.profile, false, binder), notifications: summary.notifications.map(mapNotification) } } diff --git a/testkit/Dockerfile b/testkit/Dockerfile index a65f7ad1c..15e30041a 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -1,6 +1,7 @@ FROM ubuntu:20.04 ARG NODE_VERSION=10 +ARG DENO_VERSION=1.19.3 ENV DEBIAN_FRONTEND noninteractive ENV NODE_OPTIONS --max_old_space_size=4096 --use-openssl-ca @@ -47,8 +48,10 @@ COPY CustomCAs/* /usr/local/share/custom-ca-certificates/ RUN update-ca-certificates --verbose # Add Deno -RUN curl -fsSL https://deno.land/x/install/install.sh | sh +RUN curl -fsSL https://deno.land/x/install/install.sh | sh -s v$DENO_VERSION RUN mv /root/.deno/bin/deno /usr/bin/ +# Using System CA in Deno +ENV DENO_TLS_CA_STORE=system # Creating an user for building the driver and running the tests RUN useradd -m driver && echo "driver:driver" | chpasswd && adduser driver sudo diff --git a/testkit/backend.py b/testkit/backend.py index 4acdce471..7e1bb6784 100644 --- a/testkit/backend.py +++ b/testkit/backend.py @@ -6,12 +6,18 @@ from common import ( open_proccess_in_driver_repo, is_browser, + is_deno, + run_in_driver_repo ) import os import time if __name__ == "__main__": print("starting backend") + backend_script = "start-testkit-backend" + if is_deno(): + backend_script = "start-testkit-backend::deno" + if is_browser(): print("Testkit should test browser") os.environ["TEST_ENVIRONMENT"] = "REMOTE" @@ -22,7 +28,7 @@ print("npm run start-testkit-backend") with open_proccess_in_driver_repo([ - "npm", "run", "start-testkit-backend" + "npm", "run", backend_script ], env=os.environ) as backend: if (is_browser()): time.sleep(5) diff --git a/testkit/build.py b/testkit/build.py index ae9292b17..ad428a0b5 100644 --- a/testkit/build.py +++ b/testkit/build.py @@ -2,7 +2,7 @@ Executed in Javascript driver container. Responsible for building driver and test backend. """ -from common import run, run_in_driver_repo, DRIVER_REPO +from common import is_deno, run, run_in_driver_repo, DRIVER_REPO import os diff --git a/testkit/common.py b/testkit/common.py index a6e58adff..93717b9d9 100644 --- a/testkit/common.py +++ b/testkit/common.py @@ -42,3 +42,7 @@ def is_lite(): def is_browser(): return is_enabled(os.environ.get("TEST_DRIVER_BROWSER", "false")) + + +def is_deno(): + return is_enabled(os.environ.get("TEST_DRIVER_DENO", "false")) diff --git a/testkit/integration.py b/testkit/integration.py index 1af24054e..165d84fb5 100644 --- a/testkit/integration.py +++ b/testkit/integration.py @@ -2,6 +2,7 @@ import os from common import ( is_browser, + is_deno, is_lite, run_in_driver_repo, ) @@ -14,7 +15,9 @@ else: ignore = "--ignore=neo4j-driver-lite" - if is_browser(): + if is_deno(): + pass + elif 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 7481e6bae..43332f83e 100644 --- a/testkit/stress.py +++ b/testkit/stress.py @@ -1,6 +1,7 @@ import os from common import ( is_browser, + is_deno, is_lite, run_in_driver_repo, ) @@ -17,4 +18,5 @@ else: ignore = "--ignore=neo4j-driver-lite" - run_in_driver_repo(["npm", "run", "test::stress", "--", ignore]) + if not is_deno(): + run_in_driver_repo(["npm", "run", "test::stress", "--", ignore]) diff --git a/testkit/unittests.py b/testkit/unittests.py index 24e5a9c99..791590a7c 100644 --- a/testkit/unittests.py +++ b/testkit/unittests.py @@ -3,12 +3,11 @@ Responsible for running unit tests. Assumes driver has been setup by build script prior to this. """ -import os -from common import run_in_driver_repo, is_lite +from common import is_deno, run_in_driver_repo, is_lite if __name__ == "__main__": - if is_lite(): + if is_lite() or is_deno(): ignore = "--ignore=neo4j-driver" else: ignore = "--ignore=neo4j-driver-lite"