diff --git a/bolt-connection/src/channel/browser/browser-channel.js b/bolt-connection/src/channel/browser/browser-channel.js index 2c3a2d997..496889f75 100644 --- a/bolt-connection/src/channel/browser/browser-channel.js +++ b/bolt-connection/src/channel/browser/browser-channel.js @@ -171,6 +171,16 @@ export default class WebSocketChannel { }) } + /** + * 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) {} + /** * Set connection timeout on the given WebSocket, if configured. * @return {number} the timeout id or null. diff --git a/bolt-connection/src/channel/node/node-channel.js b/bolt-connection/src/channel/node/node-channel.js index 9613814b4..c1bebe23a 100644 --- a/bolt-connection/src/channel/node/node-channel.js +++ b/bolt-connection/src/channel/node/node-channel.js @@ -148,7 +148,7 @@ const TrustStrategy = { * @param {function} onFailure - callback to execute on connection failure. * @return {*} socket connection. */ -function connect (config, onSuccess, onFailure = () => null) { +function _connect (config, onSuccess, onFailure = () => null) { const trustStrategy = trustStrategyName(config) if (!isEncrypted(config)) { const socket = net.connect( @@ -230,7 +230,7 @@ export default class NodeChannel { * Create new instance * @param {ChannelConfig} config - configuration for this channel. */ - constructor (config) { + constructor (config, connect = _connect) { const self = this this.id = _CONNECTION_IDGEN++ @@ -305,12 +305,12 @@ export default class NodeChannel { _setupConnectionTimeout (config, socket) { const timeout = config.connectionTimeout if (timeout) { - socket.on('connect', () => { + const connectListener = () => { // connected - clear connection timeout socket.setTimeout(0) - }) + } - socket.on('timeout', () => { + const timeoutListener = () => { // timeout fired - not connected within configured time. cancel timeout and destroy socket socket.setTimeout(0) socket.destroy( @@ -319,12 +319,43 @@ export default class NodeChannel { config.connectionErrorCode ) ) - }) + } + + socket.on('connect', connectListener) + socket.on('timeout', timeoutListener) + + this._removeConnectionTimeoutListeners = () => { + this._conn.off('connect', connectListener) + this._conn.off('timeout', timeoutListener) + } socket.setTimeout(timeout) } } + /** + * Setup the receive timeout for the channel. + * + * @param {number} receiveTimeout How long the channel will wait for receiving data before timing out (ms) + * @returns {void} + */ + setupReceiveTimeout (receiveTimeout) { + if (this._removeConnectionTimeoutListeners) { + this._removeConnectionTimeoutListeners() + } + + this._conn.on('timeout', () => { + this._conn.destroy( + newError( + `Connection lost. Server didn't respond in ${receiveTimeout}ms`, + this._connectionErrorCode + ) + ) + }) + + this._conn.setTimeout(receiveTimeout) + } + /** * Write the passed in buffer to connection * @param {NodeBuffer} buffer - Buffer to write diff --git a/bolt-connection/src/connection/connection-channel.js b/bolt-connection/src/connection/connection-channel.js index 95a08de5e..479765e77 100644 --- a/bolt-connection/src/connection/connection-channel.js +++ b/bolt-connection/src/connection/connection-channel.js @@ -18,7 +18,7 @@ */ import { Chunker, Dechunker, ChannelConfig, Channel } from '../channel' -import { newError, error, json, internal } from 'neo4j-driver-core' +import { newError, error, json, internal, toNumber } from 'neo4j-driver-core' import Connection from './connection' import Bolt from '../bolt' @@ -198,6 +198,28 @@ export default class ChannelConnection extends Connection { if (!this.databaseId) { this.databaseId = dbConnectionId } + + if (metadata.hints) { + const receiveTimeoutRaw = + metadata.hints['connection.recv_timeout_seconds'] + if ( + receiveTimeoutRaw !== null && + receiveTimeoutRaw !== undefined + ) { + const receiveTimeoutInSeconds = toNumber(receiveTimeoutRaw) + if ( + Number.isInteger(receiveTimeoutInSeconds) && + receiveTimeoutInSeconds > 0 + ) { + this._ch.setupReceiveTimeout(receiveTimeoutInSeconds * 1000) + } else { + this._log.info( + `Server located at ${this._address} supplied an invalid connection receive timeout value (${receiveTimeoutInSeconds}). ` + + 'Please, verify the server configuration and status because this can be the symptom of a bigger issue.' + ) + } + } + } } resolve(self) } diff --git a/bolt-connection/src/rediscovery/routing-table.js b/bolt-connection/src/rediscovery/routing-table.js index 8184ad28b..c6918eb87 100644 --- a/bolt-connection/src/rediscovery/routing-table.js +++ b/bolt-connection/src/rediscovery/routing-table.js @@ -37,13 +37,21 @@ const MIN_ROUTERS = 1 * The routing table object used to determine the role of the servers in the driver. */ export default class RoutingTable { - constructor ({ database, routers, readers, writers, expirationTime } = {}) { + constructor ({ + database, + routers, + readers, + writers, + expirationTime, + ttl + } = {}) { this.database = database this.databaseName = database || 'default database' this.routers = routers || [] this.readers = readers || [] this.writers = writers || [] this.expirationTime = expirationTime || int(0) + this.ttl = ttl } /** @@ -139,6 +147,7 @@ export function createValidRoutingTable ( routerAddress, rawRoutingTable ) { + const ttl = rawRoutingTable.ttl const expirationTime = calculateExpirationTime(rawRoutingTable, routerAddress) const { routers, readers, writers } = parseServers( rawRoutingTable, @@ -153,7 +162,8 @@ export function createValidRoutingTable ( routers, readers, writers, - expirationTime + expirationTime, + ttl }) } diff --git a/test/internal/browser/browser-channel.test.js b/bolt-connection/test/channel/browser/browser-channel.test.js similarity index 87% rename from test/internal/browser/browser-channel.test.js rename to bolt-connection/test/channel/browser/browser-channel.test.js index 3025598f0..53fa9f873 100644 --- a/test/internal/browser/browser-channel.test.js +++ b/bolt-connection/test/channel/browser/browser-channel.test.js @@ -17,10 +17,10 @@ * limitations under the License. */ -import WebSocketChannel from '../../../bolt-connection/lib/channel/browser/browser-channel' -import ChannelConfig from '../../../bolt-connection/lib/channel/channel-config' +import WebSocketChannel from '../../../src/channel/browser/browser-channel' +import ChannelConfig from '../../../src/channel/channel-config' import { error, internal } from 'neo4j-driver-core' -import { setTimeoutMock } from '../timers-util' +import { setTimeoutMock } from '../../timers-util' const { serverAddress: { ServerAddress }, @@ -35,7 +35,7 @@ const WS_CLOSING = 2 const WS_CLOSED = 3 /* eslint-disable no-global-assign */ -describe('#unit WebSocketChannel', () => { +describe('WebSocketChannel', () => { let webSocketChannel afterEach(async () => { @@ -173,7 +173,7 @@ describe('#unit WebSocketChannel', () => { createWebSocketFactory(WS_CLOSED) ) - await expectAsync(channel.close()).toBeResolved() + await expect(channel.close()).resolves.not.toThrow() }) it('should resolve close when websocket is closed', async () => { @@ -186,7 +186,7 @@ describe('#unit WebSocketChannel', () => { createWebSocketFactory(WS_OPEN) ) - await expectAsync(channel.close()).toBeResolved() + await expect(channel.close()).resolves.not.toThrow() }) function testFallbackToLiteralIPv6 (boltAddress, expectedWsAddress) { @@ -294,6 +294,39 @@ describe('#unit WebSocketChannel', () => { } }) + describe('.setupReceiveTimeout()', () => { + beforeEach(() => { + const address = ServerAddress.fromUrl('http://localhost:8989') + const channelConfig = new ChannelConfig( + address, + { connectionTimeout: 0 }, + SERVICE_UNAVAILABLE + ) + webSocketChannel = new WebSocketChannel( + channelConfig, + undefined, + createWebSocketFactory(WS_OPEN) + ) + }) + + it('should exists', () => { + expect(webSocketChannel).toHaveProperty('setupReceiveTimeout') + expect(typeof webSocketChannel.setupReceiveTimeout).toBe('function') + }) + + it('should not setTimeout', () => { + const fakeSetTimeout = setTimeoutMock.install() + try { + webSocketChannel.setupReceiveTimeout() + + expect(fakeSetTimeout._timeoutIdCounter).toEqual(0) + expect(webSocketChannel._connectionTimeoutId).toBe(null) + } finally { + fakeSetTimeout.uninstall() + } + }) + }) + function createWebSocketFactory (readyState) { const ws = {} ws.readyState = readyState diff --git a/test/internal/browser/browser-host-name-resolver.test.js b/bolt-connection/test/channel/browser/browser-host-name-resolver.test.js similarity index 92% rename from test/internal/browser/browser-host-name-resolver.test.js rename to bolt-connection/test/channel/browser/browser-host-name-resolver.test.js index 1995c096d..61653833b 100644 --- a/test/internal/browser/browser-host-name-resolver.test.js +++ b/bolt-connection/test/channel/browser/browser-host-name-resolver.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import BrowserHostNameResolver from '../../../bolt-connection/lib/channel/browser/browser-host-name-resolver' +import BrowserHostNameResolver from '../../../src/channel/browser/browser-host-name-resolver' describe('#unit BrowserHostNameResolver', () => { it('should resolve given address to itself', done => { diff --git a/bolt-connection/test/channel/node/node-channel.test.js b/bolt-connection/test/channel/node/node-channel.test.js new file mode 100644 index 000000000..bd649e854 --- /dev/null +++ b/bolt-connection/test/channel/node/node-channel.test.js @@ -0,0 +1,138 @@ +/** + * 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 NodeChannel from '../../../src/channel/node/node-channel' +import ChannelConfig from '../../../src/channel/channel-config' +import { error, internal, newError } from 'neo4j-driver-core' + +const { + serverAddress: { ServerAddress } +} = internal + +const { SERVICE_UNAVAILABLE } = error + +describe('NodeChannel', () => { + it('should resolve close if websocket is already closed', () => { + const address = ServerAddress.fromUrl('bolt://localhost:9999') + const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) + const channel = new NodeChannel(channelConfig) + + // Modify the connection to be closed + channel._open = false + + return expect(channel.close()).resolves.not.toThrow() + }) + + it('should resolve close when websocket is connected', () => { + const channel = createMockedChannel(true) + + return expect(channel.close()).resolves.not.toThrow() + }) + + describe('.setupReceiveTimeout()', () => { + it('should call socket.setTimeout(receiveTimeout)', () => { + const receiveTimeout = 42 + const channel = createMockedChannel(true) + + channel.setupReceiveTimeout(receiveTimeout) + + expect(channel._conn.getCalls().setTimeout[1]).toEqual([receiveTimeout]) + }) + + it('should unsubscribe to the on connect and on timeout created on the create socket', () => { + const receiveTimeout = 42 + const channel = createMockedChannel(true) + + channel.setupReceiveTimeout(receiveTimeout) + + expect(channel._conn.getCalls().on.slice(0, 2)).toEqual( + channel._conn.getCalls().off + ) + }) + + it('should destroy the connection when time out', () => { + const receiveTimeout = 42 + const channel = createMockedChannel(true) + + channel.setupReceiveTimeout(receiveTimeout) + + const [event, listener] = channel._conn.getCalls().on[2] + expect(event).toEqual('timeout') + listener() + + expect(channel._conn.getCalls().destroy).toEqual([ + [ + newError( + "Connection lost. Server didn't respond in 42ms", + SERVICE_UNAVAILABLE + ) + ] + ]) + }) + + it('should not unsubscribe from on connect nor from on timeout if connectionTimeout is not set', () => { + const receiveTimeout = 42 + const channel = createMockedChannel(true, { connectionTimeout: 0 }) + + channel.setupReceiveTimeout(receiveTimeout) + + expect(channel._conn.getCalls().off).toEqual([]) + }) + }) +}) + +function createMockedChannel (connected, config = {}) { + let endCallback = null + const address = ServerAddress.fromUrl('bolt://localhost:9999') + const channelConfig = new ChannelConfig(address, config, SERVICE_UNAVAILABLE) + const socketFactory = () => { + const on = [] + const off = [] + const setTimeout = [] + const destroy = [] + return { + destroyed: false, + destroy: () => { + destroy.push([...arguments]) + }, + end: () => { + channel._open = false + endCallback() + }, + removeListener: () => {}, + on: (key, cb) => { + on.push([...arguments]) + if (key === 'end') { + endCallback = cb + } + }, + off: () => { + off.push([...arguments]) + }, + setTimeout: () => { + setTimeout.push([...arguments]) + }, + getCalls: () => { + return { on, off, setTimeout, destroy } + } + } + } + const channel = new NodeChannel(channelConfig, socketFactory) + channel._open = connected + return channel +} diff --git a/test/internal/node/node-host-name-resolver.test.js b/bolt-connection/test/channel/node/node-host-name-resolver.test.js similarity index 97% rename from test/internal/node/node-host-name-resolver.test.js rename to bolt-connection/test/channel/node/node-host-name-resolver.test.js index aa5935b5d..c6cdbe206 100644 --- a/test/internal/node/node-host-name-resolver.test.js +++ b/bolt-connection/test/channel/node/node-host-name-resolver.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import NodeHostNameResolver from '../../../bolt-connection/lib/channel/node/node-host-name-resolver' +import NodeHostNameResolver from '../../../src/channel/node/node-host-name-resolver' import { internal } from 'neo4j-driver-core' const { diff --git a/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/bolt-connection/test/connection-provider/connection-provider-routing.test.js index ad0c34b44..f61b3c635 100644 --- a/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -1719,7 +1719,6 @@ describe('#unit RoutingConnectionProvider', () => { database: 'databaseX' }) } catch (error) { - console.error('Message', error) expect(error instanceof Neo4jError).toBeTruthy() expect(error.code).toBe(SERVICE_UNAVAILABLE) expect(error.message).toContain( diff --git a/bolt-connection/test/connection/connection-channel.test.js b/bolt-connection/test/connection/connection-channel.test.js new file mode 100644 index 000000000..93a3da3f3 --- /dev/null +++ b/bolt-connection/test/connection/connection-channel.test.js @@ -0,0 +1,153 @@ +/** + * 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 ChannelConnection from '../../src/connection/connection-channel' +import { int, internal } from 'neo4j-driver-core' +import { add } from 'lodash' + +const { + serverAddress: { ServerAddress }, + logger: { Logger } +} = internal + +describe('ChannelConnection', () => { + describe('.connect()', () => { + it.each([ + [42000, 42n], + [21000, 21], + [12000, int(12)] + ])( + "should call this._ch.setupReceiveTimeout(%o) when onComplete metadata.hints['connection.recv_timeout_seconds'] is %o", + async (expected, receiveTimeout) => { + const channel = { + setupReceiveTimeout: jest.fn().mockName('setupReceiveTimeout') + } + const protocol = { + initialize: jest.fn(observer => + observer.onComplete({ + hints: { 'connection.recv_timeout_seconds': receiveTimeout } + }) + ) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ channel, protocolSupplier }) + + await connection.connect('userAgent', {}) + + expect(channel.setupReceiveTimeout).toHaveBeenCalledWith(expected) + } + ) + + it.each([ + [undefined], + [null], + [{}], + [{ hints: null }], + [{ hints: {} }], + [{ hints: { 'connection.recv_timeout_seconds': null } }], + [{ hints: { 'connection.recv_timeout_seconds': -1 } }], + [{ hints: { 'connection.recv_timeout_seconds': -1n } }], + [{ hints: { 'connection.recv_timeout_seconds': int(-1) } }], + [{ hints: { 'connection.recv_timeout_seconds': 0 } }], + [{ hints: { 'connection.recv_timeout_seconds': 0n } }], + [{ hints: { 'connection.recv_timeout_seconds': int(0) } }] + ])( + 'should call not call this._ch.setupReceiveTimeout() when onComplete metadata is %o', + async metadata => { + const channel = { + setupReceiveTimeout: jest.fn().mockName('setupReceiveTimeout') + } + const protocol = { + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ channel, protocolSupplier }) + + await connection.connect('userAgent', {}) + + expect(channel.setupReceiveTimeout).not.toHaveBeenCalled() + } + ) + + it.each([ + [{ hints: { 'connection.recv_timeout_seconds': -1.5 } }], + [{ hints: { 'connection.recv_timeout_seconds': -1 } }], + [{ hints: { 'connection.recv_timeout_seconds': -1n } }], + [{ hints: { 'connection.recv_timeout_seconds': int(-1) } }], + [{ hints: { 'connection.recv_timeout_seconds': 0 } }], + [{ hints: { 'connection.recv_timeout_seconds': 0n } }], + [{ hints: { 'connection.recv_timeout_seconds': int(0) } }], + [{ hints: { 'connection.recv_timeout_seconds': 12.1 } }] + ])( + 'should call log an info when onComplete metadata is %o', + async metadata => { + const channel = { + setupReceiveTimeout: jest.fn().mockName('setupReceiveTimeout') + } + const protocol = { + initialize: jest.fn(observer => observer.onComplete(metadata)) + } + const address = ServerAddress.fromUrl('bolt://localhost') + const protocolSupplier = () => protocol + const loggerFunction = jest.fn().mockName('logger') + const logger = new Logger('info', loggerFunction) + const connection = spyOnConnectionChannel({ + channel, + protocolSupplier, + logger, + address + }) + + await connection.connect('userAgent', {}) + expect(loggerFunction).toHaveBeenCalledWith( + 'info', + `Connection [${ + connection._id + }][] Server located at ${address.asHostPort()} ` + + `supplied an invalid connection receive timeout value (${metadata.hints['connection.recv_timeout_seconds']}). ` + + 'Please, verify the server configuration and status because this can be the symptom of a bigger issue.' + ) + } + ) + }) + + function spyOnConnectionChannel ({ + channel, + errorHandler, + address, + logger, + disableLosslessIntegers, + serversideRouting, + chuncker, + protocolSupplier + }) { + address = address || ServerAddress.fromUrl('bolt://localhost') + logger = logger || new Logger('info', () => {}) + return new ChannelConnection( + channel, + errorHandler, + address, + logger, + disableLosslessIntegers, + serversideRouting, + chuncker, + protocolSupplier + ) + } +}) diff --git a/bolt-connection/test/timers-util.js b/bolt-connection/test/timers-util.js new file mode 100644 index 000000000..01ad01742 --- /dev/null +++ b/bolt-connection/test/timers-util.js @@ -0,0 +1,67 @@ +/** + * 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. + */ +class SetTimeoutMock { + constructor () { + this._clearState() + } + + install () { + this._originalSetTimeout = global.setTimeout + global.setTimeout = (code, delay) => { + if (!this._paused) { + code() + this.invocationDelays.push(delay) + } + return this._timeoutIdCounter++ + } + + this._originalClearTimeout = global.clearTimeout + global.clearTimeout = id => { + this.clearedTimeouts.push(id) + } + + return this + } + + pause () { + this._paused = true + } + + uninstall () { + global.setTimeout = this._originalSetTimeout + global.clearTimeout = this._originalClearTimeout + this._clearState() + } + + setTimeoutOriginal (code, delay) { + return this._originalSetTimeout.call(null, code, delay) + } + + _clearState () { + this._originalSetTimeout = null + this._originalClearTimeout = null + this._paused = false + this._timeoutIdCounter = 0 + + this.invocationDelays = [] + this.clearedTimeouts = [] + } +} + +export const setTimeoutMock = new SetTimeoutMock() diff --git a/core/src/integer.ts b/core/src/integer.ts index 8941f51fd..03ff8b6ba 100644 --- a/core/src/integer.ts +++ b/core/src/integer.ts @@ -909,7 +909,14 @@ class Integer { * @expose */ static toNumber (val: Integerable): number { - return Integer.fromValue(val).toNumber() + switch (typeof val) { + case 'number': + return val + case 'bigint': + return Number(val) + default: + return Integer.fromValue(val).toNumber() + } } /** diff --git a/core/test/integer.test.ts b/core/test/integer.test.ts index 7035a297c..32447e192 100644 --- a/core/test/integer.test.ts +++ b/core/test/integer.test.ts @@ -329,7 +329,7 @@ function forEachToNumberOrInfinityScenarios( } function forEachToNumberScenarios( - func: Consumer> + func: Consumer> ) { ;[ v('42', 42), @@ -337,6 +337,7 @@ function forEachToNumberScenarios( v('-999', -999), v('1000000000', 1000000000), v(1000000000, 1000000000), + v(BigInt(42), 42), v(Integer.MIN_SAFE_VALUE.toString(), Integer.MIN_SAFE_VALUE.toNumber()), v(Integer.MAX_SAFE_VALUE.toString(), Integer.MAX_SAFE_VALUE.toNumber()), v( @@ -988,7 +989,12 @@ function forEachFromStringScenarios( ].forEach(func) } -type Interable = Integer | number | { low: number; high: number } | string +type Interable = + | Integer + | number + | { low: number; high: number } + | string + | bigint function forEachFromValueScenarios( func: Consumer> ) { @@ -1003,9 +1009,13 @@ function forEachFromValueScenarios( function forEachStaticToNumberScenarios( func: Consumer> ) { - ;[v(Integer.ONE, 1), v('1', 1), v(1, 1), v({ low: 1, high: 0 }, 1)].forEach( - func - ) + ;[ + v(Integer.ONE, 1), + v('1', 1), + v(1, 1), + v({ low: 1, high: 0 }, 1), + v(BigInt(10), 10) + ].forEach(func) } function forEachStaticToStringScenarios( diff --git a/test/internal/node/node-channel.test.js b/test/internal/node/node-channel.test.js deleted file mode 100644 index 12b1f9df0..000000000 --- a/test/internal/node/node-channel.test.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * 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 NodeChannel from '../../../bolt-connection/lib/channel/node/node-channel' -import ChannelConfig from '../../../bolt-connection/lib/channel/channel-config' -import { error, internal } from 'neo4j-driver-core' - -const { - serverAddress: { ServerAddress } -} = internal - -const { SERVICE_UNAVAILABLE } = error - -describe('#unit NodeChannel', () => { - it('should resolve close if websocket is already closed', () => { - const address = ServerAddress.fromUrl('bolt://localhost:9999') - const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) - const channel = new NodeChannel(channelConfig) - - // Modify the connection to be closed - channel._open = false - - return expectAsync(channel.close()).toBeResolved() - }) - - it('should resolve close when websocket is connected', () => { - const channel = createMockedChannel(true) - - return expectAsync(channel.close()).toBeResolved() - }) -}) - -function createMockedChannel (connected) { - let endCallback = null - const address = ServerAddress.fromUrl('bolt://localhost:9999') - const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) - const channel = new NodeChannel(channelConfig) - const socket = { - destroyed: false, - destroy: () => {}, - end: () => { - channel._open = false - endCallback() - }, - removeListener: () => {}, - on: (key, cb) => { - if (key === 'end') { - endCallback = cb - } - } - } - channel._conn = socket - channel._open = connected - return channel -} diff --git a/test/internal/routing-table.test.js b/test/internal/routing-table.test.js index 378227bb4..f67c67af7 100644 --- a/test/internal/routing-table.test.js +++ b/test/internal/routing-table.test.js @@ -258,7 +258,8 @@ describe('#unit RoutingTable', () => { readers: readers.map(r => ServerAddress.fromUrl(r)), routers: routers.map(r => ServerAddress.fromUrl(r)), writers: writers.map(w => ServerAddress.fromUrl(w)), - expirationTime: calculateExpirationTime(currentTime, ttl) + expirationTime: calculateExpirationTime(currentTime, ttl), + ttl }) ) })) diff --git a/testkit-backend/src/request-handlers.js b/testkit-backend/src/request-handlers.js index fbbabf999..5dfb37a72 100644 --- a/testkit-backend/src/request-handlers.js +++ b/testkit-backend/src/request-handlers.js @@ -237,7 +237,10 @@ export function StartTest (_, { testName }, wire) { export function GetFeatures (_context, _params, wire) { wire.writeResponse('FeatureList', { - features: ['AuthorizationExpiredTreatment'] + features: [ + 'AuthorizationExpiredTreatment', + 'ConfHint:connection.recv_timeout_seconds' + ] }) } @@ -267,3 +270,25 @@ export function ResolverResolutionCompleted ( const request = context.getResolverRequest(requestId) request.resolve(addresses) } + +export function GetRoutingTable (context, { driverId, database }, wire) { + const serverAddressToString = serverAddress => serverAddress.asHostPort() + const driver = context.getDriver(driverId) + const routingTable = + driver && + driver._getOrCreateConnectionProvider() && + driver._getOrCreateConnectionProvider()._routingTableRegistry && + driver._getOrCreateConnectionProvider()._routingTableRegistry.get(database) + + if (routingTable) { + wire.writeResponse('RoutingTable', { + database: routingTable.database, + ttl: routingTable.ttl, + readers: routingTable.readers.map(serverAddressToString), + writers: routingTable.writers.map(serverAddressToString), + routers: routingTable.routers.map(serverAddressToString) + }) + } else { + wire.writeError('Could not find routing table') + } +}