From 0c5e3a8002753cc444514e4b1a5cc84908f62c33 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 27 Apr 2023 12:49:15 -0500 Subject: [PATCH] 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) + { + + } +}