From 12dfc31c8dd0f32c4350ef57c9aef9ceae6bd267 Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Mon, 19 Jul 2021 16:42:04 +0200 Subject: [PATCH] Verify connection to Elasticsearch (#1487) --- docs/connecting.asciidoc | 13 + index.js | 4 + lib/Transport.d.ts | 3 +- lib/Transport.js | 251 ++++-- lib/errors.d.ts | 7 + lib/errors.js | 13 +- test/acceptance/events-order.test.js | 3 +- test/acceptance/observability.test.js | 3 +- test/acceptance/product-check.test.js | 1131 +++++++++++++++++++++++++ test/acceptance/proxy.test.js | 2 +- test/acceptance/resurrect.test.js | 4 +- test/acceptance/sniff.test.js | 4 +- test/unit/api-async.js | 4 +- test/unit/api.test.js | 4 +- test/unit/child.test.js | 3 +- test/unit/client.test.js | 4 +- test/unit/events.test.js | 3 +- test/unit/helpers/bulk.test.js | 4 +- test/unit/helpers/msearch.test.js | 4 +- test/unit/helpers/scroll.test.js | 4 +- test/unit/helpers/search.test.js | 3 +- test/unit/transport.test.js | 68 ++ test/utils/MockConnection.js | 5 +- test/utils/index.js | 18 +- 24 files changed, 1456 insertions(+), 106 deletions(-) create mode 100644 test/acceptance/product-check.test.js diff --git a/docs/connecting.asciidoc b/docs/connecting.asciidoc index d24abe594..536b64747 100644 --- a/docs/connecting.asciidoc +++ b/docs/connecting.asciidoc @@ -10,6 +10,7 @@ This page contains the information you need to connect and use the Client with * <> * <> * <> +* <> [discrete] [[authentication]] @@ -517,3 +518,15 @@ a|* `name` - `string` * `statusCode` - `object`, the response headers * `headers` - `object`, the response status code |=== + +[discrete] +[[product-check]] +=== Automatic product check + +Since v7.14.0, the client performs a required product check before the first call. +This pre-flight product check allows the client to establish the version of Elasticsearch +that it is communicating with. The product check requires one additional HTTP request to +be sent to the server as part of the request pipeline before the main API call is sent. +In most cases, this will succeed during the very first API call that the client sends. +Once the product check completes, no further product check HTTP requests are sent for +subsequent API calls. diff --git a/index.js b/index.js index fc9e62ff3..60c44014a 100644 --- a/index.js +++ b/index.js @@ -255,6 +255,10 @@ class Client extends ESAPI { } const client = new Client(options) + // sync product check + const tSymbol = Object.getOwnPropertySymbols(this.transport) + .filter(symbol => symbol.description === 'product check')[0] + client.transport[tSymbol] = this.transport[tSymbol] // Add parent extensions if (this[kExtensions].length > 0) { this[kExtensions].forEach(({ name, opts, fn }) => { diff --git a/lib/Transport.d.ts b/lib/Transport.d.ts index 18b4c37cb..912dd96da 100644 --- a/lib/Transport.d.ts +++ b/lib/Transport.d.ts @@ -26,7 +26,8 @@ import * as errors from './errors'; export type ApiError = errors.ConfigurationError | errors.ConnectionError | errors.DeserializationError | errors.SerializationError | errors.NoLivingConnectionsError | errors.ResponseError | - errors.TimeoutError | errors.RequestAbortedError + errors.TimeoutError | errors.RequestAbortedError | + errors.ProductNotSupportedError export type Context = unknown diff --git a/lib/Transport.js b/lib/Transport.js index e99c1ccc9..6abcb723f 100644 --- a/lib/Transport.js +++ b/lib/Transport.js @@ -24,20 +24,24 @@ const os = require('os') const { gzip, unzip, createGzip } = require('zlib') const buffer = require('buffer') const ms = require('ms') +const { EventEmitter } = require('events') const { ConnectionError, RequestAbortedError, NoLivingConnectionsError, ResponseError, - ConfigurationError + ConfigurationError, + ProductNotSupportedError } = require('./errors') const noop = () => {} +const productCheckEmitter = new EventEmitter() const clientVersion = require('../package.json').version const userAgent = `elasticsearch-js/${clientVersion} (${os.platform()} ${os.release()}-${os.arch()}; Node.js ${process.version})` const MAX_BUFFER_LENGTH = buffer.constants.MAX_LENGTH const MAX_STRING_LENGTH = buffer.constants.MAX_STRING_LENGTH +const kProductCheck = Symbol('product check') const kApiVersioning = Symbol('api versioning') class Transport { @@ -65,6 +69,7 @@ class Transport { this.generateRequestId = opts.generateRequestId || generateRequestId() this.name = opts.name this.opaqueIdPrefix = opts.opaqueIdPrefix + this[kProductCheck] = 0 // 0 = to be checked, 1 = checking, 2 = checked-ok, 3 checked-notok this[kApiVersioning] = process.env.ELASTIC_CLIENT_APIVERSIONING === 'true' this.nodeFilter = opts.nodeFilter || defaultNodeFilter @@ -83,7 +88,11 @@ class Transport { this._isSniffing = false if (opts.sniffOnStart === true) { - this.sniff({ reason: Transport.sniffReasons.SNIFF_ON_START }) + // timer needed otherwise it will clash + // with the product check testing + setTimeout(() => { + this.sniff({ reason: Transport.sniffReasons.SNIFF_ON_START }) + }, 10) } } @@ -350,91 +359,124 @@ class Transport { } } - this.emit('serialization', null, result) - const headers = Object.assign({}, this.headers, lowerCaseHeaders(options.headers)) + const prepareRequest = () => { + this.emit('serialization', null, result) + const headers = Object.assign({}, this.headers, lowerCaseHeaders(options.headers)) - if (options.opaqueId !== undefined) { - headers['x-opaque-id'] = this.opaqueIdPrefix !== null - ? this.opaqueIdPrefix + options.opaqueId - : options.opaqueId - } - - // handle json body - if (params.body != null) { - if (shouldSerialize(params.body) === true) { - try { - params.body = this.serializer.serialize(params.body) - } catch (err) { - this.emit('request', err, result) - process.nextTick(callback, err, result) - return transportReturn - } + if (options.opaqueId !== undefined) { + headers['x-opaque-id'] = this.opaqueIdPrefix !== null + ? this.opaqueIdPrefix + options.opaqueId + : options.opaqueId } - if (params.body !== '') { - headers['content-type'] = headers['content-type'] || (this[kApiVersioning] ? 'application/vnd.elasticsearch+json; compatible-with=7' : 'application/json') - } - - // handle ndjson body - } else if (params.bulkBody != null) { - if (shouldSerialize(params.bulkBody) === true) { - try { - params.body = this.serializer.ndserialize(params.bulkBody) - } catch (err) { - this.emit('request', err, result) - process.nextTick(callback, err, result) - return transportReturn - } - } else { - params.body = params.bulkBody - } - if (params.body !== '') { - headers['content-type'] = headers['content-type'] || (this[kApiVersioning] ? 'application/vnd.elasticsearch+x-ndjson; compatible-with=7' : 'application/x-ndjson') - } - } - - params.headers = headers - // serializes the querystring - if (options.querystring == null) { - params.querystring = this.serializer.qserialize(params.querystring) - } else { - params.querystring = this.serializer.qserialize( - Object.assign({}, params.querystring, options.querystring) - ) - } - - // handles request timeout - params.timeout = toMs(options.requestTimeout || this.requestTimeout) - if (options.asStream === true) params.asStream = true - meta.request.params = params - meta.request.options = options - - // handle compression - if (params.body !== '' && params.body != null) { - if (isStream(params.body) === true) { - if (compression === 'gzip') { - params.headers['content-encoding'] = compression - params.body = params.body.pipe(createGzip()) - } - makeRequest() - } else if (compression === 'gzip') { - gzip(params.body, (err, buffer) => { - /* istanbul ignore next */ - if (err) { + // handle json body + if (params.body != null) { + if (shouldSerialize(params.body) === true) { + try { + params.body = this.serializer.serialize(params.body) + } catch (err) { this.emit('request', err, result) - return callback(err, result) + process.nextTick(callback, err, result) + return transportReturn } - params.headers['content-encoding'] = compression - params.headers['content-length'] = '' + Buffer.byteLength(buffer) - params.body = buffer - makeRequest() - }) + } + + if (params.body !== '') { + headers['content-type'] = headers['content-type'] || (this[kApiVersioning] ? 'application/vnd.elasticsearch+json; compatible-with=7' : 'application/json') + } + + // handle ndjson body + } else if (params.bulkBody != null) { + if (shouldSerialize(params.bulkBody) === true) { + try { + params.body = this.serializer.ndserialize(params.bulkBody) + } catch (err) { + this.emit('request', err, result) + process.nextTick(callback, err, result) + return transportReturn + } + } else { + params.body = params.bulkBody + } + if (params.body !== '') { + headers['content-type'] = headers['content-type'] || (this[kApiVersioning] ? 'application/vnd.elasticsearch+x-ndjson; compatible-with=7' : 'application/x-ndjson') + } + } + + params.headers = headers + // serializes the querystring + if (options.querystring == null) { + params.querystring = this.serializer.qserialize(params.querystring) + } else { + params.querystring = this.serializer.qserialize( + Object.assign({}, params.querystring, options.querystring) + ) + } + + // handles request timeout + params.timeout = toMs(options.requestTimeout || this.requestTimeout) + if (options.asStream === true) params.asStream = true + meta.request.params = params + meta.request.options = options + + // handle compression + if (params.body !== '' && params.body != null) { + if (isStream(params.body) === true) { + if (compression === 'gzip') { + params.headers['content-encoding'] = compression + params.body = params.body.pipe(createGzip()) + } + makeRequest() + } else if (compression === 'gzip') { + gzip(params.body, (err, buffer) => { + /* istanbul ignore next */ + if (err) { + this.emit('request', err, result) + return callback(err, result) + } + params.headers['content-encoding'] = compression + params.headers['content-length'] = '' + Buffer.byteLength(buffer) + params.body = buffer + makeRequest() + }) + } else { + params.headers['content-length'] = '' + Buffer.byteLength(params.body) + makeRequest() + } } else { - params.headers['content-length'] = '' + Buffer.byteLength(params.body) makeRequest() } + } + + // still need to check the product or waiting for the check to finish + if (this[kProductCheck] === 0 || this[kProductCheck] === 1) { + // let pass info requests + if (params.method === 'GET' && params.path === '/') { + prepareRequest() + } else { + // wait for product check to finish + productCheckEmitter.once('product-check', status => { + if (status === false) { + const err = new ProductNotSupportedError(result) + this.emit('request', err, result) + process.nextTick(callback, err, result) + } else { + prepareRequest() + } + }) + // the very first request triggers the product check + if (this[kProductCheck] === 0) { + this.productCheck() + } + } + // the product check is finished and it's not Elasticsearch + } else if (this[kProductCheck] === 3) { + const err = new ProductNotSupportedError(result) + this.emit('request', err, result) + process.nextTick(callback, err, result) + // the product check finished and it's Elasticsearch } else { - makeRequest() + prepareRequest() } return transportReturn @@ -494,6 +536,59 @@ class Transport { callback(null, hosts) }) } + + productCheck () { + debug('Start product check') + this[kProductCheck] = 1 + this.request({ + method: 'GET', + path: '/' + }, (err, result) => { + this[kProductCheck] = 3 + if (err) { + debug('Product check failed', err) + if (err.statusCode === 401 || err.statusCode === 403) { + this[kProductCheck] = 2 + process.emitWarning('The client is unable to verify that the server is Elasticsearch due to security privileges on the server side. Some functionality may not be compatible if the server is running an unsupported product.') + productCheckEmitter.emit('product-check', true) + } else { + this[kProductCheck] = 0 + productCheckEmitter.emit('product-check', false) + } + } else { + debug('Checking elasticsearch version', result.body, result.headers) + if (result.body.version == null || typeof result.body.version.number !== 'string') { + debug('Can\'t access Elasticsearch version') + return productCheckEmitter.emit('product-check', false) + } + const tagline = result.body.tagline + const version = result.body.version.number.split('.') + const major = Number(version[0]) + const minor = Number(version[1]) + if (major < 6) { + return productCheckEmitter.emit('product-check', false) + } else if (major >= 6 && major < 7) { + if (tagline !== 'You Know, for Search') { + debug('Bad tagline') + return productCheckEmitter.emit('product-check', false) + } + } else if (major === 7 && minor < 14) { + if (tagline !== 'You Know, for Search' || result.body.version.build_flavor !== 'default') { + debug('Bad tagline or build_flavor') + return productCheckEmitter.emit('product-check', false) + } + } else { + if (result.headers['x-elastic-product'] !== 'Elasticsearch') { + debug('x-elastic-product not recognized') + return productCheckEmitter.emit('product-check', false) + } + } + debug('Valid Elasticsearch distribution') + this[kProductCheck] = 2 + productCheckEmitter.emit('product-check', true) + } + }) + } } Transport.sniffReasons = { diff --git a/lib/errors.d.ts b/lib/errors.d.ts index 12241e486..3ed037fd0 100644 --- a/lib/errors.d.ts +++ b/lib/errors.d.ts @@ -81,3 +81,10 @@ export declare class RequestAbortedError, TConte meta: ApiResponse; constructor(message: string, meta: ApiResponse); } + +export declare class ProductNotSupportedError, TContext = Context> extends ElasticsearchClientError { + name: string; + message: string; + meta: ApiResponse; + constructor(meta: ApiResponse); +} diff --git a/lib/errors.js b/lib/errors.js index a62d45936..2ec9bc715 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -133,6 +133,16 @@ class RequestAbortedError extends ElasticsearchClientError { } } +class ProductNotSupportedError extends ElasticsearchClientError { + constructor (meta) { + super('Product Not Supported Error') + Error.captureStackTrace(this, ProductNotSupportedError) + this.name = 'ProductNotSupportedError' + this.message = 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.' + this.meta = meta + } +} + module.exports = { ElasticsearchClientError, TimeoutError, @@ -142,5 +152,6 @@ module.exports = { DeserializationError, ConfigurationError, ResponseError, - RequestAbortedError + RequestAbortedError, + ProductNotSupportedError } diff --git a/test/acceptance/events-order.test.js b/test/acceptance/events-order.test.js index 0bbd9a49a..335fd4ba8 100644 --- a/test/acceptance/events-order.test.js +++ b/test/acceptance/events-order.test.js @@ -21,7 +21,7 @@ const { test } = require('tap') const intoStream = require('into-stream') -const { Client, Connection, events } = require('../../index') +const { Connection, events } = require('../../index') const { TimeoutError, ConnectionError, @@ -31,6 +31,7 @@ const { DeserializationError } = require('../../lib/errors') const { + Client, buildServer, connection: { MockConnection, diff --git a/test/acceptance/observability.test.js b/test/acceptance/observability.test.js index 2d74b4f32..df889f22c 100644 --- a/test/acceptance/observability.test.js +++ b/test/acceptance/observability.test.js @@ -2,8 +2,9 @@ const { test } = require('tap') const FakeTimers = require('@sinonjs/fake-timers') -const { Client, Transport } = require('../../index') +const { Transport } = require('../../index') const { + Client, connection: { MockConnection, MockConnectionSniff } } = require('../utils') const noop = () => {} diff --git a/test/acceptance/product-check.test.js b/test/acceptance/product-check.test.js new file mode 100644 index 000000000..4b62833c4 --- /dev/null +++ b/test/acceptance/product-check.test.js @@ -0,0 +1,1131 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +'use strict' + +const { test } = require('tap') +const { Client } = require('../../') +const { + connection: { + MockConnectionTimeout, + buildMockConnection + } +} = require('../utils') + +test('No errors v8', t => { + t.plan(7) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) +}) + +test('Errors v8', t => { + t.plan(3) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('No errors ≤v7.13', t => { + t.plan(7) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '7.13.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) +}) + +test('Errors ≤v7.13', t => { + t.plan(3) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '7.13.0-SNAPSHOT', + build_flavor: 'other', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'Other' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('No errors v6', t => { + t.plan(7) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '6.8.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) +}) + +test('Errors v6', t => { + t.plan(3) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '6.8.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'Other' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('Auth error - 401', t => { + t.plan(8) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 401, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + security: 'exception' + } + } + } + }) + + process.on('warning', onWarning) + function onWarning (warning) { + t.equal(warning.message, 'The client is unable to verify that the server is Elasticsearch due to security privileges on the server side. Some functionality may not be compatible if the server is running an unsupported product.') + } + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.statusCode, 401) + process.removeListener('warning', onWarning) + }) +}) + +test('Auth error - 403', t => { + t.plan(8) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 403, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + security: 'exception' + } + } + } + }) + + process.on('warning', onWarning) + function onWarning (warning) { + t.equal(warning.message, 'The client is unable to verify that the server is Elasticsearch due to security privileges on the server side. Some functionality may not be compatible if the server is running an unsupported product.') + } + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.statusCode, 403) + process.removeListener('warning', onWarning) + }) +}) + +test('500 error', t => { + t.plan(8) + + let count = 0 + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const MockConnection = buildMockConnection({ + onRequest (params) { + const req = requests.shift() + t.equal(req.method, params.method) + t.equal(req.path, params.path) + + if (count++ >= 1) { + return { + statusCode: 200, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } else { + return { + statusCode: 500, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + error: 'kaboom' + } + } + } + } + }) + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) + }) +}) + +test('TimeoutError', t => { + t.plan(3) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnectionTimeout, + maxRetries: 0 + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('Multiple subsequent calls, no errors', t => { + t.plan(15) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }, { + method: 'HEAD', + path: '/' + }, { + method: 'POST', + path: '/foo/_doc' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) + + client.ping((err, result) => { + t.error(err) + }) + + client.index({ + index: 'foo', + body: { + foo: 'bar' + } + }, (err, result) => { + t.error(err) + }) +}) + +test('Multiple subsequent calls, with errors', t => { + t.plan(7) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }, { + method: 'HEAD', + path: '/' + }, { + method: 'POST', + path: '/foo/_doc' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) + + client.ping((err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) + + client.index({ + index: 'foo', + body: { + foo: 'bar' + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('Later successful call', t => { + t.plan(11) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) + + setTimeout(() => { + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) + }, 100) +}) + +test('Later errored call', t => { + t.plan(5) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) + + setTimeout(() => { + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) + }, 100) +}) + +test('Errors ≤v5', t => { + t.plan(3) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '5.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('Bad info response', t => { + t.plan(3) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + const req = requests.shift() + if (req.method === 'GET') { + t.error(err) + } else { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + } + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.equal(err.message, 'The client noticed that the server is not Elasticsearch and we do not support this unknown product.') + }) +}) + +test('No multiple checks with child clients', t => { + t.plan(11) + const MockConnection = buildMockConnection({ + onRequest (params) { + return { + statusCode: 200, + headers: { + 'x-elastic-product': 'Elasticsearch' + }, + body: { + name: '1ef419078577', + cluster_name: 'docker-cluster', + cluster_uuid: 'cQ5pAMvRRTyEzObH4L5mTA', + version: { + number: '8.0.0-SNAPSHOT', + build_flavor: 'default', + build_type: 'docker', + build_hash: '5fb4c050958a6b0b6a70a6fb3e616d0e390eaac3', + build_date: '2021-07-10T01:45:02.136546168Z', + build_snapshot: true, + lucene_version: '8.9.0', + minimum_wire_compatibility_version: '7.15.0', + minimum_index_compatibility_version: '7.0.0' + }, + tagline: 'You Know, for Search' + } + } + } + }) + + const requests = [{ + method: 'GET', + path: '/' + }, { + method: 'POST', + path: '/foo/_search' + }, { + method: 'POST', + path: '/foo/_search' + }] + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.on('request', (err, event) => { + t.error(err) + const req = requests.shift() + t.equal(event.meta.request.params.method, req.method) + t.equal(event.meta.request.params.path, req.path) + }) + + client.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) + + setTimeout(() => { + const child = client.child() + child.search({ + index: 'foo', + body: { + query: { + match_all: {} + } + } + }, (err, result) => { + t.error(err) + }) + }, 100) +}) diff --git a/test/acceptance/proxy.test.js b/test/acceptance/proxy.test.js index da54084cb..29dd6b2a3 100644 --- a/test/acceptance/proxy.test.js +++ b/test/acceptance/proxy.test.js @@ -4,8 +4,8 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0 const { test } = require('tap') -const { Client } = require('../../index') const { + Client, buildProxy: { createProxy, createSecureProxy, diff --git a/test/acceptance/resurrect.test.js b/test/acceptance/resurrect.test.js index 432929852..c2e43a6c6 100644 --- a/test/acceptance/resurrect.test.js +++ b/test/acceptance/resurrect.test.js @@ -23,8 +23,8 @@ const { test } = require('tap') const { URL } = require('url') const FakeTimers = require('@sinonjs/fake-timers') const workq = require('workq') -const { buildCluster } = require('../utils') -const { Client, events } = require('../../index') +const { Client, buildCluster } = require('../utils') +const { events } = require('../../index') /** * The aim of this test is to verify how the resurrect logic behaves diff --git a/test/acceptance/sniff.test.js b/test/acceptance/sniff.test.js index ee18c9298..5dfaa3f76 100644 --- a/test/acceptance/sniff.test.js +++ b/test/acceptance/sniff.test.js @@ -23,8 +23,8 @@ const { test } = require('tap') const { URL } = require('url') const FakeTimers = require('@sinonjs/fake-timers') const workq = require('workq') -const { buildCluster } = require('../utils') -const { Client, Connection, Transport, events, errors } = require('../../index') +const { Client, buildCluster } = require('../utils') +const { Connection, Transport, events, errors } = require('../../index') /** * The aim of this test is to verify how the sniffer behaves diff --git a/test/unit/api-async.js b/test/unit/api-async.js index fb0bb1b80..219df5533 100644 --- a/test/unit/api-async.js +++ b/test/unit/api-async.js @@ -19,8 +19,8 @@ 'use strict' -const { Client, errors } = require('../../index') -const { buildServer } = require('../utils') +const { errors } = require('../../index') +const { Client, buildServer } = require('../utils') function runAsyncTest (test) { test('async await (search)', t => { diff --git a/test/unit/api.test.js b/test/unit/api.test.js index 667ec66ab..de5021b66 100644 --- a/test/unit/api.test.js +++ b/test/unit/api.test.js @@ -20,8 +20,8 @@ 'use strict' const { test } = require('tap') -const { Client, errors } = require('../../index') -const { buildServer } = require('../utils') +const { errors } = require('../../index') +const { Client, buildServer } = require('../utils') test('Basic (callback)', t => { t.plan(2) diff --git a/test/unit/child.test.js b/test/unit/child.test.js index 626d8941b..a07a6d135 100644 --- a/test/unit/child.test.js +++ b/test/unit/child.test.js @@ -20,8 +20,9 @@ 'use strict' const { test } = require('tap') -const { Client, errors } = require('../../index') +const { errors } = require('../../index') const { + Client, buildServer, connection: { MockConnection } } = require('../utils') diff --git a/test/unit/client.test.js b/test/unit/client.test.js index 8c65cf21d..d9a26c110 100644 --- a/test/unit/client.test.js +++ b/test/unit/client.test.js @@ -23,9 +23,9 @@ const { test } = require('tap') const { URL } = require('url') const buffer = require('buffer') const intoStream = require('into-stream') -const { Client, ConnectionPool, Transport, Connection, errors } = require('../../index') +const { ConnectionPool, Transport, Connection, errors } = require('../../index') const { CloudConnectionPool } = require('../../lib/pool') -const { buildServer } = require('../utils') +const { Client, buildServer } = require('../utils') let clientVersion = require('../../package.json').version if (clientVersion.includes('-')) { clientVersion = clientVersion.slice(0, clientVersion.indexOf('-')) + 'p' diff --git a/test/unit/events.test.js b/test/unit/events.test.js index 6612213ca..5286a3f40 100644 --- a/test/unit/events.test.js +++ b/test/unit/events.test.js @@ -20,9 +20,10 @@ 'use strict' const { test } = require('tap') -const { Client, events } = require('../../index') +const { events } = require('../../index') const { TimeoutError } = require('../../lib/errors') const { + Client, connection: { MockConnection, MockConnectionTimeout diff --git a/test/unit/helpers/bulk.test.js b/test/unit/helpers/bulk.test.js index 5fda2856f..b8a00a1f0 100644 --- a/test/unit/helpers/bulk.test.js +++ b/test/unit/helpers/bulk.test.js @@ -24,8 +24,8 @@ const { join } = require('path') const split = require('split2') const FakeTimers = require('@sinonjs/fake-timers') const { test } = require('tap') -const { Client, errors } = require('../../../') -const { buildServer, connection } = require('../../utils') +const { errors } = require('../../../') +const { Client, buildServer, connection } = require('../../utils') let clientVersion = require('../../../package.json').version if (clientVersion.includes('-')) { clientVersion = clientVersion.slice(0, clientVersion.indexOf('-')) + 'p' diff --git a/test/unit/helpers/msearch.test.js b/test/unit/helpers/msearch.test.js index b80792fa5..b7a06ac8f 100644 --- a/test/unit/helpers/msearch.test.js +++ b/test/unit/helpers/msearch.test.js @@ -20,8 +20,8 @@ 'use strict' const { test } = require('tap') -const { Client, errors } = require('../../../') -const { connection } = require('../../utils') +const { errors } = require('../../../') +const { Client, connection } = require('../../utils') const FakeTimers = require('@sinonjs/fake-timers') test('Basic', async t => { diff --git a/test/unit/helpers/scroll.test.js b/test/unit/helpers/scroll.test.js index 4943e703b..2a8ea2a4f 100644 --- a/test/unit/helpers/scroll.test.js +++ b/test/unit/helpers/scroll.test.js @@ -20,8 +20,8 @@ 'use strict' const { test } = require('tap') -const { Client, errors } = require('../../../') -const { connection } = require('../../utils') +const { errors } = require('../../../') +const { Client, connection } = require('../../utils') let clientVersion = require('../../../package.json').version if (clientVersion.includes('-')) { clientVersion = clientVersion.slice(0, clientVersion.indexOf('-')) + 'p' diff --git a/test/unit/helpers/search.test.js b/test/unit/helpers/search.test.js index ad01ad35a..bea39bc62 100644 --- a/test/unit/helpers/search.test.js +++ b/test/unit/helpers/search.test.js @@ -20,8 +20,7 @@ 'use strict' const { test } = require('tap') -const { Client } = require('../../../') -const { connection } = require('../../utils') +const { Client, connection } = require('../../utils') test('Search should have an additional documents property', async t => { const MockConnection = connection.buildMockConnection({ diff --git a/test/unit/transport.test.js b/test/unit/transport.test.js index 410fe8995..c617bc3af 100644 --- a/test/unit/transport.test.js +++ b/test/unit/transport.test.js @@ -27,6 +27,7 @@ const os = require('os') const intoStream = require('into-stream') const { buildServer, + skipProductCheck, connection: { MockConnection, MockConnectionTimeout, MockConnectionError } } = require('../utils') const { @@ -65,6 +66,7 @@ test('Basic', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -92,6 +94,7 @@ test('Basic (promises support)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport .request({ @@ -119,6 +122,7 @@ test('Basic - failing (promises support)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport .request({ @@ -145,6 +149,7 @@ test('Basic (options + promises support)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport .request({ @@ -190,6 +195,7 @@ test('Send POST', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -246,6 +252,7 @@ test('Send POST (ndjson)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -289,6 +296,7 @@ test('Send stream', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -332,6 +340,7 @@ test('Send stream (bulkBody)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -365,6 +374,7 @@ test('Not JSON payload from server', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -396,6 +406,7 @@ test('NoLivingConnectionsError (null connection)', t => { return null } }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -424,6 +435,7 @@ test('NoLivingConnectionsError (undefined connection)', t => { return undefined } }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -447,6 +459,7 @@ test('SerializationError', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const body = { hello: 'world' } body.o = body @@ -473,6 +486,7 @@ test('SerializationError (bulk)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const bulkBody = { hello: 'world' } bulkBody.o = bulkBody @@ -505,6 +519,7 @@ test('DeserializationError', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -541,6 +556,7 @@ test('TimeoutError (should call markDead on the failing connection)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -575,6 +591,7 @@ test('ConnectionError (should call markDead on the failing connection)', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -620,6 +637,7 @@ test('Retry mechanism', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -664,6 +682,7 @@ test('Should not retry if the body is a stream', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -709,6 +728,7 @@ test('Should not retry if the bulkBody is a stream', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -754,6 +774,7 @@ test('No retry', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -805,6 +826,7 @@ test('Custom retry mechanism', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -852,6 +874,7 @@ test('Should not retry on 429', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -889,6 +912,7 @@ test('Should call markAlive with a successful response', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -926,6 +950,7 @@ test('Should call resurrect on every request', t => { sniffOnStart: false, name: 'elasticsearch-js' }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -954,6 +979,7 @@ test('Should return a request aborter utility', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const request = transport.request({ method: 'GET', @@ -1002,6 +1028,7 @@ test('Retry mechanism and abort', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const request = transport.request({ method: 'GET', @@ -1031,6 +1058,7 @@ test('Abort a request with the promise API', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const request = transport.request({ method: 'GET', @@ -1070,6 +1098,7 @@ test('ResponseError', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1105,6 +1134,7 @@ test('Override requestTimeout', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1167,6 +1197,7 @@ test('sniff', t => { sniffOnConnectionFault: true, sniffEndpoint: '/sniff' }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1200,6 +1231,7 @@ test('sniff', t => { sniffInterval: 1, sniffEndpoint: '/sniff' }) + skipProductCheck(transport) const params = { method: 'GET', path: '/' } clock.tick(100) @@ -1233,6 +1265,7 @@ test('sniff', t => { sniffInterval: false, sniffEndpoint: '/sniff' }) + skipProductCheck(transport) transport.sniff((err, hosts) => { t.ok(err instanceof ConnectionError) @@ -1269,6 +1302,7 @@ test(`Should mark as dead connections where the statusCode is 502/3/4 sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1323,6 +1357,7 @@ test('Should retry the request if the statusCode is 502/3/4', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1354,6 +1389,7 @@ test('Ignore status code', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1403,6 +1439,7 @@ test('Should serialize the querystring', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1446,6 +1483,7 @@ test('timeout option', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1476,6 +1514,7 @@ test('timeout option', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1512,6 +1551,7 @@ test('timeout option', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1542,6 +1582,7 @@ test('timeout option', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1576,6 +1617,7 @@ test('Should cast to boolean HEAD request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'HEAD', @@ -1601,6 +1643,7 @@ test('Should cast to boolean HEAD request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'HEAD', @@ -1627,6 +1670,7 @@ test('Should cast to boolean HEAD request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'HEAD', @@ -1652,6 +1696,7 @@ test('Should cast to boolean HEAD request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'HEAD', @@ -1694,6 +1739,7 @@ test('Suggest compression', t => { sniffOnStart: false, suggestCompression: true }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1734,6 +1780,7 @@ test('Broken compression', t => { sniffOnStart: false, suggestCompression: true }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1769,6 +1816,7 @@ test('Warning header', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1806,6 +1854,7 @@ test('Warning header', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1840,6 +1889,7 @@ test('Warning header', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1875,6 +1925,7 @@ test('asStream set to true', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -1933,6 +1984,7 @@ test('Compress request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -1981,6 +2033,7 @@ test('Compress request', t => { sniffOnStart: false, compression: 'gzip' }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -2026,6 +2079,7 @@ test('Compress request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -2085,6 +2139,7 @@ test('Compress request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'DELETE', @@ -2151,6 +2206,7 @@ test('Compress request', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'POST', @@ -2195,6 +2251,7 @@ test('Headers configuration', t => { 'x-foo': 'bar' } }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2234,6 +2291,7 @@ test('Headers configuration', t => { 'x-foo': 'bar' } }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2272,6 +2330,7 @@ test('Headers configuration', t => { 'x-foo': 'bar' } }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2312,6 +2371,7 @@ test('nodeFilter and nodeSelector', t => { return conns[0] } }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2345,6 +2405,7 @@ test('Should accept custom querystring in the optons object', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2381,6 +2442,7 @@ test('Should accept custom querystring in the optons object', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2425,6 +2487,7 @@ test('Should add an User-Agent header', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2459,6 +2522,7 @@ test('Should pass request params and options to generateRequestId', t => { return 'id' } }) + skipProductCheck(transport) transport.request(params, options, t.error) }) @@ -2484,6 +2548,7 @@ test('Secure json parsing', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2516,6 +2581,7 @@ test('Secure json parsing', t => { sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) transport.request({ method: 'GET', @@ -2574,6 +2640,7 @@ test('The callback with a sync error should be called in the next tick - json', sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const body = { a: true } body.o = body @@ -2605,6 +2672,7 @@ test('The callback with a sync error should be called in the next tick - ndjson' sniffInterval: false, sniffOnStart: false }) + skipProductCheck(transport) const field = { a: true } field.o = field diff --git a/test/utils/MockConnection.js b/test/utils/MockConnection.js index f714fdd30..0719696af 100644 --- a/test/utils/MockConnection.js +++ b/test/utils/MockConnection.js @@ -133,7 +133,7 @@ function buildMockConnection (opts) { class MockConnection extends Connection { request (params, callback) { - let { body, statusCode } = opts.onRequest(params) + let { body, statusCode, headers } = opts.onRequest(params) if (typeof body !== 'string') { body = JSON.stringify(body) } @@ -144,7 +144,8 @@ function buildMockConnection (opts) { 'content-type': 'application/json;utf=8', date: new Date().toISOString(), connection: 'keep-alive', - 'content-length': Buffer.byteLength(body) + 'content-length': Buffer.byteLength(body), + ...headers } process.nextTick(() => { if (!aborted) { diff --git a/test/utils/index.js b/test/utils/index.js index ac513fde9..1adff9f3b 100644 --- a/test/utils/index.js +++ b/test/utils/index.js @@ -25,6 +25,7 @@ const buildServer = require('./buildServer') const buildCluster = require('./buildCluster') const buildProxy = require('./buildProxy') const connection = require('./MockConnection') +const { Client } = require('../../') async function waitCluster (client, waitForStatus = 'green', timeout = '50s', times = 0) { if (!client) { @@ -41,10 +42,25 @@ async function waitCluster (client, waitForStatus = 'green', timeout = '50s', ti } } +function skipProductCheck (client) { + const tSymbol = Object.getOwnPropertySymbols(client.transport || client) + .filter(symbol => symbol.description === 'product check')[0] + ;(client.transport || client)[tSymbol] = 2 +} + +class NoProductCheckClient extends Client { + constructor (opts) { + super(opts) + skipProductCheck(this) + } +} + module.exports = { buildServer, buildCluster, buildProxy, connection, - waitCluster + waitCluster, + skipProductCheck, + Client: NoProductCheckClient }