From a12418f489673b558c7ddac180ef6e50adfddb2b Mon Sep 17 00:00:00 2001
From: Su <szhongna@amazon.com>
Date: Tue, 7 Mar 2023 17:42:36 -0800
Subject: [PATCH 1/3] - Refactor dev tool console to use opensearch-js client
 to interact with OpenSearch - Update tests

Signed-off-by: Su <szhongna@amazon.com>
---
 CHANGELOG.md                                  |   7 +-
 config/opensearch_dashboards.yml              |   2 +-
 .../api/console/proxy/create_handler.ts       | 174 +++++-------------
 .../api/console/proxy/tests/body.test.ts      |  41 ++---
 .../api/console/proxy/tests/headers.test.ts   |  31 ++--
 .../api/console/proxy/tests/params.test.ts    |  28 ++-
 .../proxy/tests/proxy_fallback.test.ts        |  77 --------
 .../console/proxy/tests/query_string.test.ts  |  32 ++--
 8 files changed, 123 insertions(+), 269 deletions(-)
 delete mode 100644 src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8096613fb702..85b727573672 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -62,12 +62,13 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 - [Optimizer] Increase timeout waiting for the exiting of an optimizer worker ([#3193](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3193))
 - [Data] Update `createAggConfig` so that newly created configs can be added to beginning of `aggConfig` array ([#3160](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3160))
 - Add disablePrototypePoisoningProtection configuration to prevent JS client from erroring when cluster utilizes JS reserved words ([#2992](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2992))
-- [Multiple DataSource] Add support for SigV4 authentication ([#3058](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3058))
-- Make build scripts find and use the latest version of Node.js that satisfies `engines.node` ([#3467](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3467))
-- [Multiple DataSource] Refactor test connection to support SigV4 auth type ([#3456](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3456))
+- [Multiple DataSource] Add support for SigV4 authentication ([#3058](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3058))
+- Make build scripts find and use the latest version of Node.js that satisfies `engines.node` ([#3467](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3467))
+- [Multiple DataSource] Refactor test connection to support SigV4 auth type ([#3456](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3456))
 - [Darwin] Add support for Darwin for running OpenSearch snapshots with `yarn opensearch snapshot` ([#3537](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3537))
 - [Vis Builder] Add metric to metric, bucket to bucket aggregation persistence ([#3495](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3495))
 - Use mirrors to download Node.js binaries to escape sporadic 404 errors ([#3619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3619))
+- [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544))
 
 ### 🐛 Bug Fixes
 
diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml
index 6bb4f63bbbde..1c1c5b100013 100644
--- a/config/opensearch_dashboards.yml
+++ b/config/opensearch_dashboards.yml
@@ -231,7 +231,7 @@
 
 # Set the value of this setting to true to enable the experimental multiple data source
 # support feature. Use with caution.
-#data_source.enabled: false
+data_source.enabled: true
 # Set the value of these settings to customize crypto materials to encryption saved credentials
 # in data sources.
 #data_source.encryption.wrappingKeyName: 'changeme'
diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
index 9f564d9c8b81..4209e0a2faca 100644
--- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
@@ -28,22 +28,12 @@
  * under the License.
  */
 
-import { Agent, IncomingMessage } from 'http';
-import * as url from 'url';
-import { pick, trimStart, trimEnd } from 'lodash';
-
 import { OpenSearchDashboardsRequest, RequestHandler } from 'opensearch-dashboards/server';
+import { trimStart } from 'lodash';
 
-import { OpenSearchConfigForProxy } from '../../../../types';
-import {
-  getOpenSearchProxyConfig,
-  ProxyConfigCollection,
-  proxyRequest,
-  setHeaders,
-} from '../../../../lib';
+import { ResponseError } from '@opensearch-project/opensearch/lib/errors';
+import { ApiResponse } from '@opensearch-project/opensearch/';
 
-// TODO: find a better way to get information from the request like remoteAddress and remotePort
-// for forwarding.
 // eslint-disable-next-line @osd/eslint/no-restricted-paths
 import { ensureRawRequest } from '../../../../../../../core/server/http/router';
 
@@ -51,54 +41,6 @@ import { RouteDependencies } from '../../../';
 
 import { Body, Query } from './validation_config';
 
-function toURL(base: string, path: string) {
-  const urlResult = new url.URL(`${trimEnd(base, '/')}/${trimStart(path, '/')}`);
-  // Appending pretty here to have OpenSearch do the JSON formatting, as doing
-  // in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of
-  // measurement precision)
-  if (!urlResult.searchParams.get('pretty')) {
-    urlResult.searchParams.append('pretty', 'true');
-  }
-  return urlResult;
-}
-
-function filterHeaders(originalHeaders: object, headersToKeep: string[]): object {
-  const normalizeHeader = function (header: any) {
-    if (!header) {
-      return '';
-    }
-    header = header.toString();
-    return header.trim().toLowerCase();
-  };
-
-  // Normalize list of headers we want to allow in upstream request
-  const headersToKeepNormalized = headersToKeep.map(normalizeHeader);
-
-  return pick(originalHeaders, headersToKeepNormalized);
-}
-
-function getRequestConfig(
-  headers: object,
-  opensearchConfig: OpenSearchConfigForProxy,
-  proxyConfigCollection: ProxyConfigCollection,
-  uri: string
-): { agent: Agent; timeout: number; headers: object; rejectUnauthorized?: boolean } {
-  const filteredHeaders = filterHeaders(headers, opensearchConfig.requestHeadersWhitelist);
-  const newHeaders = setHeaders(filteredHeaders, opensearchConfig.customHeaders);
-
-  if (proxyConfigCollection.hasConfig()) {
-    return {
-      ...proxyConfigCollection.configForUri(uri),
-      headers: newHeaders,
-    } as any;
-  }
-
-  return {
-    ...getOpenSearchProxyConfig(opensearchConfig),
-    headers: newHeaders,
-  };
-}
-
 function getProxyHeaders(req: OpenSearchDashboardsRequest) {
   const headers = Object.create(null);
 
@@ -124,6 +66,17 @@ function getProxyHeaders(req: OpenSearchDashboardsRequest) {
   return headers;
 }
 
+function toUrlPath(path: string) {
+  const urlPath = `/${trimStart(path, '/')}`;
+  // Appending pretty here to have OpenSearch do the JSON formatting, as doing
+  // in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of
+  // measurement precision)
+  if (!urlPath.includes('?pretty')) {
+    urlPath.concat('?pretty=true');
+  }
+  return urlPath;
+}
+
 export const createHandler = ({
   log,
   proxy: { readLegacyOpenSearchConfig, pathFilters, proxyConfigCollection },
@@ -131,6 +84,10 @@ export const createHandler = ({
   const { body, query } = request;
   const { path, method } = query;
 
+  let opensearchResponse: ApiResponse;
+
+  const client = ctx.core.opensearch.client.asCurrentUser;
+
   if (!pathFilters.some((re) => re.test(path))) {
     return response.forbidden({
       body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
@@ -140,77 +97,46 @@ export const createHandler = ({
     });
   }
 
-  const legacyConfig = await readLegacyOpenSearchConfig();
-  const { hosts } = legacyConfig;
-  let opensearchIncomingMessage: IncomingMessage;
-
-  for (let idx = 0; idx < hosts.length; ++idx) {
-    const host = hosts[idx];
-    try {
-      const uri = toURL(host, path);
-
-      // Because this can technically be provided by a settings-defined proxy config, we need to
-      // preserve these property names to maintain BWC.
-      const { timeout, agent, headers, rejectUnauthorized } = getRequestConfig(
-        request.headers,
-        legacyConfig,
-        proxyConfigCollection,
-        uri.toString()
-      );
-
-      const requestHeaders = {
-        ...headers,
-        ...getProxyHeaders(request),
-      };
-
-      opensearchIncomingMessage = await proxyRequest({
-        method: method.toLowerCase() as any,
-        headers: requestHeaders,
-        uri,
-        timeout,
-        payload: body,
-        rejectUnauthorized,
-        agent,
+  try {
+    const requestHeaders = {
+      ...getProxyHeaders(request),
+    };
+
+    opensearchResponse = await client.transport.request(
+      { path: toUrlPath(path), method, body },
+      { headers: requestHeaders }
+    );
+
+    const { statusCode, body: responseContent, warnings } = opensearchResponse;
+
+    if (method.toUpperCase() !== 'HEAD') {
+      return response.custom({
+        statusCode: statusCode!,
+        body: responseContent,
+        headers: {
+          warning: warnings || '',
+        },
       });
-
-      break;
-    } catch (e) {
-      // If we reached here it means we hit a lower level network issue than just, for e.g., a 500.
-      // We try contacting another node in that case.
-      log.error(e);
-      if (idx === hosts.length - 1) {
-        log.warn(`Could not connect to any configured OpenSearch node [${hosts.join(', ')}]`);
-        return response.customError({
-          statusCode: 502,
-          body: e,
-        });
-      }
-      // Otherwise, try the next host...
     }
-  }
-
-  const {
-    statusCode,
-    statusMessage,
-    headers: { warning },
-  } = opensearchIncomingMessage!;
 
-  if (method.toUpperCase() !== 'HEAD') {
     return response.custom({
       statusCode: statusCode!,
-      body: opensearchIncomingMessage!,
+      body: `${statusCode} - ${responseContent}`,
       headers: {
-        warning: warning || '',
+        warning: warnings || '',
+        'Content-Type': 'text/plain',
       },
     });
+  } catch (e: any) {
+    log.error(e);
+    const isResponseErrorFlag = isResponseError(e);
+    return response.customError({
+      statusCode: isResponseErrorFlag ? e.statusCode : 502,
+      body: isResponseErrorFlag ? JSON.stringify(e.meta.body) : `502.${e.statusCode || 0}`,
+    });
   }
+};
 
-  return response.custom({
-    statusCode: statusCode!,
-    body: `${statusCode} - ${statusMessage}`,
-    headers: {
-      warning: warning || '',
-      'Content-Type': 'text/plain',
-    },
-  });
+const isResponseError = (error: any): error is ResponseError => {
+  return Boolean(error && error.body && error.statusCode && error.header);
 };
diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts
index 92349984dade..a7fb88a8bdaa 100644
--- a/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/tests/body.test.ts
@@ -31,23 +31,31 @@
 import { getProxyRouteHandlerDeps } from './mocks';
 
 import expect from '@osd/expect';
-import { Readable } from 'stream';
 
-import { opensearchDashboardsResponseFactory } from '../../../../../../../../core/server';
+import {
+  IScopedClusterClient,
+  opensearchDashboardsResponseFactory,
+} from '../../../../../../../../core/server';
 import { createHandler } from '../create_handler';
-import * as requestModule from '../../../../../lib/proxy_request';
-import { createResponseStub } from './stubs';
+
+import { coreMock, opensearchServiceMock } from '../../../../../../../../core/server/mocks';
 
 describe('Console Proxy Route', () => {
   let request: any;
+  let opensearchClient: DeeplyMockedKeys<IScopedClusterClient>;
 
   beforeEach(() => {
     request = (method: string, path: string, response: string) => {
-      (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub(response));
+      const mockResponse = opensearchServiceMock.createSuccessTransportRequestPromise(response);
+
+      const requestHandlerContextMock = coreMock.createRequestHandlerContext();
+      opensearchClient = requestHandlerContextMock.opensearch.client;
+
+      opensearchClient.asCurrentUser.transport.request.mockResolvedValueOnce(mockResponse);
       const handler = createHandler(getProxyRouteHandlerDeps({}));
 
       return handler(
-        {} as any,
+        { core: requestHandlerContextMock, dataSource: {} as any },
         {
           headers: {},
           query: { method, path },
@@ -57,15 +65,6 @@ describe('Console Proxy Route', () => {
     };
   });
 
-  const readStream = (s: Readable) =>
-    new Promise((resolve) => {
-      let v = '';
-      s.on('data', (data) => {
-        v += data;
-      });
-      s.on('end', () => resolve(v));
-    });
-
   afterEach(async () => {
     jest.resetAllMocks();
   });
@@ -74,36 +73,36 @@ describe('Console Proxy Route', () => {
     describe('GET request', () => {
       it('returns the exact body', async () => {
         const { payload } = await request('GET', '/', 'foobar');
-        expect(await readStream(payload)).to.be('foobar');
+        expect(payload).to.be('foobar');
       });
     });
     describe('POST request', () => {
       it('returns the exact body', async () => {
         const { payload } = await request('POST', '/', 'foobar');
-        expect(await readStream(payload)).to.be('foobar');
+        expect(payload).to.be('foobar');
       });
     });
     describe('PUT request', () => {
       it('returns the exact body', async () => {
         const { payload } = await request('PUT', '/', 'foobar');
-        expect(await readStream(payload)).to.be('foobar');
+        expect(payload).to.be('foobar');
       });
     });
     describe('DELETE request', () => {
       it('returns the exact body', async () => {
         const { payload } = await request('DELETE', '/', 'foobar');
-        expect(await readStream(payload)).to.be('foobar');
+        expect(payload).to.be('foobar');
       });
     });
     describe('HEAD request', () => {
       it('returns the status code and text', async () => {
-        const { payload } = await request('HEAD', '/');
+        const { payload } = await request('HEAD', '/', 'OK');
         expect(typeof payload).to.be('string');
         expect(payload).to.be('200 - OK');
       });
       describe('mixed casing', () => {
         it('returns the status code and text', async () => {
-          const { payload } = await request('HeAd', '/');
+          const { payload } = await request('HeAd', '/', 'OK');
           expect(typeof payload).to.be('string');
           expect(payload).to.be('200 - OK');
         });
diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts
index 488d52a4fc8b..a1964d160e2c 100644
--- a/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/tests/headers.test.ts
@@ -32,25 +32,21 @@ jest.mock('../../../../../../../../core/server/http/router/request', () => ({
   ensureRawRequest: jest.fn(),
 }));
 
-import { opensearchDashboardsResponseFactory } from '../../../../../../../../core/server';
-
+import {
+  IScopedClusterClient,
+  opensearchDashboardsResponseFactory,
+} from '../../../../../../../../core/server';
 // eslint-disable-next-line @osd/eslint/no-restricted-paths
 import { ensureRawRequest } from '../../../../../../../../core/server/http/router/request';
-
 import { getProxyRouteHandlerDeps } from './mocks';
-
 import expect from '@osd/expect';
-import * as requestModule from '../../../../../lib/proxy_request';
-
 import { createHandler } from '../create_handler';
-
-import { createResponseStub } from './stubs';
+import { coreMock } from '../../../../../../../../core/server/mocks';
 
 describe('Console Proxy Route', () => {
   let handler: ReturnType<typeof createHandler>;
 
   beforeEach(() => {
-    (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub(''));
     handler = createHandler(getProxyRouteHandlerDeps({}));
   });
 
@@ -59,7 +55,10 @@ describe('Console Proxy Route', () => {
   });
 
   describe('headers', () => {
+    let opensearchClient: DeeplyMockedKeys<IScopedClusterClient>;
     it('forwards the remote header info', async () => {
+      const requestHandlerContextMock = coreMock.createRequestHandlerContext();
+      opensearchClient = requestHandlerContextMock.opensearch.client;
       (ensureRawRequest as jest.Mock).mockReturnValue({
         // This mocks the shape of the hapi request object, will probably change
         info: {
@@ -75,7 +74,7 @@ describe('Console Proxy Route', () => {
       });
 
       await handler(
-        {} as any,
+        { core: requestHandlerContextMock, dataSource: {} as any },
         {
           headers: {},
           query: {
@@ -86,16 +85,16 @@ describe('Console Proxy Route', () => {
         opensearchDashboardsResponseFactory
       );
 
-      expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1);
-      const [[{ headers }]] = (requestModule.proxyRequest as jest.Mock).mock.calls;
+      const [[, opts]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
+      const headers = opts?.headers;
       expect(headers).to.have.property('x-forwarded-for');
-      expect(headers['x-forwarded-for']).to.be('0.0.0.0');
+      expect(headers!['x-forwarded-for']).to.be('0.0.0.0');
       expect(headers).to.have.property('x-forwarded-port');
-      expect(headers['x-forwarded-port']).to.be('1234');
+      expect(headers!['x-forwarded-port']).to.be('1234');
       expect(headers).to.have.property('x-forwarded-proto');
-      expect(headers['x-forwarded-proto']).to.be('http');
+      expect(headers!['x-forwarded-proto']).to.be('http');
       expect(headers).to.have.property('x-forwarded-host');
-      expect(headers['x-forwarded-host']).to.be('test');
+      expect(headers!['x-forwarded-host']).to.be('test');
     });
   });
 });
diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts
index da6b83f54dab..80523c8031df 100644
--- a/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/tests/params.test.ts
@@ -28,16 +28,24 @@
  * under the License.
  */
 
-import { opensearchDashboardsResponseFactory } from '../../../../../../../../core/server';
+import {
+  IScopedClusterClient,
+  opensearchDashboardsResponseFactory,
+} from '../../../../../../../../core/server';
 import { getProxyRouteHandlerDeps } from './mocks';
-import { createResponseStub } from './stubs';
-import * as requestModule from '../../../../../lib/proxy_request';
 import expect from '@osd/expect';
 
 import { createHandler } from '../create_handler';
+import { coreMock, opensearchServiceMock } from '../../../../../../../../core/server/mocks';
 
 describe('Console Proxy Route', () => {
   let handler: ReturnType<typeof createHandler>;
+  let requestHandlerContextMock: any;
+  let opensearchClient: DeeplyMockedKeys<IScopedClusterClient>;
+  beforeEach(() => {
+    requestHandlerContextMock = coreMock.createRequestHandlerContext();
+    opensearchClient = requestHandlerContextMock.opensearch.client;
+  });
 
   afterEach(() => {
     jest.resetAllMocks();
@@ -52,7 +60,7 @@ describe('Console Proxy Route', () => {
           );
 
           const { status } = await handler(
-            {} as any,
+            { core: requestHandlerContextMock, dataSource: {} as any },
             { query: { method: 'POST', path: '/baz/id' } } as any,
             opensearchDashboardsResponseFactory
           );
@@ -66,16 +74,16 @@ describe('Console Proxy Route', () => {
             getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
           );
 
-          (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+          const mockResponse = opensearchServiceMock.createSuccessTransportRequestPromise('foo');
+          opensearchClient.asCurrentUser.transport.request.mockResolvedValueOnce(mockResponse);
 
           const { status } = await handler(
-            {} as any,
+            { core: requestHandlerContextMock, dataSource: {} as any },
             { headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
             opensearchDashboardsResponseFactory
           );
 
           expect(status).to.be(200);
-          expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1);
         });
       });
       describe('all match', () => {
@@ -84,16 +92,16 @@ describe('Console Proxy Route', () => {
             getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
           );
 
-          (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+          const mockResponse = opensearchServiceMock.createSuccessTransportRequestPromise('foo');
+          opensearchClient.asCurrentUser.transport.request.mockResolvedValueOnce(mockResponse);
 
           const { status } = await handler(
-            {} as any,
+            { core: requestHandlerContextMock, dataSource: {} as any },
             { headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
             opensearchDashboardsResponseFactory
           );
 
           expect(status).to.be(200);
-          expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1);
         });
       });
     });
diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts
deleted file mode 100644
index 585f76066fad..000000000000
--- a/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * SPDX-License-Identifier: Apache-2.0
- *
- * The OpenSearch Contributors require contributions made to
- * this file be licensed under the Apache-2.0 license or a
- * compatible open source license.
- *
- * Any modifications Copyright OpenSearch Contributors. See
- * GitHub history for details.
- */
-
-/*
- * 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 { duration } from 'moment';
-import { getProxyRouteHandlerDeps } from './mocks';
-
-import { opensearchDashboardsResponseFactory } from '../../../../../../../../core/server';
-import * as requestModule from '../../../../../lib/proxy_request';
-import { createHandler } from '../create_handler';
-
-describe('Console Proxy Route', () => {
-  afterEach(async () => {
-    jest.resetAllMocks();
-  });
-
-  describe('fallback behaviour', () => {
-    it('falls back to all configured endpoints regardless of error', async () => {
-      // Describe a situation where all three configured nodes reject
-      (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('ECONNREFUSED'));
-      (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('EHOSTUNREACH'));
-      (requestModule.proxyRequest as jest.Mock).mockRejectedValueOnce(new Error('ESOCKETTIMEDOUT'));
-
-      const handler = createHandler(
-        getProxyRouteHandlerDeps({
-          proxy: {
-            readLegacyOpenSearchConfig: async () => ({
-              requestTimeout: duration(30000),
-              customHeaders: {},
-              requestHeadersWhitelist: [],
-              hosts: ['http://localhost:9201', 'http://localhost:9202', 'http://localhost:9203'],
-            }),
-          },
-        })
-      );
-
-      const response = await handler(
-        {} as any,
-        {
-          headers: {},
-          query: { method: 'get', path: 'test' },
-        } as any,
-        opensearchDashboardsResponseFactory
-      );
-
-      expect(response.status).toBe(502);
-      // Return the message from the OpenSearch node we attempted last.
-      expect(response.payload.message).toBe('ESOCKETTIMEDOUT');
-    });
-  });
-});
diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts
index 9b73457f3f89..6c5155c8f6e4 100644
--- a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts
@@ -28,25 +28,26 @@
  * under the License.
  */
 
-import { opensearchDashboardsResponseFactory } from '../../../../../../../../core/server';
+import {
+  IScopedClusterClient,
+  opensearchDashboardsResponseFactory,
+} from '../../../../../../../../core/server';
 import { getProxyRouteHandlerDeps } from './mocks';
-import { createResponseStub } from './stubs';
-import * as requestModule from '../../../../../lib/proxy_request';
-
-import expect from '@osd/expect';
-
 import { createHandler } from '../create_handler';
+import { coreMock } from '../../../../../../../../core/server/mocks';
 
 describe('Console Proxy Route', () => {
   let request: any;
+  let opensearchClient: DeeplyMockedKeys<IScopedClusterClient>;
   beforeEach(() => {
-    (requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
+    const requestHandlerContextMock = coreMock.createRequestHandlerContext();
+    opensearchClient = requestHandlerContextMock.opensearch.client;
 
     request = async (method: string, path: string) => {
       const handler = createHandler(getProxyRouteHandlerDeps({}));
 
       return handler(
-        {} as any,
+        { core: requestHandlerContextMock, dataSource: {} as any },
         { headers: {}, query: { method, path } } as any,
         opensearchDashboardsResponseFactory
       );
@@ -62,25 +63,22 @@ describe('Console Proxy Route', () => {
       describe('contains full url', () => {
         it('treats the url as a path', async () => {
           await request('GET', 'http://evil.com/test');
-          expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1);
-          const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls;
-          expect(args.uri.href).to.be('http://localhost:9200/http://evil.com/test?pretty=true');
+          const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
+          expect(args.path).toBe('http://evil.com/test?pretty=true');
         });
       });
       describe('starts with a slash', () => {
         it('combines well with the base url', async () => {
           await request('GET', '/index/id');
-          expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1);
-          const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls;
-          expect(args.uri.href).to.be('http://localhost:9200/index/id?pretty=true');
+          const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
+          expect(args.path).toBe('/index/id?pretty=true');
         });
       });
       describe(`doesn't start with a slash`, () => {
         it('combines well with the base url', async () => {
           await request('GET', 'index/id');
-          expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).to.be(1);
-          const [[args]] = (requestModule.proxyRequest as jest.Mock).mock.calls;
-          expect(args.uri.href).to.be('http://localhost:9200/index/id?pretty=true');
+          const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
+          expect(args.path).toBe('index/id?pretty=true');
         });
       });
     });

From 7422ab785f6bd2c648a4d40b2d3406bd87f3132e Mon Sep 17 00:00:00 2001
From: Su <szhongna@amazon.com>
Date: Tue, 14 Mar 2023 16:41:14 -0700
Subject: [PATCH 2/3] reset feature switch

Signed-off-by: Su <szhongna@amazon.com>
---
 config/opensearch_dashboards.yml                          | 2 +-
 .../server/routes/api/console/proxy/create_handler.ts     | 4 ++--
 .../routes/api/console/proxy/tests/query_string.test.ts   | 8 ++++----
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml
index 1c1c5b100013..6bb4f63bbbde 100644
--- a/config/opensearch_dashboards.yml
+++ b/config/opensearch_dashboards.yml
@@ -231,7 +231,7 @@
 
 # Set the value of this setting to true to enable the experimental multiple data source
 # support feature. Use with caution.
-data_source.enabled: true
+#data_source.enabled: false
 # Set the value of these settings to customize crypto materials to encryption saved credentials
 # in data sources.
 #data_source.encryption.wrappingKeyName: 'changeme'
diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
index 4209e0a2faca..5940581b246a 100644
--- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
@@ -67,12 +67,12 @@ function getProxyHeaders(req: OpenSearchDashboardsRequest) {
 }
 
 function toUrlPath(path: string) {
-  const urlPath = `/${trimStart(path, '/')}`;
+  let urlPath = `/${trimStart(path, '/')}`;
   // Appending pretty here to have OpenSearch do the JSON formatting, as doing
   // in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of
   // measurement precision)
   if (!urlPath.includes('?pretty')) {
-    urlPath.concat('?pretty=true');
+    urlPath += '?pretty=true';
   }
   return urlPath;
 }
diff --git a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts b/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts
index 6c5155c8f6e4..4b4c412d4cd2 100644
--- a/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/tests/query_string.test.ts
@@ -64,21 +64,21 @@ describe('Console Proxy Route', () => {
         it('treats the url as a path', async () => {
           await request('GET', 'http://evil.com/test');
           const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
-          expect(args.path).toBe('http://evil.com/test?pretty=true');
+          expect(args.path).toBe('/http://evil.com/test?pretty=true');
         });
       });
       describe('starts with a slash', () => {
-        it('combines well with the base url', async () => {
+        it('keeps as it is', async () => {
           await request('GET', '/index/id');
           const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
           expect(args.path).toBe('/index/id?pretty=true');
         });
       });
       describe(`doesn't start with a slash`, () => {
-        it('combines well with the base url', async () => {
+        it('adds slash to path before sending request', async () => {
           await request('GET', 'index/id');
           const [[args]] = opensearchClient.asCurrentUser.transport.request.mock.calls;
-          expect(args.path).toBe('index/id?pretty=true');
+          expect(args.path).toBe('/index/id?pretty=true');
         });
       });
     });

From 98428743d3aa306e4d05c86c9bb7823658c07138 Mon Sep 17 00:00:00 2001
From: Su <szhongna@amazon.com>
Date: Mon, 20 Mar 2023 16:22:14 -0700
Subject: [PATCH 3/3] address comment

Signed-off-by: Su <szhongna@amazon.com>
---
 .../console/server/routes/api/console/proxy/create_handler.ts  | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
index 5940581b246a..2ed943f4576a 100644
--- a/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
+++ b/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
@@ -83,11 +83,10 @@ export const createHandler = ({
 }: RouteDependencies): RequestHandler<unknown, Query, Body> => async (ctx, request, response) => {
   const { body, query } = request;
   const { path, method } = query;
+  const client = ctx.core.opensearch.client.asCurrentUser;
 
   let opensearchResponse: ApiResponse;
 
-  const client = ctx.core.opensearch.client.asCurrentUser;
-
   if (!pathFilters.some((re) => re.test(path))) {
     return response.forbidden({
       body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,