From 4b452e4f3f0a44b30b18a1818dd985e0f5c6cd4a Mon Sep 17 00:00:00 2001 From: David Jimenez Barrantes Date: Mon, 25 Sep 2023 09:46:38 -0600 Subject: [PATCH] [RND-642] Adds QUERY_FAILURE_CONNECTION_ERROR as a QueryResult response (#299) * Adds QUERY_FAILURE_CONNECTION_ERROR as a QueryResult response * Error types. * QUERY_FAILURE_INVALID_QUERY should return 500 and not 502 --- .../src/repository/ElasticSearchException.ts | 12 +-- .../test/QueryElasticsearch.test.ts | 80 ++++++++++++++++++- .../src/repository/OpenSearchException.ts | 16 ++-- .../test/QueryOpensearch.test.ts | 38 ++++++++- .../meadowlark-core/src/handler/Query.ts | 13 ++- .../src/message/QueryResult.ts | 3 +- .../test/handler/Query.test.ts | 66 +++++++++++++++ 7 files changed, 212 insertions(+), 16 deletions(-) diff --git a/Meadowlark-js/backends/meadowlark-elasticsearch-backend/src/repository/ElasticSearchException.ts b/Meadowlark-js/backends/meadowlark-elasticsearch-backend/src/repository/ElasticSearchException.ts index 7b6284e0..70164fe5 100644 --- a/Meadowlark-js/backends/meadowlark-elasticsearch-backend/src/repository/ElasticSearchException.ts +++ b/Meadowlark-js/backends/meadowlark-elasticsearch-backend/src/repository/ElasticSearchException.ts @@ -22,12 +22,7 @@ export async function handleElasticSearchError( switch (elasticSearchClientError.name) { case 'ConfigurationError': case 'ConnectionError': - case 'DeserializationError': - case 'NoLivingConnectionsError': - case 'NotCompatibleError': - case 'ElasticsearchClientError': case 'RequestAbortedError': - case 'SerializationError': case 'TimeoutError': { if (elasticSearchClientError?.message !== undefined) { Logger.error( @@ -36,7 +31,7 @@ export async function handleElasticSearchError( `(${elasticSearchClientError.name}) - ${elasticSearchClientError.message}`, ); return { - response: 'QUERY_FAILURE_INVALID_QUERY', + response: 'QUERY_FAILURE_CONNECTION_ERROR', documents: [], failureMessage: elasticSearchClientError.message, }; @@ -65,6 +60,11 @@ export async function handleElasticSearchError( } break; } + case 'DeserializationError': + case 'NoLivingConnectionsError': + case 'NotCompatibleError': + case 'ElasticsearchClientError': + case 'SerializationError': default: { break; } diff --git a/Meadowlark-js/backends/meadowlark-elasticsearch-backend/test/QueryElasticsearch.test.ts b/Meadowlark-js/backends/meadowlark-elasticsearch-backend/test/QueryElasticsearch.test.ts index daacc4a6..18569182 100644 --- a/Meadowlark-js/backends/meadowlark-elasticsearch-backend/test/QueryElasticsearch.test.ts +++ b/Meadowlark-js/backends/meadowlark-elasticsearch-backend/test/QueryElasticsearch.test.ts @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import { Client } from '@elastic/elasticsearch'; +import { Client, errors } from '@elastic/elasticsearch'; import Mock from '@elastic/elasticsearch-mock'; import { PaginationParameters, @@ -321,4 +321,82 @@ describe('when querying for students', () => { }); }); }); + + describe('given there is an invalid query error', () => { + let queryResult: QueryResult; + const setupMockRequestConnectionError = (): Client => { + mock.add( + { + method: 'POST', + path: `/${indexFromResourceInfo(resourceInfo)}/_search`, + }, + () => + new errors.ResponseError({ + body: { errors: {}, status: 500 }, + statusCode: 500, + warnings: ['index_not_found_exception'], + meta: {} as any, + }), + ); + + const client = new Client({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), + }); + return client; + }; + + beforeAll(async () => { + const client = setupMockRequestConnectionError(); + const request = setupQueryRequest({ type: 'FULL_ACCESS' }, {}, {}); + + queryResult = await queryDocuments(request, client); + }); + + it('should return connection error', async () => { + expect(queryResult.failureMessage).toMatchInlineSnapshot('"{"errors":{},"status":500}"'); + expect(queryResult.response).toBe('QUERY_FAILURE_INVALID_QUERY'); + expect(queryResult.documents.length).toBe(0); + }); + + afterAll(() => { + mock.clearAll(); + }); + }); + + describe('given there is a connection error', () => { + let queryResult: QueryResult; + const setupMockRequestConnectionError = (): Client => { + mock.add( + { + method: 'POST', + path: `/${indexFromResourceInfo(resourceInfo)}/_search`, + }, + () => new errors.ConnectionError('Connection error failure message'), + ); + + const client = new Client({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), + }); + return client; + }; + + beforeAll(async () => { + const client = setupMockRequestConnectionError(); + const request = setupQueryRequest({ type: 'FULL_ACCESS' }, {}, {}); + + queryResult = await queryDocuments(request, client); + }); + + it('should return connection error', async () => { + expect(queryResult.failureMessage).toBe('Connection error failure message'); + expect(queryResult.response).toBe('QUERY_FAILURE_CONNECTION_ERROR'); + expect(queryResult.documents.length).toBe(0); + }); + + afterAll(() => { + mock.clearAll(); + }); + }); }); diff --git a/Meadowlark-js/backends/meadowlark-opensearch-backend/src/repository/OpenSearchException.ts b/Meadowlark-js/backends/meadowlark-opensearch-backend/src/repository/OpenSearchException.ts index 9e7fcd46..5be61ca4 100644 --- a/Meadowlark-js/backends/meadowlark-opensearch-backend/src/repository/OpenSearchException.ts +++ b/Meadowlark-js/backends/meadowlark-opensearch-backend/src/repository/OpenSearchException.ts @@ -21,12 +21,7 @@ export async function handleOpenSearchError( switch (openSearchClientError.name) { case 'ConfigurationError': case 'ConnectionError': - case 'DeserializationError': - case 'NoLivingConnectionsError': - case 'NotCompatibleError': - case 'OpenSearchClientError': case 'RequestAbortedError': - case 'SerializationError': case 'TimeoutError': { if (openSearchClientError?.message !== undefined) { Logger.error( @@ -34,7 +29,11 @@ export async function handleOpenSearchError( traceId, `(${openSearchClientError.name}) - ${openSearchClientError.message}`, ); - return { response: 'QUERY_FAILURE_INVALID_QUERY', documents: [], failureMessage: openSearchClientError.message }; + return { + response: 'QUERY_FAILURE_CONNECTION_ERROR', + documents: [], + failureMessage: openSearchClientError.message, + }; } break; } @@ -94,6 +93,11 @@ export async function handleOpenSearchError( } break; } + case 'DeserializationError': + case 'NoLivingConnectionsError': + case 'NotCompatibleError': + case 'OpenSearchClientError': + case 'SerializationError': default: { break; } diff --git a/Meadowlark-js/backends/meadowlark-opensearch-backend/test/QueryOpensearch.test.ts b/Meadowlark-js/backends/meadowlark-opensearch-backend/test/QueryOpensearch.test.ts index ca8c0993..4a786060 100644 --- a/Meadowlark-js/backends/meadowlark-opensearch-backend/test/QueryOpensearch.test.ts +++ b/Meadowlark-js/backends/meadowlark-opensearch-backend/test/QueryOpensearch.test.ts @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -import { Client } from '@opensearch-project/opensearch'; +import { Client, errors } from '@opensearch-project/opensearch'; import Mock from '@short.io/opensearch-mock'; import { PaginationParameters, @@ -325,4 +325,40 @@ describe('when querying for students', () => { }); }); }); + + describe('given there is a connection error', () => { + let queryResult: QueryResult; + const setupMockRequestConnectionError = (): Client => { + mock.add( + { + method: 'POST', + path: `/${indexFromResourceInfo(resourceInfo)}/_search`, + }, + () => new errors.ConnectionError('Connection error failure message'), + ); + + const client = new Client({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), + }); + return client; + }; + + beforeAll(async () => { + const client = setupMockRequestConnectionError(); + const request = setupQueryRequest({ type: 'FULL_ACCESS' }, {}, {}); + + queryResult = await queryDocuments(request, client); + }); + + it('should return connection error', async () => { + expect(queryResult.failureMessage).toBe('Connection error failure message'); + expect(queryResult.response).toBe('QUERY_FAILURE_CONNECTION_ERROR'); + expect(queryResult.documents.length).toBe(0); + }); + + afterAll(() => { + mock.clearAll(); + }); + }); }); diff --git a/Meadowlark-js/packages/meadowlark-core/src/handler/Query.ts b/Meadowlark-js/packages/meadowlark-core/src/handler/Query.ts index f34a1365..88ca2d45 100644 --- a/Meadowlark-js/packages/meadowlark-core/src/handler/Query.ts +++ b/Meadowlark-js/packages/meadowlark-core/src/handler/Query.ts @@ -68,6 +68,18 @@ export async function query(frontendRequest: FrontendRequest): Promise; failureMessage?: string; totalCount?: number; diff --git a/Meadowlark-js/packages/meadowlark-core/test/handler/Query.test.ts b/Meadowlark-js/packages/meadowlark-core/test/handler/Query.test.ts index c9dd845e..74ce13f0 100644 --- a/Meadowlark-js/packages/meadowlark-core/test/handler/Query.test.ts +++ b/Meadowlark-js/packages/meadowlark-core/test/handler/Query.test.ts @@ -56,6 +56,72 @@ describe('given persistence is going to fail', () => { }); }); +describe('given persistence fails with connection error', () => { + let response: FrontendResponse; + let mockQueryHandler: any; + const expectedError = 'Error'; + + beforeAll(async () => { + mockQueryHandler = jest.spyOn(PluginLoader, 'getQueryHandler').mockReturnValue({ + ...NoDocumentStorePlugin, + queryDocuments: async () => + Promise.resolve({ + response: 'QUERY_FAILURE_CONNECTION_ERROR', + documents: [], + failureMessage: expectedError, + }), + }); + + // Act + response = await query(frontendRequest); + }); + + afterAll(() => { + mockQueryHandler.mockRestore(); + }); + + it('returns status 502', () => { + expect(response.statusCode).toEqual(502); + }); + + it('does not return a message body', () => { + expect(response.body).toBeUndefined(); + }); +}); + +describe('given persistence fails with invalid query', () => { + let response: FrontendResponse; + let mockQueryHandler: any; + const expectedError = 'Error'; + + beforeAll(async () => { + mockQueryHandler = jest.spyOn(PluginLoader, 'getQueryHandler').mockReturnValue({ + ...NoDocumentStorePlugin, + queryDocuments: async () => + Promise.resolve({ + response: 'QUERY_FAILURE_INVALID_QUERY', + documents: [], + failureMessage: expectedError, + }), + }); + + // Act + response = await query(frontendRequest); + }); + + afterAll(() => { + mockQueryHandler.mockRestore(); + }); + + it('returns status 500', () => { + expect(response.statusCode).toEqual(500); + }); + + it('does not return a message body', () => { + expect(response.body).toBeUndefined(); + }); +}); + describe('given successful query result', () => { let response: FrontendResponse; let mockQueryHandler: any;