Many tests. small refactors to support.

This commit is contained in:
2023-04-27 12:49:15 -05:00
parent d12bf3decc
commit 0c5e3a8002
7 changed files with 1394 additions and 10 deletions

View File

@ -34,10 +34,6 @@
<properties>
<!-- props specifically to this module -->
<!-- none at this time -->
<!-- todo - remove this once module is further built out and we can hit standard ratio -->
<coverage.instructionCoveredRatioMinimum>0.00</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.00</coverage.classCoveredRatioMinimum>
</properties>
<dependencies>

View File

@ -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);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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}");

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QHttpResponse> mockResponseQueue = new ArrayDeque<>();
private UnsafeConsumer<HttpRequestBase, ? extends Throwable> 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<QHttpResponse> getMockResponseQueue()
{
return (this.mockResponseQueue);
}
/*******************************************************************************
** Setter for mockResponseQueue
*******************************************************************************/
public void setMockResponseQueue(Deque<QHttpResponse> mockResponseQueue)
{
this.mockResponseQueue = mockResponseQueue;
}
/*******************************************************************************
** Fluent setter for mockResponseQueue
*******************************************************************************/
public MockApiUtilsHelper withMockResponseQueue(Deque<QHttpResponse> mockResponseQueue)
{
this.mockResponseQueue = mockResponseQueue;
return (this);
}
/*******************************************************************************
** Getter for mockRequestAsserter
*******************************************************************************/
public UnsafeConsumer<HttpRequestBase, ? extends Throwable> getMockRequestAsserter()
{
return (this.mockRequestAsserter);
}
/*******************************************************************************
** Setter for mockRequestAsserter
*******************************************************************************/
public void setMockRequestAsserter(UnsafeConsumer<HttpRequestBase, ? extends Throwable> mockRequestAsserter)
{
this.mockRequestAsserter = mockRequestAsserter;
}
/*******************************************************************************
** Fluent setter for mockRequestAsserter
*******************************************************************************/
public MockApiUtilsHelper withMockRequestAsserter(UnsafeConsumer<HttpRequestBase, ? extends Throwable> mockRequestAsserter)
{
this.mockRequestAsserter = mockRequestAsserter;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public QHttpResponse defaultMockMakeRequest(MockApiUtilsHelper mockApiUtilsHelper, QTableMetaData table, HttpRequestBase request, UnsafeSupplier<QHttpResponse, QException> 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())));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
{
}
}