Files
elasticsearch-js/test/unit/client.test.ts
github-actions[bot] 61c18a6ba5 [Backport 8.x] Upgrade tap to latest (#2402)
Co-authored-by: Josh Mock <joshua.mock@elastic.co>
2024-10-24 11:38:47 -05:00

529 lines
13 KiB
TypeScript

/*
* 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.
*/
import * as http from 'http'
import { test } from 'tap'
import { URL } from 'url'
import FakeTimers from '@sinonjs/fake-timers'
import { buildServer, connection } from '../utils'
import { Client, errors } from '../..'
import * as symbols from '@elastic/transport/lib/symbols'
import { BaseConnectionPool, CloudConnectionPool, WeightedConnectionPool } from '@elastic/transport'
let clientVersion: string = require('../../package.json').version // eslint-disable-line
if (clientVersion.includes('-')) {
clientVersion = clientVersion.slice(0, clientVersion.indexOf('-')) + 'p'
}
let transportVersion: string = require('@elastic/transport/package.json').version // eslint-disable-line
if (transportVersion.includes('-')) {
transportVersion = transportVersion.slice(0, transportVersion.indexOf('-')) + 'p'
}
const nodeVersion = process.versions.node
test('Create a client instance, single node as string', t => {
const client = new Client({ node: 'http://localhost:9200' })
t.ok(client.connectionPool instanceof WeightedConnectionPool)
t.equal(client.connectionPool.size, 1)
t.end()
})
test('Create a client instance, multi node as strings', t => {
const client = new Client({ nodes: ['http://localhost:9200', 'http://localhost:9201'] })
t.ok(client.connectionPool instanceof WeightedConnectionPool)
t.equal(client.connectionPool.size, 2)
t.end()
})
test('Create a client instance, single node as object', t => {
const client = new Client({
node: {
url: new URL('http://localhost:9200')
}
})
t.equal(client.connectionPool.size, 1)
t.end()
})
test('Create a client instance, multi node as object', t => {
const client = new Client({
nodes: [{
url: new URL('http://localhost:9200')
}, {
url: new URL('http://localhost:9201')
}]
})
t.equal(client.connectionPool.size, 2)
t.end()
})
test('Missing node(s)', t => {
t.throws(() => new Client({}), errors.ConfigurationError)
t.end()
})
test('Custom headers', t => {
const client = new Client({
node: 'http://localhost:9200',
headers: { foo: 'bar' }
})
t.match(client.transport[symbols.kHeaders], { foo: 'bar' })
t.end()
})
test('Basic auth', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { authorization: 'Basic aGVsbG86d29ybGQ=' })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
auth: {
username: 'hello',
password: 'world'
}
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('Basic auth via url', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { authorization: 'Basic aGVsbG86d29ybGQ=' })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://hello:world@localhost:9200',
Connection
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('ApiKey as string', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { authorization: 'ApiKey foobar' })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
auth: {
apiKey: 'foobar'
}
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('ApiKey as object', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { authorization: 'ApiKey Zm9vOmJhcg==' })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
auth: {
apiKey: {
id: 'foo',
api_key: 'bar'
}
}
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('Bearer auth', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { authorization: 'Bearer token' })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
auth: {
bearer: 'token'
}
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('Override authentication per request', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { authorization: 'Basic foobar' })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
auth: {
username: 'hello',
password: 'world'
}
})
await client.transport.request(
{ method: 'GET', path: '/' },
{ headers: { authorization: 'Basic foobar' } }
)
})
test('Custom headers per request', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, {
foo: 'bar',
faz: 'bar'
})
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
headers: { foo: 'bar' }
})
await client.transport.request(
{ method: 'GET', path: '/' },
{ headers: { faz: 'bar' } }
)
})
test('Close the client', async t => {
t.plan(1)
class MyConnectionPool extends BaseConnectionPool {
async empty (): Promise<void> {
t.pass('called')
}
}
const client = new Client({
node: 'http://localhost:9200',
ConnectionPool: MyConnectionPool
})
await client.close()
})
test('Elastic Cloud config', t => {
const client = new Client({
cloud: {
// 'localhost$abcd$'
id: 'name:bG9jYWxob3N0JGFiY2Qk'
},
auth: {
username: 'elastic',
password: 'changeme'
}
})
t.ok(client.connectionPool instanceof CloudConnectionPool)
const connection = client.connectionPool.connections.find(c => c.id === 'https://abcd.localhost/')
t.equal(connection?.headers?.authorization, `Basic ${Buffer.from('elastic:changeme').toString('base64')}`)
t.same(connection?.tls, { secureProtocol: 'TLSv1_2_method' })
t.equal(connection?.url.hostname, 'abcd.localhost')
t.equal(connection?.url.protocol, 'https:')
t.end()
})
test('Override default Elastic Cloud options', t => {
const client = new Client({
cloud: {
// 'localhost$abcd$efgh'
id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==',
},
auth: {
username: 'elastic',
password: 'changeme'
},
compression: false,
tls: {
secureProtocol: 'TLSv1_1_method'
}
})
t.ok(client.connectionPool instanceof CloudConnectionPool)
t.equal(client.transport[symbols.kCompression], false)
t.same(client.connectionPool._tls, { secureProtocol: 'TLSv1_1_method' })
t.end()
})
test('Configure opaqueIdPrefix', t => {
const client = new Client({
node: 'http://localhost:9200',
opaqueIdPrefix: 'foobar'
})
t.equal(client.transport[symbols.kOpaqueIdPrefix], 'foobar')
t.end()
})
test('name as string', t => {
const client = new Client({
node: 'http://localhost:9200',
name: 'es-client'
})
t.equal(client.name, 'es-client')
t.end()
})
test('name as symbol', t => {
const s = Symbol()
const client = new Client({
node: 'http://localhost:9200',
name: s
})
t.equal(client.name, s)
t.end()
})
test('Meta header enabled by default', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.match(opts.headers, { 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${transportVersion},hc=${nodeVersion}` })
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('Meta header disabled', async t => {
t.plan(1)
const Connection = connection.buildMockConnection({
onRequest (opts) {
t.notOk(opts.headers?.['x-elastic-client-meta'])
return {
statusCode: 200,
body: { hello: 'world' }
}
}
})
const client = new Client({
node: 'http://localhost:9200',
Connection,
enableMetaHeader: false
})
await client.transport.request({ method: 'GET', path: '/' })
})
test('caFingerprint', t => {
const client = new Client({
node: 'https://localhost:9200',
caFingerprint: 'FO:OB:AR'
})
t.equal(client.connectionPool[symbols.kCaFingerprint], 'FO:OB:AR')
t.end()
})
test('caFingerprint can\'t be configured over http / 1', t => {
t.throws(() => new Client({
node: 'http://localhost:9200',
caFingerprint: 'FO:OB:AR'
}),
errors.ConfigurationError
)
t.end()
})
test('caFingerprint can\'t be configured over http / 2', t => {
t.throws(() => new Client({
nodes: ['http://localhost:9200'],
caFingerprint: 'FO:OB:AR'
}),
errors.ConfigurationError
)
t.end()
})
test('user agent is in the correct format', t => {
const client = new Client({ node: 'http://localhost:9200' })
const agentRaw = client.transport[symbols.kHeaders]['user-agent'] || ''
const agentSplit = agentRaw.split(/\s+/)
t.equal(agentSplit[0].split('/')[0], 'elasticsearch-js')
t.ok(/^\d+\.\d+\.\d+/.test(agentSplit[0].split('/')[1]))
t.end()
})
test('Ensure new client instance stores requestTimeout for each connection', t => {
const client = new Client({
node: { url: new URL('http://localhost:9200') },
requestTimeout: 60000,
})
t.equal(client.connectionPool.connections[0].timeout, 60000)
t.end()
})
test('Ensure new client does not time out at default (30s) when client sets requestTimeout', async t => {
const clock = FakeTimers.install({ toFake: ['setTimeout', 'clearTimeout'] })
t.teardown(() => clock.uninstall())
function handler (_req: http.IncomingMessage, res: http.ServerResponse) {
setTimeout(() => {
t.pass('timeout ended')
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ success: true }))
}, 31000) // default is 30000
clock.runToLast()
}
const [{ port }, server] = await buildServer(handler)
const client = new Client({
node: `http://localhost:${port}`,
requestTimeout: 60000
})
try {
await client.transport.request({ method: 'GET', path: '/' })
} catch (error) {
t.fail('timeout error hit')
} finally {
server.stop()
t.end()
}
})
test('Pass disablePrototypePoisoningProtection option to serializer', async t => {
let client = new Client({
node: 'http://localhost:9200',
disablePrototypePoisoningProtection: false
})
t.same(client.serializer[symbols.kJsonOptions], {
protoAction: 'error',
constructorAction: 'error'
})
client = new Client({
node: 'http://localhost:9200',
disablePrototypePoisoningProtection: true
})
t.same(client.serializer[symbols.kJsonOptions], {
protoAction: 'ignore',
constructorAction: 'ignore'
})
client = new Client({
node: 'http://localhost:9200',
disablePrototypePoisoningProtection: 'proto'
})
t.same(client.serializer[symbols.kJsonOptions], {
protoAction: 'error',
constructorAction: 'ignore'
})
client = new Client({
node: 'http://localhost:9200',
disablePrototypePoisoningProtection: 'constructor'
})
t.same(client.serializer[symbols.kJsonOptions], {
protoAction: 'ignore',
constructorAction: 'error'
})
})
test('disablePrototypePoisoningProtection is true by default', async t => {
const client = new Client({ node: 'http://localhost:9200' })
t.same(client.serializer[symbols.kJsonOptions], {
protoAction: 'ignore',
constructorAction: 'ignore'
})
})