Files
qbit-webhooks/src/test/java/com/kingsrook/qbits/webhooks/actions/FirePostUpdateWebhookEventTest.java
T

307 lines
19 KiB
Java

/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.qbits.webhooks.actions;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qbits.webhooks.BaseTest;
import com.kingsrook.qbits.webhooks.WebhooksTestApplication;
import com.kingsrook.qbits.webhooks.model.Webhook;
import com.kingsrook.qbits.webhooks.model.WebhookEvent;
import com.kingsrook.qbits.webhooks.model.WebhookEventCategory;
import com.kingsrook.qbits.webhooks.model.WebhooksActionFlags;
import com.kingsrook.qbits.webhooks.registry.WebhookEventType;
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.logging.QCollectingLogger;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for FirePostInsertWebhookEvent
*******************************************************************************/
class FirePostUpdateWebhookEventTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
try
{
Integer personId = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord())).getRecords().get(0).getValueInteger("id");
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class);
////////////////////////////////////////////////
// update without any webhook - assert no log //
////////////////////////////////////////////////
new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bubba")));
assertThat(collectingLogger.getCollectedMessages())
.noneMatch(m -> m.getMessage().matches(".*Building webhook event.*"));
///////////////////////////////////////////////////////////////////
// register event type and make a sub - then assert positive log //
///////////////////////////////////////////////////////////////////
String eventTypeName = "updatedPerson";
registerEventType(eventTypeName, WebhookEventCategory.UPDATE, WebhooksTestApplication.TABLE_NAME_PERSON);
Integer subscriptionId = insert(newWebhookSubscription(eventTypeName));
new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON)
.withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bobby")));
assertThat(collectingLogger.getCollectedMessages())
.anyMatch(m -> m.getMessage().matches(".*Building webhook event.*"));
collectingLogger.clear();
///////////////////////////////////////////////
// query to make sure event was inserted too //
///////////////////////////////////////////////
List<QRecord> insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME)
.withFilter(new QQueryFilter(new QFilterCriteria("webhookSubscriptionId", QCriteriaOperator.EQUALS, subscriptionId)))
.withIncludeAssociations(true)).getRecords();
assertEquals(1, insertedEvents.size());
assertEquals(personId, insertedEvents.get(0).getValueInteger("eventSourceRecordId"));
///////////////////////////////////////
// run again, with omit flag present //
///////////////////////////////////////
new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON)
.withFlag(WebhooksActionFlags.OMIT_WEBHOOKS)
.withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bobby")));
assertThat(collectingLogger.getCollectedMessages())
.noneMatch(m -> m.getMessage().matches(".*Building webhook event.*"));
////////////////////////////////////
// assert about the contents too! //
////////////////////////////////////
List<QRecord> contents = insertedEvents.get(0).getAssociatedRecords().get(WebhookEvent.CONTENT_ASSOCIATION_NAME);
assertEquals(1, contents.size());
String postBody = contents.get(0).getValueString("postBody");
assertNotNull(postBody);
JSONObject postBodyJson = new JSONObject(postBody);
assertTrue(postBodyJson.has("record"));
assertEquals("Bobby", postBodyJson.getJSONObject("record").getString("firstName"));
assertEquals(personId, postBodyJson.getJSONObject("record").getInt("id"));
}
finally
{
QLogger.deactivateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStoreEventType() throws QException
{
try
{
Integer personId = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_PERSON).withRecord(new QRecord())).getRecords().get(0).getValueInteger("id");
QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class);
///////////////////////////////////////////////////////////////////
// register event type and make a sub - then assert positive log //
///////////////////////////////////////////////////////////////////
Integer subscriptionId = insert(newWebhookSubscription(WebhooksTestApplication.PERSON_STORED_EVENT_TYPE_NAME));
new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_PERSON)
.withRecord(new QRecord().withValue("id", personId).withValue("firstName", "Bobby")));
assertThat(collectingLogger.getCollectedMessages())
.anyMatch(m -> m.getMessage().matches(".*Building webhook event.*"));
collectingLogger.clear();
///////////////////////////////////////////////
// query to make sure event was inserted too //
///////////////////////////////////////////////
List<QRecord> insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME)
.withFilter(new QQueryFilter(new QFilterCriteria("webhookSubscriptionId", QCriteriaOperator.EQUALS, subscriptionId)))
.withIncludeAssociations(true)).getRecords();
assertEquals(1, insertedEvents.size());
assertEquals(personId, insertedEvents.get(0).getValueInteger("eventSourceRecordId"));
}
finally
{
QLogger.deactivateCollectingLoggerForClass(FirePostInsertOrUpdateWebhookEventUtil.class);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSecurityKeys() throws QException
{
Integer storeA = 17;
Integer storeB = 18;
Integer orderIdA = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("storeId", storeA))).getRecords().get(0).getValueInteger("id");
Integer orderIdB = new InsertAction().execute(new InsertInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("storeId", storeB))).getRecords().get(0).getValueInteger("id");
/////////////////////////////////////////////////////////////////////
// register event type and make a webhook with a sub - for store A //
/////////////////////////////////////////////////////////////////////
String eventTypeName = "updatedOrder";
registerEventType(eventTypeName, WebhookEventCategory.UPDATE, WebhooksTestApplication.TABLE_NAME_ORDER);
Integer webhookId = new InsertAction().execute(new InsertInput(Webhook.TABLE_NAME).withRecord(newWebhook(eventTypeName).toQRecord().withValue("storeId", storeA))).getRecords().get(0).getValueInteger("id");
Integer subscriptionId = insert(newWebhookSubscription(eventTypeName).withWebhookId(webhookId));
///////////////////////////////////////////////////////////////////////////////////
// update the 2 orders - only an event for A should fire //
// of note - the record that's updated doesn't have its storeId field in it - //
// so originally this case failed to match the subscription - but, now it works. //
///////////////////////////////////////////////////////////////////////////////////
new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("id", orderIdA).withValue("orderNo", "A")));
new UpdateAction().execute(new UpdateInput(WebhooksTestApplication.TABLE_NAME_ORDER).withRecord(new QRecord().withValue("id", orderIdB).withValue("orderNo", "B")));
List<QRecord> insertedEvents = new QueryAction().execute(new QueryInput(WebhookEvent.TABLE_NAME)
.withFilter(new QQueryFilter(new QFilterCriteria("webhookSubscriptionId", QCriteriaOperator.EQUALS, subscriptionId)))
.withIncludeAssociations(true)).getRecords();
assertEquals(1, insertedEvents.size());
assertEquals(orderIdA, insertedEvents.get(0).getValueInteger("eventSourceRecordId"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDoesRecordMatchWebhookEventType()
{
WebhookEventType update = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE);
WebhookEventType updateFirstName = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE_WITH_FIELD).withFieldName("firstName");
WebhookEventType updateFirstNameJohn = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE_WITH_VALUE).withFieldName("firstName").withValue("John");
WebhookEventType updateFirstNameAnyBeatle = new WebhookEventType().withTableName(WebhooksTestApplication.TABLE_NAME_PERSON).withCategory(WebhookEventCategory.UPDATE_WITH_VALUE).withFieldName("firstName").withValue(new ArrayList<>(List.of("John", "George", "Paul", "Ringo")));
////////////////////////////////////////////////
// w/ no values, only the generic rule passes //
////////////////////////////////////////////////
QRecord blankRecord = new QRecord();
assertTrue(doesMatch(blankRecord, blankRecord, update));
assertFalse(doesMatch(blankRecord, blankRecord, updateFirstName));
assertFalse(doesMatch(blankRecord, blankRecord, updateFirstNameJohn));
assertFalse(doesMatch(blankRecord, blankRecord, updateFirstNameAnyBeatle));
/////////////////////////////////////////////////////////////////////////
// w/ firstname set to Joe, the generic and "any firstname" rules pass //
/////////////////////////////////////////////////////////////////////////
QRecord firstNameJoeRecord = new QRecord().withValue("firstName", "Joe");
assertTrue(doesMatch(firstNameJoeRecord, blankRecord, update));
assertTrue(doesMatch(firstNameJoeRecord, blankRecord, updateFirstName));
assertFalse(doesMatch(firstNameJoeRecord, blankRecord, updateFirstNameJohn));
assertFalse(doesMatch(firstNameJoeRecord, blankRecord, updateFirstNameAnyBeatle));
/////////////////////////////////////////////////////////////////////////////////
// an update that doesn't change firstName won't fire the "any firstname" rule //
/////////////////////////////////////////////////////////////////////////////////
QRecord firstNameJoeLastNameSmithRecord = new QRecord().withValue("firstName", "Joe").withValue("lastName", "Smith");
QRecord firstNameJoeLastNameJonesRecord = new QRecord().withValue("firstName", "Joe").withValue("lastName", "Jones");
assertTrue(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, update));
assertFalse(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, updateFirstName));
assertFalse(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, updateFirstNameJohn));
assertFalse(doesMatch(firstNameJoeLastNameSmithRecord, firstNameJoeLastNameJonesRecord, updateFirstNameAnyBeatle));
///////////////////////////////////////////////////////////////////////
// an update that only changes last name only fires the generic rule //
///////////////////////////////////////////////////////////////////////
QRecord lastNameJonesRecord = new QRecord().withValue("lastName", "Jones");
assertTrue(doesMatch(lastNameJonesRecord, blankRecord, update));
assertFalse(doesMatch(lastNameJonesRecord, blankRecord, updateFirstName));
assertFalse(doesMatch(lastNameJonesRecord, blankRecord, updateFirstNameJohn));
assertFalse(doesMatch(lastNameJonesRecord, blankRecord, updateFirstNameAnyBeatle));
////////////////////////////////////////////////////////////////
// changing first name to the target value passes all 4 rules //
////////////////////////////////////////////////////////////////
QRecord firstNameJohnRecord = new QRecord().withValue("firstName", "John");
assertTrue(doesMatch(firstNameJohnRecord, blankRecord, update));
assertTrue(doesMatch(firstNameJohnRecord, blankRecord, updateFirstName));
assertTrue(doesMatch(firstNameJohnRecord, blankRecord, updateFirstNameJohn));
assertTrue(doesMatch(firstNameJohnRecord, blankRecord, updateFirstNameAnyBeatle));
/////////////////////////////////////////////////////////////////////////////////////////
// changing first name to the target value from the list but not the single-value rule //
/////////////////////////////////////////////////////////////////////////////////////////
QRecord firstNameRingoRecord = new QRecord().withValue("firstName", "Ringo");
assertTrue(doesMatch(firstNameRingoRecord, blankRecord, update));
assertTrue(doesMatch(firstNameRingoRecord, blankRecord, updateFirstName));
assertFalse(doesMatch(firstNameRingoRecord, blankRecord, updateFirstNameJohn));
assertTrue(doesMatch(firstNameRingoRecord, blankRecord, updateFirstNameAnyBeatle));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// an update that doesn't change firstName (even if it has the target value) won't fire the "firstname John" rule //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord firstNameJohnRecordLastNameSmithRecord = new QRecord().withValue("firstName", "John").withValue("lastName", "Smith");
QRecord firstNameJohnRecordLastNameJonesRecord = new QRecord().withValue("firstName", "John").withValue("lastName", "Jones");
assertTrue(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, update));
assertFalse(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, updateFirstName));
assertFalse(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, updateFirstNameJohn));
assertFalse(doesMatch(firstNameJohnRecordLastNameSmithRecord, firstNameJohnRecordLastNameJonesRecord, updateFirstNameAnyBeatle));
//////////////////////////////////////////////////////////////
// without old-record, always fail the value-checking rules //
//////////////////////////////////////////////////////////////
assertTrue(doesMatch(firstNameJohnRecord, null, update));
assertFalse(doesMatch(firstNameJohnRecord, null, updateFirstName));
assertFalse(doesMatch(firstNameJohnRecord, null, updateFirstNameJohn));
assertFalse(doesMatch(firstNameJohnRecord, null, updateFirstNameAnyBeatle));
}
/***************************************************************************
**
***************************************************************************/
private boolean doesMatch(QRecord record, QRecord oldRecord, WebhookEventType webhookEventType)
{
FirePostUpdateWebhookEvent firePostUpdateWebhookEvent = new FirePostUpdateWebhookEvent();
QFieldMetaData field = QContext.getQInstance().getTable(webhookEventType.getTableName()).getFields().get(webhookEventType.getFieldName());
return firePostUpdateWebhookEvent.doesRecordMatchWebhookEventType(record, Optional.ofNullable(oldRecord), webhookEventType, field);
}
}