From d12bf3decc58532dae66037976469f918a74f01d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 12:48:01 -0500 Subject: [PATCH 1/2] Make it an option in the module's interface, whether or not to query for all records being updated or deleted first (makes more sense for an api backend to NOT do this). --- .../core/actions/interfaces/DeleteInterface.java | 9 +++++++++ .../core/actions/interfaces/UpdateInterface.java | 10 ++++++++++ .../backend/core/actions/tables/DeleteAction.java | 6 +++--- .../backend/core/actions/tables/UpdateAction.java | 15 +++++++++++---- .../module/api/actions/APIUpdateAction.java | 12 ++++++++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java index 669967d2..43781917 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java @@ -50,4 +50,13 @@ public interface DeleteInterface return (false); } + /******************************************************************************* + ** Specify whether this particular module's delete action can & should fetch + ** records before deleting them, e.g., for audits or "not-found-checks" + *******************************************************************************/ + default boolean supportsPreFetchQuery() + { + return (true); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java index fa87f5e8..e89baeed 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/UpdateInterface.java @@ -37,4 +37,14 @@ public interface UpdateInterface ** *******************************************************************************/ UpdateOutput execute(UpdateInput updateInput) throws QException; + + /******************************************************************************* + ** Specify whether this particular module's update action can & should fetch + ** records before updating them, e.g., for audits or "not-found-checks" + *******************************************************************************/ + default boolean supportsPreFetchQuery() + { + return (true); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java index 195200e7..ec240c2c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java @@ -76,13 +76,13 @@ public class DeleteAction QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend()); + DeleteInterface deleteInterface = qModule.getDeleteInterface(); if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null) { throw (new QException("A delete request may not contain both a list of primary keys and a query filter.")); } - DeleteInterface deleteInterface = qModule.getDeleteInterface(); if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput()) { LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes"); @@ -99,8 +99,8 @@ public class DeleteAction } } - List recordListForAudit = getRecordListForAuditIfNeeded(deleteInput); - List recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit); + List recordListForAudit = deleteInterface.supportsPreFetchQuery() ? getRecordListForAuditIfNeeded(deleteInput) : new ArrayList<>(); + List recordsWithValidationErrors = deleteInterface.supportsPreFetchQuery() ? validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit) : new ArrayList<>(); DeleteOutput deleteOutput = deleteInterface.execute(deleteInput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java index 0ada6d9e..17663aa7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java @@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; +import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.context.QContext; @@ -87,18 +88,24 @@ public class UpdateAction ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords()); // todo - need to handle records with errors coming out of here... - List oldRecordList = getOldRecordListForAuditIfNeeded(updateInput); - QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend()); + UpdateInterface updateInterface = qModule.getUpdateInterface(); + + List oldRecordList = updateInterface.supportsPreFetchQuery() ? getOldRecordListForAuditIfNeeded(updateInput) : new ArrayList<>(); validatePrimaryKeysAreGiven(updateInput); - validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList); + + if(updateInterface.supportsPreFetchQuery()) + { + validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList); + } + validateRequiredFields(updateInput); ValidateRecordSecurityLockHelper.validateSecurityFields(updateInput.getTable(), updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); // todo pre-customization - just get to modify the request? - UpdateOutput updateOutput = qModule.getUpdateInterface().execute(updateInput); + UpdateOutput updateOutput = updateInterface.execute(updateInput); // todo post-customization - can do whatever w/ the result if you want List errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList(); diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java index 89cafaf5..e0af323d 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/APIUpdateAction.java @@ -45,4 +45,16 @@ public class APIUpdateAction extends AbstractAPIAction implements UpdateInterfac return (apiActionUtil.doUpdate(table, updateInput)); } + + + /******************************************************************************* + ** Specify whether this particular module's update action can & should fetch + ** records before updating them, e.g., for audits or "not-found-checks" + *******************************************************************************/ + @Override + public boolean supportsPreFetchQuery() + { + return (false); + } + } From 0c5e3a8002753cc444514e4b1a5cc84908f62c33 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 12:49:15 -0500 Subject: [PATCH 2/2] Many tests. small refactors to support. --- qqq-backend-module-api/pom.xml | 4 - .../module/api/actions/BaseAPIActionUtil.java | 26 +- .../qqq/backend/module/api/TestUtils.java | 66 +- .../api/actions/BaseAPIActionUtilTest.java | 672 ++++++++++++++++++ .../module/api/mocks/MockApiActionUtils.java | 108 +++ .../module/api/mocks/MockApiUtilsHelper.java | 226 ++++++ .../module/api/mocks/MockHttpResponse.java | 302 ++++++++ 7 files changed, 1394 insertions(+), 10 deletions(-) create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java create mode 100644 qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java diff --git a/qqq-backend-module-api/pom.xml b/qqq-backend-module-api/pom.xml index f86fb9d8..e390f623 100644 --- a/qqq-backend-module-api/pom.xml +++ b/qqq-backend-module-api/pom.xml @@ -34,10 +34,6 @@ - - - 0.00 - 0.00 diff --git a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java index d8ecf17a..f3cd1107 100644 --- a/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java +++ b/qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java @@ -634,7 +634,7 @@ public class BaseAPIActionUtil request.setEntity(new StringEntity(postBody)); request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); - HttpResponse response = client.execute(request); + HttpResponse response = executeOAuthTokenRequest(client, request); int statusCode = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); String resultString = EntityUtils.toString(entity); @@ -669,6 +669,16 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** one-line method, factored out so mock/tests can override + *******************************************************************************/ + protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException + { + return client.execute(request); + } + + + /******************************************************************************* ** As part of making a request - set up its content-type header. *******************************************************************************/ @@ -880,7 +890,7 @@ public class BaseAPIActionUtil LOG.info("POST contents [" + ((HttpPost) request).getEntity().toString() + "]"); } - try(CloseableHttpResponse response = httpClient.execute(request)) + try(CloseableHttpResponse response = executeHttpRequest(request, httpClient)) { QHttpResponse qResponse = new QHttpResponse(response); @@ -924,7 +934,7 @@ public class BaseAPIActionUtil rateLimitsCaught++; if(rateLimitsCaught > getMaxAllowedRateLimitErrors()) { - LOG.error("Giving up POST to [" + table.getName() + "] after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + ")"); + LOG.error("Giving up " + request.getMethod() + " to [" + table.getName() + "] after too many rate-limit errors (" + getMaxAllowedRateLimitErrors() + ")"); throw (new QException(rle)); } @@ -950,6 +960,16 @@ public class BaseAPIActionUtil + /******************************************************************************* + ** one-line method, factored out so mock/tests can override + *******************************************************************************/ + protected CloseableHttpResponse executeHttpRequest(HttpRequestBase request, CloseableHttpClient httpClient) throws IOException + { + return httpClient.execute(request); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java index 1217445b..27a12a55 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java @@ -32,6 +32,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; +import com.kingsrook.qqq.backend.module.api.mocks.MockApiActionUtils; import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails; @@ -42,7 +44,10 @@ import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetail *******************************************************************************/ public class TestUtils { + public static final String MEMORY_BACKEND_NAME = "memory"; public static final String EASYPOST_BACKEND_NAME = "easypost"; + public static final String MOCK_BACKEND_NAME = "mock"; + public static final String MOCK_TABLE_NAME = "mock"; @@ -52,14 +57,69 @@ public class TestUtils public static QInstance defineInstance() { QInstance qInstance = new QInstance(); - qInstance.addBackend(defineBackend()); - qInstance.addTable(defineTableEasypostTracker()); qInstance.setAuthentication(defineAuthentication()); + + qInstance.addBackend(defineMemoryBackend()); + + qInstance.addBackend(defineMockBackend()); + qInstance.addTable(defineMockTable()); + + qInstance.addBackend(defineEasypostBackend()); + qInstance.addTable(defineTableEasypostTracker()); + return (qInstance); } + /******************************************************************************* + ** Define the in-memory backend used in standard tests + *******************************************************************************/ + public static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withName(MEMORY_BACKEND_NAME) + .withBackendType(MemoryBackendModule.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QBackendMetaData defineMockBackend() + { + return (new APIBackendMetaData() + .withName(MOCK_BACKEND_NAME) + .withAuthorizationType(AuthorizationType.API_KEY_HEADER) + .withBaseUrl("http://localhost:9999/mock") + .withContentType("application/json") + .withActionUtil(new QCodeReference(MockApiActionUtils.class, QCodeUsage.CUSTOMIZER)) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QTableMetaData defineMockTable() + { + return (new QTableMetaData() + .withName(MOCK_TABLE_NAME) + .withBackendName(MOCK_BACKEND_NAME) + .withField(new QFieldMetaData("id", QFieldType.STRING)) + .withField(new QFieldMetaData("name", QFieldType.STRING)) + .withPrimaryKeyField("id") + .withBackendDetails(new APITableBackendDetails() + .withTablePath("mock") + .withTableWrapperObjectName("mocks") + ) + ); + } + + + /******************************************************************************* ** Define the authentication used in standard tests - using 'mock' type. ** @@ -76,7 +136,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - public static QBackendMetaData defineBackend() + public static QBackendMetaData defineEasypostBackend() { String apiKey = new QMetaDataVariableInterpreter().interpret("${env.EASYPOST_API_KEY}"); diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java new file mode 100644 index 00000000..04275a80 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtilTest.java @@ -0,0 +1,672 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.actions; + + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.module.api.BaseTest; +import com.kingsrook.qqq.backend.module.api.TestUtils; +import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException; +import com.kingsrook.qqq.backend.module.api.mocks.MockApiActionUtils; +import com.kingsrook.qqq.backend.module.api.mocks.MockApiUtilsHelper; +import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; +import com.kingsrook.qqq.backend.module.api.model.OutboundAPILog; +import com.kingsrook.qqq.backend.module.api.model.OutboundAPILogMetaDataProvider; +import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; +import org.apache.http.Header; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil + *******************************************************************************/ +class BaseAPIActionUtilTest extends BaseTest +{ + private static MockApiUtilsHelper mockApiUtilsHelper = new MockApiUtilsHelper(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + void beforeEach() + { + mockApiUtilsHelper = new MockApiUtilsHelper(); + mockApiUtilsHelper.setUseMock(true); + MockApiActionUtils.mockApiUtilsHelper = mockApiUtilsHelper; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCount() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + [ + {"id": 1, "name": "Homer"}, + {"id": 2, "name": "Marge"}, + {"id": 3, "name": "Bart"}, + {"id": 4, "name": "Lisa"}, + {"id": 5, "name": "Maggie"} + ] + """); + + CountInput countInput = new CountInput(); + countInput.setTableName(TestUtils.MOCK_TABLE_NAME); + CountOutput countOutput = new CountAction().execute(countInput); + assertEquals(5, countOutput.getCount()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCountError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + CountInput countInput = new CountInput(); + countInput.setTableName(TestUtils.MOCK_TABLE_NAME); + assertThatThrownBy(() -> new CountAction().execute(countInput)).hasRootCauseInstanceOf(Exception.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGet() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetByKey() throws QException + { + QContext.getQInstance().getTable(TestUtils.MOCK_TABLE_NAME).withUniqueKey(new UniqueKey("id")); + + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setUniqueKey(Map.of("id", 3)); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQuery() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + [ + {"id": 1, "name": "Homer"}, + {"id": 2, "name": "Marge"}, + {"id": 3, "name": "Bart"}, + {"id": 4, "name": "Lisa"}, + {"id": 5, "name": "Maggie"} + ] + """); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size()); + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryObjectWrappingList() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"mocks": [ + {"id": 1, "name": "Homer"}, + {"id": 2, "name": "Marge"}, + {"id": 3, "name": "Bart"}, + {"id": 4, "name": "Lisa"}, + {"id": 5, "name": "Maggie"} + ]} + """); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(5, queryOutput.getRecords().size()); + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryObjectWrappingSingleObject() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"mocks": + {"id": 1, "name": "Homer"} + } + """); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size()); + assertEquals(1, queryOutput.getRecords().get(0).getValueInteger("id")); + assertEquals("Homer", queryOutput.getRecords().get(0).getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryPaginate() throws QException + { + String oneObject = """ + {"id": 1, "name": "Homer"} + """; + StringBuilder response = new StringBuilder("["); + for(int i = 0; i < 19; i++) + { + response.append(oneObject).append(","); + } + response.append(oneObject); + response.append("]"); + mockApiUtilsHelper.enqueueMockResponse(response.toString()); + mockApiUtilsHelper.enqueueMockResponse(response.toString()); + mockApiUtilsHelper.enqueueMockResponse(response.toString()); + mockApiUtilsHelper.enqueueMockResponse("[]"); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(60, queryOutput.getRecords().size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQueryError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(TestUtils.MOCK_TABLE_NAME); + assertThatThrownBy(() -> new QueryAction().execute(queryInput)).hasRootCauseInstanceOf(Exception.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsert() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 6} + """); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsertEmptyInputList() throws QException + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + insertInput.setRecords(List.of()); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsertError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + InsertInput insertInput = new InsertInput(); + insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertTrue(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdate() throws QException + { + mockApiUtilsHelper.enqueueMockResponse(""); + + mockApiUtilsHelper.setMockRequestAsserter(httpRequestBase -> + { + String requestBody = MockApiUtilsHelper.readRequestBody(httpRequestBase); + JSONObject requestObject = new JSONObject(requestBody); + + JSONArray mocks = requestObject.getJSONArray("mocks"); + JSONObject record = mocks.getJSONObject(0); + + assertEquals("Bartholomew", record.getString("name")); + assertEquals(3, record.getInt("id")); + }); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(TestUtils.MOCK_TABLE_NAME); + updateInput.setRecords(List.of(new QRecord().withValue("id", "3").withValue("name", "Bartholomew"))); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + + // not sure what to assert in here... + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdateEmptyInputList() throws QException + { + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(TestUtils.MOCK_TABLE_NAME); + updateInput.setRecords(List.of()); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUpdateError() throws QException + { + //////////////////////////////////////// + // avoid the fully mocked makeRequest // + //////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent(""" + {"error": "Server error"} + """)); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + updateInput.setTableName(TestUtils.MOCK_TABLE_NAME); + + ///////////////////////////////////////////////////////////////////////////////// + // note - right now this is inconsistent with insertAction (and rdbms update), // + // where errors are placed in the records, rather than thrown... // + ///////////////////////////////////////////////////////////////////////////////// + assertThatThrownBy(() -> new UpdateAction().execute(updateInput)).hasRootCauseInstanceOf(Exception.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMakeRequest() throws QException + { + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test429Then200() throws QException + { + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + // specifically, that we can get one 429, and then eventually a 200 // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + GetOutput getOutput = new GetAction().execute(getInput); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTooMany429() throws QException + { + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + // specifically, that after too many 429's we get an error // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(429).withContent("Try again")); + + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + + assertThatThrownBy(() -> new GetAction().execute(getInput)).hasRootCauseInstanceOf(RateLimitException.class); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testApiLogs() throws QException + { + QInstance qInstance = QContext.getQInstance(); + OutboundAPILogMetaDataProvider.defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 6} + """); + + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.MOCK_TABLE_NAME); + insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse"))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(6, insertOutput.getRecords().get(0).getValueInteger("id")); + + QueryInput queryInput = new QueryInput(); + queryInput.setTableName(OutboundAPILog.TABLE_NAME); + QueryOutput apiLogRecords = new QueryAction().execute(queryInput); + assertEquals(1, apiLogRecords.getRecords().size()); + assertEquals("POST", apiLogRecords.getRecords().get(0).getValueString("method")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicAuthApiKey() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.BASIC_AUTH_API_KEY); + backend.setApiKey("9876-WXYZ"); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + mockApiUtilsHelper.setMockRequestAsserter(request -> + { + Header authHeader = request.getFirstHeader("Authorization"); + assertTrue(authHeader.getValue().startsWith("Basic ")); + String apiKey = new String(Base64.getDecoder().decode(authHeader.getValue().replace("Basic ", "")), StandardCharsets.UTF_8); + assertEquals("9876-WXYZ", apiKey); + }); + + runSimpleGetAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicAuthUsernamePassword() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.BASIC_AUTH_USERNAME_PASSWORD); + backend.setUsername("god"); + backend.setPassword("5fingers"); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + mockApiUtilsHelper.setMockRequestAsserter(request -> + { + Header authHeader = request.getFirstHeader("Authorization"); + assertTrue(authHeader.getValue().startsWith("Basic ")); + String usernamePassword = new String(Base64.getDecoder().decode(authHeader.getValue().replace("Basic ", "")), StandardCharsets.UTF_8); + assertEquals("god:5fingers", usernamePassword); + }); + + runSimpleGetAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOAuth2ValidToken() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.OAUTH2); + backend.withCustomValue("accessToken", "validToken"); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + mockApiUtilsHelper.setMockRequestAsserter(request -> + { + Header authHeader = request.getFirstHeader("Authorization"); + assertTrue(authHeader.getValue().startsWith("Bearer ")); + String token = authHeader.getValue().replace("Bearer ", ""); + assertEquals("validToken", token); + }); + + runSimpleGetAction(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOAuth2NullToken() throws QException + { + APIBackendMetaData backend = (APIBackendMetaData) QContext.getQInstance().getBackend(TestUtils.MOCK_BACKEND_NAME); + backend.setAuthorizationType(AuthorizationType.OAUTH2); + + //////////////////////////////////////////////////////////////////////////////////////////// + // this will make it not use the mock makeRequest method, // + // but instead the mock executeHttpRequest, so we can test code from the base makeRequest // + //////////////////////////////////////////////////////////////////////////////////////////// + mockApiUtilsHelper.setUseMock(false); + mockApiUtilsHelper.enqueueMockResponse(""" + {"access_token": "myNewToken"} + """); + mockApiUtilsHelper.enqueueMockResponse(""" + {"id": 3, "name": "Bart"}, + """); + + GetOutput getOutput = runSimpleGetAction(); + assertEquals(3, getOutput.getRecord().getValueInteger("id")); + assertEquals("Bart", getOutput.getRecord().getValueString("name")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static GetOutput runSimpleGetAction() throws QException + { + GetInput getInput = new GetInput(); + getInput.setTableName(TestUtils.MOCK_TABLE_NAME); + getInput.setPrimaryKey(3); + return (new GetAction().execute(getInput)); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java new file mode 100644 index 00000000..b54f37d7 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiActionUtils.java @@ -0,0 +1,108 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.mocks; + + +import java.io.IOException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.module.api.actions.BaseAPIActionUtil; +import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MockApiActionUtils extends BaseAPIActionUtil +{ + public static MockApiUtilsHelper mockApiUtilsHelper; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException + { + return (mockApiUtilsHelper.defaultMockMakeRequest(mockApiUtilsHelper, table, request, () -> super.makeRequest(table, request))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected CloseableHttpResponse executeHttpRequest(HttpRequestBase request, CloseableHttpClient httpClient) throws IOException + { + runMockAsserter(request); + return new MockHttpResponse(mockApiUtilsHelper); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void runMockAsserter(HttpRequestBase request) + { + if(mockApiUtilsHelper.getMockRequestAsserter() != null) + { + try + { + mockApiUtilsHelper.getMockRequestAsserter().run(request); + } + catch(Exception e) + { + throw (new RuntimeException("Error running mock request asserter", e)); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected CloseableHttpResponse executeOAuthTokenRequest(CloseableHttpClient client, HttpPost request) throws IOException + { + runMockAsserter(request); + return new MockHttpResponse(mockApiUtilsHelper); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + protected int getInitialRateLimitBackoffMillis() + { + return (1); + } + +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java new file mode 100644 index 00000000..65b973df --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockApiUtilsHelper.java @@ -0,0 +1,226 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.mocks; + + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; +import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MockApiUtilsHelper +{ + private static final QLogger LOG = QLogger.getLogger(MockApiUtilsHelper.class); + + private boolean useMock = true; + private Deque mockResponseQueue = new ArrayDeque<>(); + private UnsafeConsumer mockRequestAsserter = null; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enqueueMockResponse(String json) + { + mockResponseQueue.addLast(new QHttpResponse() + .withStatusCode(200) + .withContent(json) + ); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void enqueueMockResponse(QHttpResponse qHttpResponse) + { + mockResponseQueue.addLast(qHttpResponse); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QHttpResponse returnMockResponseFromQueue(HttpRequestBase request) throws QException + { + if(getMockRequestAsserter() != null) + { + try + { + getMockRequestAsserter().run(request); + } + catch(Exception e) + { + throw (new QException("Error running mock request asserter", e)); + } + } + + if(mockResponseQueue.isEmpty()) + { + fail("No mock response is in the queue for " + request.getMethod() + " " + request.getURI()); + } + + LOG.info("Returning mock http response for " + request.getMethod() + " " + request.getURI()); + return (mockResponseQueue.removeFirst()); + } + + + + /******************************************************************************* + ** Getter for useMock + *******************************************************************************/ + public boolean getUseMock() + { + return (this.useMock); + } + + + + /******************************************************************************* + ** Setter for useMock + *******************************************************************************/ + public void setUseMock(boolean useMock) + { + this.useMock = useMock; + } + + + + /******************************************************************************* + ** Fluent setter for useMock + *******************************************************************************/ + public MockApiUtilsHelper withUseMock(boolean useMock) + { + this.useMock = useMock; + return (this); + } + + + + /******************************************************************************* + ** Getter for mockResponseQueue + *******************************************************************************/ + public Deque getMockResponseQueue() + { + return (this.mockResponseQueue); + } + + + + /******************************************************************************* + ** Setter for mockResponseQueue + *******************************************************************************/ + public void setMockResponseQueue(Deque mockResponseQueue) + { + this.mockResponseQueue = mockResponseQueue; + } + + + + /******************************************************************************* + ** Fluent setter for mockResponseQueue + *******************************************************************************/ + public MockApiUtilsHelper withMockResponseQueue(Deque mockResponseQueue) + { + this.mockResponseQueue = mockResponseQueue; + return (this); + } + + + + /******************************************************************************* + ** Getter for mockRequestAsserter + *******************************************************************************/ + public UnsafeConsumer getMockRequestAsserter() + { + return (this.mockRequestAsserter); + } + + + + /******************************************************************************* + ** Setter for mockRequestAsserter + *******************************************************************************/ + public void setMockRequestAsserter(UnsafeConsumer mockRequestAsserter) + { + this.mockRequestAsserter = mockRequestAsserter; + } + + + + /******************************************************************************* + ** Fluent setter for mockRequestAsserter + *******************************************************************************/ + public MockApiUtilsHelper withMockRequestAsserter(UnsafeConsumer mockRequestAsserter) + { + this.mockRequestAsserter = mockRequestAsserter; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QHttpResponse defaultMockMakeRequest(MockApiUtilsHelper mockApiUtilsHelper, QTableMetaData table, HttpRequestBase request, UnsafeSupplier superMethod) throws QException + { + if(!mockApiUtilsHelper.getUseMock()) + { + QHttpResponse superResponse = superMethod.get(); + System.out.println("== non-mock response content: =="); + System.out.println("Code: " + superResponse.getStatusCode()); + System.out.println(superResponse.getContent()); + System.out.println("== =="); + return (superResponse); + } + + return mockApiUtilsHelper.returnMockResponseFromQueue(request); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + public static String readRequestBody(HttpRequestBase request) throws IOException + { + return (StringUtils.join("\n", IOUtils.readLines(((HttpPost) request).getEntity().getContent()))); + } +} diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java new file mode 100644 index 00000000..dbc61f69 --- /dev/null +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/mocks/MockHttpResponse.java @@ -0,0 +1,302 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.module.api.mocks; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Locale; +import com.kingsrook.qqq.backend.module.api.actions.QHttpResponse; +import org.apache.http.Header; +import org.apache.http.HeaderIterator; +import org.apache.http.HttpEntity; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.params.HttpParams; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MockHttpResponse implements CloseableHttpResponse +{ + private final MockApiUtilsHelper mockApiUtilsHelper; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MockHttpResponse(MockApiUtilsHelper mockApiUtilsHelper) + { + this.mockApiUtilsHelper = mockApiUtilsHelper; + } + + + + @Override + public void close() throws IOException + { + + } + + + + @Override + public StatusLine getStatusLine() + { + ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); + + if(!mockApiUtilsHelper.getMockResponseQueue().isEmpty()) + { + QHttpResponse qHttpResponse = mockApiUtilsHelper.getMockResponseQueue().peekFirst(); + return (new BasicStatusLine(protocolVersion, qHttpResponse.getStatusCode(), qHttpResponse.getStatusReasonPhrase())); + } + else + { + return (new BasicStatusLine(protocolVersion, 200, "OK")); + } + } + + + + @Override + public void setStatusLine(StatusLine statusLine) + { + + } + + + + @Override + public void setStatusLine(ProtocolVersion protocolVersion, int i) + { + + } + + + + @Override + public void setStatusLine(ProtocolVersion protocolVersion, int i, String s) + { + + } + + + + @Override + public void setStatusCode(int i) throws IllegalStateException + { + + } + + + + @Override + public void setReasonPhrase(String s) throws IllegalStateException + { + + } + + + + @Override + public HttpEntity getEntity() + { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + + if(!mockApiUtilsHelper.getMockResponseQueue().isEmpty()) + { + QHttpResponse qHttpResponse = mockApiUtilsHelper.getMockResponseQueue().removeFirst(); + basicHttpEntity.setContent(new ByteArrayInputStream(qHttpResponse.getContent().getBytes())); + } + else + { + basicHttpEntity.setContent(new ByteArrayInputStream("".getBytes())); + } + return (basicHttpEntity); + } + + + + @Override + public void setEntity(HttpEntity httpEntity) + { + + } + + + + @Override + public Locale getLocale() + { + return null; + } + + + + @Override + public void setLocale(Locale locale) + { + + } + + + + @Override + public ProtocolVersion getProtocolVersion() + { + return null; + } + + + + @Override + public boolean containsHeader(String s) + { + return false; + } + + + + @Override + public Header[] getHeaders(String s) + { + return new Header[0]; + } + + + + @Override + public Header getFirstHeader(String s) + { + return null; + } + + + + @Override + public Header getLastHeader(String s) + { + return null; + } + + + + @Override + public Header[] getAllHeaders() + { + return new Header[0]; + } + + + + @Override + public void addHeader(Header header) + { + + } + + + + @Override + public void addHeader(String s, String s1) + { + + } + + + + @Override + public void setHeader(Header header) + { + + } + + + + @Override + public void setHeader(String s, String s1) + { + + } + + + + @Override + public void setHeaders(Header[] headers) + { + + } + + + + @Override + public void removeHeader(Header header) + { + + } + + + + @Override + public void removeHeaders(String s) + { + + } + + + + @Override + public HeaderIterator headerIterator() + { + return null; + } + + + + @Override + public HeaderIterator headerIterator(String s) + { + return null; + } + + + + @Override + public HttpParams getParams() + { + return null; + } + + + + @Override + public void setParams(HttpParams httpParams) + { + + } +}