webhook: fixes, filter enhancement (#12023)

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2026-01-05 13:42:06 +05:30 committed by GitHub
parent 81b991ae9c
commit cd55796972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 4303 additions and 123 deletions

View File

@ -375,6 +375,7 @@ public class ApiConstants {
public static final String MAC_ADDRESS = "macaddress";
public static final String MAC_ADDRESSES = "macaddresses";
public static final String MANUAL_UPGRADE = "manualupgrade";
public static final String MATCH_TYPE = "matchtype";
public static final String MAX = "max";
public static final String MAX_SNAPS = "maxsnaps";
public static final String MAX_BACKUPS = "maxbackups";

View File

@ -23,3 +23,19 @@
-- Update value to firstfit for the config 'vm.allocation.algorithm' or 'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit
UPDATE `cloud`.`configuration` SET value='random' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_random';
UPDATE `cloud`.`configuration` SET value='firstfit' WHERE name IN ('vm.allocation.algorithm', 'volume.allocation.algorithm') AND value='userconcentratedpod_firstfit';
-- Create webhook_filter table
DROP TABLE IF EXISTS `cloud`.`webhook_filter`;
CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id of the webhook filter',
`uuid` varchar(255) COMMENT 'uuid of the webhook filter',
`webhook_id` bigint unsigned NOT NULL COMMENT 'id of the webhook',
`type` varchar(20) COMMENT 'type of the filter',
`mode` varchar(20) COMMENT 'mode of the filter',
`match_type` varchar(20) COMMENT 'match type of the filter',
`value` varchar(256) NOT NULL COMMENT 'value of the filter used for matching',
`created` datetime NOT NULL COMMENT 'date created',
PRIMARY KEY (`id`),
INDEX `i_webhook_filter__webhook_id`(`webhook_id`),
CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -24,8 +24,8 @@ import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface Webhook extends ControlledEntity, Identity, InternalIdentity {
public static final long ID_DUMMY = 0L;
public static final String NAME_DUMMY = "Test";
long ID_DUMMY = 0L;
String NAME_DUMMY = "Test";
enum State {
Enabled, Disabled;
};

View File

@ -18,14 +18,18 @@
package org.apache.cloudstack.mom.webhook;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.mom.webhook.api.command.user.AddWebhookFilterCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookDeliveryCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookFilterCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ExecuteWebhookDeliveryCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookDeliveriesCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookFiltersCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhooksCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookCmd;
import org.apache.cloudstack.mom.webhook.api.response.WebhookDeliveryResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
import com.cloud.utils.component.PluggableService;
@ -41,4 +45,7 @@ public interface WebhookApiService extends PluggableService {
ListResponse<WebhookDeliveryResponse> listWebhookDeliveries(ListWebhookDeliveriesCmd cmd);
int deleteWebhookDelivery(DeleteWebhookDeliveryCmd cmd) throws CloudRuntimeException;
WebhookDeliveryResponse executeWebhookDelivery(ExecuteWebhookDeliveryCmd cmd) throws CloudRuntimeException;
ListResponse<WebhookFilterResponse> listWebhookFilters(ListWebhookFiltersCmd cmd) throws CloudRuntimeException;
WebhookFilterResponse addWebhookFilter(AddWebhookFilterCmd cmd) throws CloudRuntimeException;
int deleteWebhookFilter(DeleteWebhookFilterCmd cmd) throws CloudRuntimeException;
}

View File

@ -29,23 +29,30 @@ import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.api.command.user.AddWebhookFilterCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.CreateWebhookCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookDeliveryCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.DeleteWebhookFilterCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ExecuteWebhookDeliveryCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookDeliveriesCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhookFiltersCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.ListWebhooksCmd;
import org.apache.cloudstack.mom.webhook.api.command.user.UpdateWebhookCmd;
import org.apache.cloudstack.mom.webhook.api.response.WebhookDeliveryResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
import org.apache.cloudstack.mom.webhook.dao.WebhookDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryJoinDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookFilterDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookJoinDao;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryJoinVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookFilterVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookJoinVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookVO;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@ -59,6 +66,7 @@ import com.cloud.exception.PermissionDeniedException;
import com.cloud.projects.Project;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.EnumUtils;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.UriUtils;
@ -84,6 +92,8 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
@Inject
WebhookDeliveryJoinDao webhookDeliveryJoinDao;
@Inject
WebhookFilterDao webhookFilterDao;
@Inject
ManagementServerHostDao managementServerHostDao;
@Inject
WebhookService webhookService;
@ -225,6 +235,25 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
throw new InvalidParameterValueException(error);
}
protected WebhookFilterResponse createWebhookFilterResponse(WebhookFilter webhookFilter, WebhookVO webhookVO) {
WebhookFilterResponse response = new WebhookFilterResponse();
response.setObjectName("webhookfilter");
response.setId(webhookFilter.getUuid());
if (webhookVO == null) {
webhookVO = webhookDao.findById(webhookFilter.getWebhookId());
}
if (webhookVO != null) {
response.setWebhookId(webhookVO.getUuid());
response.setWebhookName(webhookVO.getName());
}
response.setType(webhookFilter.getType().toString());
response.setMode(webhookFilter.getMode().toString());
response.setMatchType(webhookFilter.getMatchType().toString());
response.setValue(webhookFilter.getValue());
response.setCreated(webhookFilter.getCreated());
return response;
}
@Override
public ListResponse<WebhookResponse> listWebhooks(ListWebhooksCmd cmd) {
final CallContext ctx = CallContext.current();
@ -234,6 +263,25 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
final String name = cmd.getName();
final String keyword = cmd.getKeyword();
final String scopeStr = cmd.getScope();
Webhook.Scope scope = null;
if (StringUtils.isNotEmpty(scopeStr)) {
scope = EnumUtils.getEnumIgnoreCase(Webhook.Scope.class, scopeStr);
if (scope == null) {
throw new InvalidParameterValueException("Invalid scope specified");
}
}
if ((Webhook.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(caller.getType())) ||
(Webhook.Scope.Domain.equals(scope) &&
!List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(caller.getType()))) {
throw new InvalidParameterValueException(String.format("Scope %s can not be specified", scope));
}
Webhook.State state = null;
if (StringUtils.isNotEmpty(stateStr)) {
state = EnumUtils.getEnumIgnoreCase(Webhook.State.class, stateStr);
if (state == null) {
throw new InvalidParameterValueException("Invalid state specified");
}
}
List<WebhookResponse> responsesList = new ArrayList<>();
List<Long> permittedAccounts = new ArrayList<>();
Ternary<Long, Boolean, Project.ListProjectResourcesCriteria> domainIdRecursiveListProject =
@ -258,27 +306,6 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
SearchCriteria<WebhookJoinVO> sc = sb.create();
accountManager.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts,
listProjectResourcesCriteria);
Webhook.Scope scope = null;
if (StringUtils.isNotEmpty(scopeStr)) {
try {
scope = Webhook.Scope.valueOf(scopeStr);
} catch (IllegalArgumentException iae) {
throw new InvalidParameterValueException("Invalid scope specified");
}
}
if ((Webhook.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(caller.getType())) ||
(Webhook.Scope.Domain.equals(scope) &&
!List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(caller.getType()))) {
throw new InvalidParameterValueException(String.format("Scope %s can not be specified", scope));
}
Webhook.State state = null;
if (StringUtils.isNotEmpty(stateStr)) {
try {
state = Webhook.State.valueOf(stateStr);
} catch (IllegalArgumentException iae) {
throw new InvalidParameterValueException("Invalid state specified");
}
}
if (scope != null) {
sc.setParameters("scope", scope.name());
}
@ -316,9 +343,8 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
final String stateStr = cmd.getState();
Webhook.Scope scope = Webhook.Scope.Local;
if (StringUtils.isNotEmpty(scopeStr)) {
try {
scope = Webhook.Scope.valueOf(scopeStr);
} catch (IllegalArgumentException iae) {
scope = EnumUtils.getEnumIgnoreCase(Webhook.Scope.class, scopeStr);
if (scope == null) {
throw new InvalidParameterValueException("Invalid scope specified");
}
}
@ -330,9 +356,8 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
}
Webhook.State state = Webhook.State.Enabled;
if (StringUtils.isNotEmpty(stateStr)) {
try {
state = Webhook.State.valueOf(stateStr);
} catch (IllegalArgumentException iae) {
state = EnumUtils.getEnumIgnoreCase(Webhook.State.class, stateStr);
if (state == null) {
throw new InvalidParameterValueException("Invalid state specified");
}
}
@ -353,6 +378,7 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
WebhookVO webhook = new WebhookVO(name, description, state, domainId, owner.getId(), payloadUrl, secretKey,
sslVerification, scope);
webhook = webhookDao.persist(webhook);
webhookService.invalidateWebhooksCache();
return createWebhookResponse(webhook.getId());
}
@ -365,7 +391,11 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
throw new InvalidParameterValueException("Unable to find the webhook with the specified ID");
}
accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhook);
return webhookDao.remove(id);
boolean removed = webhookDao.remove(id);
if (removed) {
webhookService.invalidateWebhooksCache();
}
return removed;
}
@Override
@ -394,18 +424,19 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
updateNeeded = true;
}
if (StringUtils.isNotEmpty(stateStr)) {
try {
Webhook.State state = Webhook.State.valueOf(stateStr);
webhook.setState(state);
updateNeeded = true;
} catch (IllegalArgumentException iae) {
Webhook.State state = EnumUtils.getEnumIgnoreCase(Webhook.State.class, stateStr);
if (state == null) {
throw new InvalidParameterValueException("Invalid state specified");
}
webhook.setState(state);
updateNeeded = true;
}
Account owner = accountManager.getAccount(webhook.getAccountId());
if (StringUtils.isNotEmpty(scopeStr)) {
try {
Webhook.Scope scope = Webhook.Scope.valueOf(scopeStr);
Webhook.Scope scope = EnumUtils.getEnumIgnoreCase(Webhook.Scope.class, scopeStr);
if (scope == null) {
throw new InvalidParameterValueException("Invalid scope specified");
}
if ((Webhook.Scope.Global.equals(scope) && !Account.Type.ADMIN.equals(owner.getType())) ||
(Webhook.Scope.Domain.equals(scope) &&
!List.of(Account.Type.ADMIN, Account.Type.DOMAIN_ADMIN).contains(owner.getType()))) {
@ -414,9 +445,6 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
}
webhook.setScope(scope);
updateNeeded = true;
} catch (IllegalArgumentException iae) {
throw new InvalidParameterValueException("Invalid scope specified");
}
}
URI uri = URI.create(webhook.getPayloadUrl());
if (StringUtils.isNotEmpty(payloadUrl)) {
@ -427,7 +455,7 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
updateNeeded = true;
}
if (sslVerification != null) {
if (Boolean.TRUE.equals(sslVerification) && !HttpConstants.HTTPS.equalsIgnoreCase(uri.getScheme())) {
if (sslVerification && !HttpConstants.HTTPS.equalsIgnoreCase(uri.getScheme())) {
throw new InvalidParameterValueException(
String.format("SSL verification can be specified only for HTTPS URLs, %s", payloadUrl));
}
@ -444,6 +472,7 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
if (updateNeeded && !webhookDao.update(id, webhook)) {
return null;
}
webhookService.invalidateWebhooksCache();
return createWebhookResponse(webhook.getId());
}
@ -455,8 +484,7 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
@Override
public ListResponse<WebhookDeliveryResponse> listWebhookDeliveries(ListWebhookDeliveriesCmd cmd) {
final CallContext ctx = CallContext.current();
final Account caller = ctx.getCallingAccount();
final Account caller = CallContext.current().getCallingAccount();
final Long id = cmd.getId();
final Long webhookId = cmd.getWebhookId();
final Long managementServerId = cmd.getManagementServerId();
@ -507,20 +535,23 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
@Override
public WebhookDeliveryResponse executeWebhookDelivery(ExecuteWebhookDeliveryCmd cmd) throws CloudRuntimeException {
final CallContext ctx = CallContext.current();
final Account caller = ctx.getCallingAccount();
final Account caller = CallContext.current().getCallingAccount();
final Long deliveryId = cmd.getId();
final Long webhookId = cmd.getWebhookId();
final String payloadUrl = getNormalizedPayloadUrl(cmd.getPayloadUrl());
final String secretKey = cmd.getSecretKey();
final Boolean sslVerification = cmd.isSslVerification();
final String payload = cmd.getPayload();
final Account owner = accountManager.finalizeOwner(caller, null, null, null);
if (ObjectUtils.allNull(deliveryId, webhookId) && StringUtils.isBlank(payloadUrl)) {
throw new InvalidParameterValueException(String.format("One of the %s, %s or %s must be specified",
ApiConstants.ID, ApiConstants.WEBHOOK_ID, ApiConstants.PAYLOAD_URL));
}
if (deliveryId != null && (webhookId != null || StringUtils.isNotBlank(payloadUrl))) {
throw new InvalidParameterValueException(
String.format("%s cannot be specified with %s or %s", ApiConstants.ID, ApiConstants.WEBHOOK_ID,
ApiConstants.PAYLOAD_URL));
}
WebhookDeliveryVO existingDelivery = null;
WebhookVO webhook = null;
if (deliveryId != null) {
@ -545,11 +576,14 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
webhook.setSecretKey(secretKey);
}
if (sslVerification != null) {
webhook.setSslVerification(Boolean.TRUE.equals(sslVerification));
webhook.setSslVerification(sslVerification);
}
}
if (webhook != null) {
accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhook);
}
if (ObjectUtils.allNull(deliveryId, webhookId)) {
webhook = new WebhookVO(owner.getDomainId(), owner.getId(), payloadUrl, secretKey,
webhook = new WebhookVO(caller.getDomainId(), caller.getId(), payloadUrl, secretKey,
Boolean.TRUE.equals(sslVerification));
}
WebhookDelivery webhookDelivery = webhookService.executeWebhookDelivery(existingDelivery, webhook, payload);
@ -559,6 +593,87 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
return createTestWebhookDeliveryResponse(webhookDelivery, webhook);
}
@Override
public ListResponse<WebhookFilterResponse> listWebhookFilters(ListWebhookFiltersCmd cmd) throws CloudRuntimeException {
Pair<List<WebhookFilterVO>, Integer> filtersAndCount = webhookFilterDao.searchBy(cmd.getId(), cmd.getWebhookId(),
cmd.getStartIndex(), cmd.getPageSizeVal());
List<WebhookFilterResponse> responsesList = new ArrayList<>();
WebhookVO webhookVO = null;
if (filtersAndCount.second() > 0) {
webhookVO = webhookDao.findById(filtersAndCount.first().get(0).getWebhookId());
}
for (WebhookFilterVO filter : filtersAndCount.first()) {
WebhookFilterResponse response = createWebhookFilterResponse(filter, webhookVO);
responsesList.add(response);
}
ListResponse<WebhookFilterResponse> response = new ListResponse<>();
response.setResponses(responsesList, responsesList.size());
return response;
}
@Override
public WebhookFilterResponse addWebhookFilter(AddWebhookFilterCmd cmd) throws CloudRuntimeException {
final Account caller = CallContext.current().getCallingAccount();
final long id = cmd.getId();
final String typeStr = cmd.getType();
final String modeStr = cmd.getMode();
final String matchTypeStr = cmd.getMatchType();
final String value = cmd.getValue();
WebhookVO webhook = webhookDao.findById(id);
if (webhook == null) {
throw new InvalidParameterValueException("Unable to find the webhook with the specified ID");
}
accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhook);
WebhookFilter.Type type = EnumUtils.getEnumIgnoreCase(WebhookFilter.Type.class, typeStr, WebhookFilter.Type.EventType);
WebhookFilter.Mode mode = WebhookFilter.Mode.Include;
if (StringUtils.isNotBlank(modeStr)) {
mode = EnumUtils.getEnumIgnoreCase(WebhookFilter.Mode.class, modeStr);
if (mode == null) {
throw new InvalidParameterValueException("Invalid mode specified");
}
}
WebhookFilter.MatchType matchType = WebhookFilter.MatchType.Exact;
if (StringUtils.isNotBlank(matchTypeStr)) {
matchType = EnumUtils.getEnumIgnoreCase(WebhookFilter.MatchType.class, matchTypeStr);
if (matchType == null) {
throw new InvalidParameterValueException("Invalid match type specified");
}
}
WebhookFilterVO webhookFilter = new WebhookFilterVO(webhook.getId(), type, mode, matchType, value);
List<? extends WebhookFilter> existingFilters = webhookFilterDao.listByWebhook(webhook.getId());
if (CollectionUtils.isNotEmpty(existingFilters)) {
WebhookFilter conflicting = webhookFilter.getConflicting(existingFilters);
if (conflicting != null) {
logger.error("Conflict detected when adding WebhookFilter having type: {}, mode: {}, " +
"matchtype: {}, value: {} with existing {} for {}", type, mode, matchType, value, conflicting,
webhook);
throw new InvalidParameterValueException(String.format("Conflicting Webhook filter exists ID: %s",
conflicting.getId()));
}
}
webhookFilter = webhookFilterDao.persist(webhookFilter);
webhookService.invalidateWebhookFiltersCache(webhook.getId());
return createWebhookFilterResponse(webhookFilter, webhook);
}
@Override
public int deleteWebhookFilter(DeleteWebhookFilterCmd cmd) throws CloudRuntimeException {
final Account caller = CallContext.current().getCallingAccount();
final Pair<List<WebhookFilterVO>, Integer> filtersAndCount =
webhookFilterDao.searchBy(cmd.getId(), cmd.getWebhookId(), 0L, 1L);
if (filtersAndCount.second() == 0) {
return 0;
}
final long webhookId = filtersAndCount.first().get(0).getWebhookId();
Webhook webhook = webhookDao.findById(webhookId);
accountManager.checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhook);
int result = webhookFilterDao.delete(cmd.getId(), webhookId);
if (result > 0) {
webhookService.invalidateWebhookFiltersCache(webhookId);
}
return result;
}
@Override
public List<Class<?>> getCommands() {
List<Class<?>> cmdList = new ArrayList<>();
@ -569,6 +684,9 @@ public class WebhookApiServiceImpl extends ManagerBase implements WebhookApiServ
cmdList.add(ListWebhookDeliveriesCmd.class);
cmdList.add(DeleteWebhookDeliveryCmd.class);
cmdList.add(ExecuteWebhookDeliveryCmd.class);
cmdList.add(ListWebhookFiltersCmd.class);
cmdList.add(AddWebhookFilterCmd.class);
cmdList.add(DeleteWebhookFilterCmd.class);
return cmdList;
}
}

View File

@ -0,0 +1,114 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface WebhookFilter extends Identity, InternalIdentity {
enum Type {
EventType
}
enum Mode {
Include, Exclude
}
enum MatchType {
Exact, Prefix, Suffix, Contains
}
long getId();
long getWebhookId();
Type getType();
Mode getMode();
MatchType getMatchType();
String getValue();
Date getCreated();
static boolean overlaps(WebhookFilter.MatchType oldMatchType, String oldValue, WebhookFilter.MatchType newMatchType, String newValue) {
switch (oldMatchType) {
case Exact:
switch (newMatchType) {
case Exact:
return oldValue.equals(newValue);
}
break;
case Prefix:
switch (newMatchType) {
case Exact:
case Prefix:
return newValue.startsWith(oldValue);
}
break;
case Suffix:
switch (newMatchType) {
case Exact:
case Suffix:
return newValue.endsWith(oldValue);
}
break;
case Contains:
switch (newMatchType) {
case Exact:
case Prefix:
case Suffix:
case Contains:
return newValue.contains(oldValue);
}
break;
default:
break;
}
return false;
}
default WebhookFilter getConflicting(List<? extends WebhookFilter> existing) {
for (WebhookFilter f : existing) {
if (f.getType() != this.getType()) {
continue;
}
// 1. Duplicate entry (same mode, match type, and value)
if (f.getMode() == this.getMode()
&& f.getMatchType() == this.getMatchType()
&& f.getValue().equalsIgnoreCase(this.getValue())) {
return f;
}
// 2. Opposite mode (INCLUDE vs EXCLUDE) check for overlap
if (f.getMode() != this.getMode()) {
String oldVal = f.getValue().toUpperCase();
String newVal = this.getValue().toUpperCase();
if (overlaps(f.getMatchType(), oldVal, this.getMatchType(), newVal)) {
return f;
}
}
}
return null;
}
}

View File

@ -60,4 +60,6 @@ public interface WebhookService extends PluggableService, Configurable {
void handleEvent(Event event) throws EventBusException;
WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload)
throws CloudRuntimeException;
void invalidateWebhooksCache();
void invalidateWebhookFiltersCache(long webhookId);
}

View File

@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -32,7 +33,6 @@ import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.framework.async.AsyncCallFuture;
import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.framework.async.AsyncRpcContext;
@ -42,10 +42,14 @@ import org.apache.cloudstack.framework.events.EventBusException;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.mom.webhook.dao.WebhookDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookFilterDao;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookFilterVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookVO;
import org.apache.cloudstack.utils.cache.LazyCache;
import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.webhook.WebhookHelper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.api.query.vo.EventJoinVO;
@ -75,7 +79,9 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
@Inject
WebhookDao webhookDao;
@Inject
protected WebhookDeliveryDao webhookDeliveryDao;
WebhookDeliveryDao webhookDeliveryDao;
@Inject
WebhookFilterDao webhookFilterDao;
@Inject
ManagementServerHostDao managementServerHostDao;
@Inject
@ -83,6 +89,9 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
@Inject
AccountManager accountManager;
protected LazyCache<org.apache.commons.lang3.tuple.Pair<Long, List<Long>>, List<WebhookVO>> webhooksCache;
protected LazyCache<Long, List<WebhookFilterVO>> webhookFiltersCache;
protected WebhookDeliveryThread getDeliveryJob(Event event, Webhook webhook, Pair<Integer, Integer> configs) {
WebhookDeliveryThread.WebhookDeliveryContext<WebhookDeliveryThread.WebhookDeliveryResult> context =
new WebhookDeliveryThread.WebhookDeliveryContext<>(null, event.getEventId(), webhook.getId());
@ -97,13 +106,74 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
return job;
}
protected String getEventValueByFilterType(Event event, WebhookFilter.Type filterType) {
if (WebhookFilter.Type.EventType.equals(filterType)) {
return event.getEventType();
}
return null;
}
protected boolean isValueMatchingFilter(String eventValue, WebhookFilter.MatchType matchType, String filterValue) {
switch (matchType) {
case Exact:
return eventValue.equals(filterValue);
case Prefix:
return eventValue.startsWith(filterValue);
case Suffix:
return eventValue.endsWith(filterValue);
case Contains:
return eventValue.contains(filterValue);
default:
return false;
}
}
protected boolean isEventMatchingFilters(Event event, List<? extends WebhookFilter> filters) {
if (CollectionUtils.isEmpty(filters)) {
return true;
}
boolean hasAnyInclude = false;
boolean anyIncludeMatched = false;
// First pass: short-circuit on any Exclude match; track Include presence/match
for (WebhookFilter f : filters) {
final WebhookFilter.Type type = f.getType();
String eventValue = getEventValueByFilterType(event, type);
if (f.getMode() == WebhookFilter.Mode.Exclude) {
if (eventValue != null && isValueMatchingFilter(eventValue, f.getMatchType(), f.getValue())) {
logger.trace("{} matched Exclude {}, webhook delivery will be skipped", event, f);
return false;
}
continue;
}
if (f.getMode() == WebhookFilter.Mode.Include) {
hasAnyInclude = true;
if (!anyIncludeMatched && eventValue != null &&
isValueMatchingFilter(eventValue, f.getMatchType(), f.getValue())) {
logger.trace("{} matched Include {}", event, f);
anyIncludeMatched = true;
}
}
}
// If there were includes, we must have matched at least one; otherwise allow by default
if (hasAnyInclude && !anyIncludeMatched) {
return false;
}
return true;
}
protected List<Runnable> getDeliveryJobs(Event event) throws EventBusException {
List<Runnable> jobs = new ArrayList<>();
if (!EventCategory.ACTION_EVENT.getName().equals(event.getEventCategory())) {
return jobs;
}
if (event.getResourceAccountId() == null) {
logger.warn("Skipping delivering event {} to any webhook as account ID is missing", event);
logger.warn("Skipping delivering {} to any webhook as account ID is missing", event);
throw new EventBusException(String.format("Account missing for the event ID: %s", event.getEventUuid()));
}
List<Long> domainIds = new ArrayList<>();
@ -112,9 +182,14 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
domainIds.addAll(domainDao.getDomainParentIds(event.getResourceDomainId()));
}
List<WebhookVO> webhooks =
webhookDao.listByEnabledForDelivery(event.getResourceAccountId(), domainIds);
webhooksCache.get(org.apache.commons.lang3.tuple.Pair.of(event.getResourceAccountId(), domainIds));
Map<Long, Pair<Integer, Integer>> domainConfigs = new HashMap<>();
for (WebhookVO webhook : webhooks) {
List<? extends WebhookFilter> filters = webhookFiltersCache.get(webhook.getId());
if (!isEventMatchingFilters(event, filters)) {
logger.debug("Skipping delivering {} to {} as it doesn't match filters", event, webhook);
continue;
}
if (!domainConfigs.containsKey(webhook.getDomainId())) {
domainConfigs.put(webhook.getDomainId(),
new Pair<>(WebhookDeliveryTries.valueIn(webhook.getDomainId()),
@ -128,7 +203,7 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
}
protected Runnable getManualDeliveryJob(WebhookDelivery existingDelivery, Webhook webhook, String payload,
AsyncCallFuture<WebhookDeliveryThread.WebhookDeliveryResult> future) {
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future) {
if (StringUtils.isBlank(payload)) {
payload = "{ \"CloudStack\": \"works!\" }";
}
@ -155,7 +230,7 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
event.setDescription(description);
event.setResourceAccountUuid(resourceAccountUuid);
ManualDeliveryContext<WebhookDeliveryThread.WebhookDeliveryResult> context =
new ManualDeliveryContext<>(null, webhook, future);
new ManualDeliveryContext<>(null, future);
AsyncCallbackDispatcher<WebhookServiceImpl, WebhookDeliveryThread.WebhookDeliveryResult> caller =
AsyncCallbackDispatcher.create(this);
caller.setCallback(caller.getTarget().manualDeliveryCompleteCallback(null, null))
@ -181,7 +256,7 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
AsyncCallbackDispatcher<WebhookServiceImpl, WebhookDeliveryThread.WebhookDeliveryResult> callback,
ManualDeliveryContext<WebhookDeliveryThread.WebhookDeliveryResult> context) {
WebhookDeliveryThread.WebhookDeliveryResult result = callback.getResult();
context.future.complete(result);
context.getFuture().complete(result);
return null;
}
@ -205,8 +280,20 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
return processed;
}
protected void initCaches() {
webhooksCache = new LazyCache<>(
16, 60,
(key) -> webhookDao.listByEnabledForDelivery(key.getLeft(), key.getRight())
);
webhookFiltersCache = new LazyCache<>(
16, 60,
(webhookId) -> webhookFilterDao.listByWebhook(webhookId)
);
}
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
initCaches();
try {
webhookJobExecutor = Executors.newFixedThreadPool(WebhookDeliveryThreadPoolSize.value(),
new NamedThreadFactory(WEBHOOK_JOB_POOL_THREAD_PREFIX));
@ -273,7 +360,7 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
@Override
public WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload)
throws CloudRuntimeException {
AsyncCallFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = new AsyncCallFuture<>();
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = new CompletableFuture<>();
Runnable job = getManualDeliveryJob(delivery, webhook, payload, future);
webhookJobExecutor.submit(job);
WebhookDeliveryThread.WebhookDeliveryResult result = null;
@ -297,22 +384,33 @@ public class WebhookServiceImpl extends ManagerBase implements WebhookService, W
return webhookDeliveryVO;
}
@Override
public void invalidateWebhooksCache() {
webhooksCache.clear();
}
@Override
public void invalidateWebhookFiltersCache(long webhookId) {
webhookFiltersCache.invalidate(webhookId);
}
@Override
public List<Class<?>> getCommands() {
return new ArrayList<>();
}
static public class ManualDeliveryContext<T> extends AsyncRpcContext<T> {
final Webhook webhook;
final AsyncCallFuture<WebhookDeliveryThread.WebhookDeliveryResult> future;
protected static class ManualDeliveryContext<T> extends AsyncRpcContext<T> {
private final CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future;
public ManualDeliveryContext(AsyncCompletionCallback<T> callback, Webhook webhook,
AsyncCallFuture<WebhookDeliveryThread.WebhookDeliveryResult> future) {
super(callback);
this.webhook = webhook;
this.future = future;
public CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> getFuture() {
return future;
}
public ManualDeliveryContext(AsyncCompletionCallback<T> callback,
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future) {
super(callback);
this.future = future;
}
}
public class WebhookDeliveryCleanupWorker extends ManagedContextRunnable {

View File

@ -0,0 +1,118 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.command.user;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.WebhookFilter;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "addWebhookFilter",
description = "Adds a Webhook filter",
responseObject = WebhookResponse.class,
entityType = {WebhookFilter.class},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.23.0")
public class AddWebhookFilterCmd extends BaseCmd {
@Inject
WebhookApiService webhookApiService;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.WEBHOOK_ID, type = CommandType.UUID, required = true,
entityType = WebhookResponse.class, description = "ID for the Webhook")
private Long id;
@Parameter(name = ApiConstants.MODE, type = BaseCmd.CommandType.STRING,
description = "Mode for the Webhook filter - Include or Exclude")
private String mode;
@Parameter(name = ApiConstants.MATCH_TYPE, type = BaseCmd.CommandType.STRING,
description = "Match type for the Webhook filter - Exact, Prefix, Suffix or Contains")
private String matchType;
@Parameter(name = ApiConstants.VALUE, type = BaseCmd.CommandType.STRING, required = true,
description = "Value for the Webhook which that will be matched")
private String value;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccountId();
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getType() {
return WebhookFilter.Type.EventType.name();
}
public String getMode() {
return mode;
}
public String getMatchType() {
return matchType;
}
public String getValue() {
return value;
}
@Override
public void execute() throws ServerApiException {
try {
WebhookFilterResponse response = webhookApiService.addWebhookFilter(this);
if (response == null) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add webhook filter");
}
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (CloudRuntimeException ex) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage());
}
}
}

View File

@ -28,13 +28,12 @@ import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ProjectResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.Webhook;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
import com.cloud.utils.exception.CloudRuntimeException;
@ -42,10 +41,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "createWebhook",
description = "Creates a Webhook",
responseObject = WebhookResponse.class,
responseView = ResponseObject.ResponseView.Restricted,
entityType = {Webhook.class},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = true,
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.20.0")
public class CreateWebhookCmd extends BaseCmd {

View File

@ -0,0 +1,91 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.command.user;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.WebhookFilter;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "deleteWebhookFilter",
description = "Deletes Webhook filter",
responseObject = SuccessResponse.class,
entityType = {WebhookFilter.class},
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.20.0")
public class DeleteWebhookFilterCmd extends BaseCmd {
@Inject
WebhookApiService webhookApiService;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID,
entityType = WebhookFilterResponse.class,
description = "The ID of the Webhook filter")
private Long id;
@Parameter(name = ApiConstants.WEBHOOK_ID, type = BaseCmd.CommandType.UUID,
entityType = WebhookResponse.class,
description = "The ID of the Webhook")
private Long webhookId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public Long getWebhookId() {
return webhookId;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccountId();
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException {
try {
webhookApiService.deleteWebhookFilter(this);
SuccessResponse response = new SuccessResponse(getCommandName());
setResponseObject(response);
} catch (CloudRuntimeException ex) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage());
}
}
}

View File

@ -39,8 +39,6 @@ import com.cloud.utils.exception.CloudRuntimeException;
description = "Executes a Webhook delivery",
responseObject = WebhookDeliveryResponse.class,
entityType = {WebhookDelivery.class},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.20.0")
public class ExecuteWebhookDeliveryCmd extends BaseCmd {

View File

@ -27,7 +27,6 @@ import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ManagementServerResponse;
@ -39,7 +38,6 @@ import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
@APICommand(name = "listWebhookDeliveries",
description = "Lists Webhook deliveries",
responseObject = WebhookResponse.class,
responseView = ResponseObject.ResponseView.Restricted,
entityType = {WebhookDelivery.class},
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.20.0")

View File

@ -0,0 +1,80 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.command.user;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.WebhookFilter;
import org.apache.cloudstack.mom.webhook.api.response.WebhookDeliveryResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
@APICommand(name = "listWebhookFilters",
description = "Lists Webhook filters",
responseObject = WebhookFilterResponse.class,
entityType = {WebhookFilter.class},
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.23.0")
public class ListWebhookFiltersCmd extends BaseListCmd {
@Inject
WebhookApiService webhookApiService;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = BaseCmd.CommandType.UUID,
entityType = WebhookDeliveryResponse.class,
description = "The ID of the Webhook delivery")
private Long id;
@Parameter(name = ApiConstants.WEBHOOK_ID, type = BaseCmd.CommandType.UUID,
entityType = WebhookResponse.class,
description = "The ID of the Webhook")
private Long webhookId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public Long getWebhookId() {
return webhookId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ServerApiException {
ListResponse<WebhookFilterResponse> response = webhookApiService.listWebhookFilters(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}
}

View File

@ -25,17 +25,15 @@ import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.Webhook;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
@APICommand(name = "listWebhooks",
description = "Lists Webhooks",
responseObject = WebhookResponse.class,
responseView = ResponseObject.ResponseView.Restricted,
entityType = {Webhook.class},
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.20.0")

View File

@ -26,17 +26,16 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.Webhook;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.api.response.WebhookResponse;
import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "updateWebhook",
description = "Updates a Webhook",
responseObject = SuccessResponse.class,
responseObject = WebhookResponse.class,
entityType = {Webhook.class},
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
since = "4.20.0")

View File

@ -0,0 +1,95 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.response;
import java.util.Date;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.mom.webhook.WebhookFilter;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
@EntityReference(value = {WebhookFilter.class})
public class WebhookFilterResponse extends BaseResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "The ID of the Webhook filter")
private String id;
@SerializedName(ApiConstants.WEBHOOK_ID)
@Param(description = "The ID of the Webhook")
private String webhookId;
@SerializedName(ApiConstants.WEBHOOK_NAME)
@Param(description = "The name of the Webhook")
private String webhookName;
@SerializedName(ApiConstants.TYPE)
@Param(description = "The type of the Webhook filter")
private String type;
@SerializedName(ApiConstants.MODE)
@Param(description = "The type of the Webhook filter")
private String mode;
@SerializedName(ApiConstants.MATCH_TYPE)
@Param(description = "The type of the Webhook filter")
private String matchType;
@SerializedName(ApiConstants.VALUE)
@Param(description = "The type of the Webhook filter")
private String value;
@SerializedName(ApiConstants.CREATED)
@Param(description = "The type of the Webhook filter")
private Date created;
public void setId(String id) {
this.id = id;
}
public void setWebhookId(String webhookId) {
this.webhookId = webhookId;
}
public void setWebhookName(String webhookName) {
this.webhookName = webhookName;
}
public void setType(String type) {
this.type = type;
}
public void setMode(String mode) {
this.mode = mode;
}
public void setMatchType(String matchType) {
this.matchType = matchType;
}
public void setValue(String value) {
this.value = value;
}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@ -21,6 +21,7 @@ import java.util.Date;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryVO;
import org.apache.commons.collections.CollectionUtils;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
@ -64,6 +65,9 @@ public class WebhookDeliveryDaoImpl extends GenericDaoBase<WebhookDeliveryVO, Lo
SearchCriteria<WebhookDeliveryVO> sc = sb.create();
sc.setParameters("webhookId", webhookId);
List<WebhookDeliveryVO> keep = listBy(sc, searchFilter);
if (CollectionUtils.isEmpty(keep)) {
return;
}
SearchBuilder<WebhookDeliveryVO> sbDelete = createSearchBuilder();
sbDelete.and("id", sbDelete.entity().getId(), SearchCriteria.Op.NOTIN);
SearchCriteria<WebhookDeliveryVO> scDelete = sbDelete.create();

View File

@ -0,0 +1,31 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookFilterVO;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
public interface WebhookFilterDao extends GenericDao<WebhookFilterVO, Long> {
Pair<List<WebhookFilterVO>, Integer> searchBy(Long id, Long webhookId, Long startIndex, Long pageSize);
List<WebhookFilterVO> listByWebhook(Long webhookId);
int delete(Long id, Long webhookId);
}

View File

@ -0,0 +1,79 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookFilterVO;
import org.apache.commons.lang3.ObjectUtils;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
public class WebhookFilterDaoImpl extends GenericDaoBase<WebhookFilterVO, Long> implements WebhookFilterDao {
SearchBuilder<WebhookFilterVO> IdWebhookIdSearch;
public WebhookFilterDaoImpl() {
IdWebhookIdSearch = createSearchBuilder();
IdWebhookIdSearch.and("id", IdWebhookIdSearch.entity().getId(), SearchCriteria.Op.EQ);
IdWebhookIdSearch.and("webhookId", IdWebhookIdSearch.entity().getWebhookId(), SearchCriteria.Op.EQ);
IdWebhookIdSearch.done();
}
@Override
public Pair<List<WebhookFilterVO>, Integer> searchBy(Long id, Long webhookId, Long startIndex, Long pageSize) {
Filter searchFilter = new Filter(WebhookFilterVO.class, "id", false, startIndex,
pageSize);
SearchCriteria<WebhookFilterVO> sc = IdWebhookIdSearch.create();
if (id != null) {
sc.setParameters("id", id);
}
if (webhookId != null) {
sc.setParameters("webhookId", webhookId);
}
return searchAndCount(sc, searchFilter);
}
@Override
public List<WebhookFilterVO> listByWebhook(Long webhookId) {
SearchCriteria<WebhookFilterVO> sc = IdWebhookIdSearch.create();
if (webhookId != null) {
sc.setParameters("webhookId", webhookId);
}
return listBy(sc);
}
@Override
public int delete(Long id, Long webhookId) {
SearchCriteria<WebhookFilterVO> sc = IdWebhookIdSearch.create();
if (ObjectUtils.allNull(id, webhookId)) {
return 0;
}
if (id != null) {
sc.setParameters("id", id);
}
if (webhookId != null) {
sc.setParameters("webhookId", webhookId);
}
return remove(sc);
}
}

View File

@ -0,0 +1,155 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.vo;
import java.util.Date;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.apache.cloudstack.mom.webhook.WebhookFilter;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import com.cloud.utils.db.GenericDao;
@Entity
@Table(name = "webhook_filter")
public class WebhookFilterVO implements WebhookFilter {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@Column(name = "uuid")
private String uuid;
@Column(name = "webhook_id", nullable = false)
private Long webhookId;
@Column(name = "type", length = 20)
@Enumerated(value = EnumType.STRING)
private Type type;
@Column(name = "mode", length = 20)
@Enumerated(value = EnumType.STRING)
private Mode mode;
@Column(name = "match_type", length = 20)
@Enumerated(value = EnumType.STRING)
private MatchType matchType;
@Column(name = "value", nullable = false, length = 128)
private String value;
@Column(name = GenericDao.CREATED_COLUMN)
private Date created;
public WebhookFilterVO() {
this.uuid = UUID.randomUUID().toString();
}
public WebhookFilterVO(Long webhookId, Type type, Mode mode, MatchType matchType, String value) {
this.uuid = UUID.randomUUID().toString();
this.webhookId = webhookId;
this.type = type;
this.mode = mode;
this.matchType = matchType;
this.value = value;
}
@Override
public long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUuid() {
return uuid;
}
@Override
public long getWebhookId() {
return webhookId;
}
public void setWebhookId(Long webhookId) {
this.webhookId = webhookId;
}
@Override
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
@Override
public Mode getMode() {
return mode;
}
public void setMode(Mode mode) {
this.mode = mode;
}
@Override
public MatchType getMatchType() {
return matchType;
}
public void setMatchType(MatchType matchType) {
this.matchType = matchType;
}
@Override
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
@Override
public String toString() {
return String.format("WebhookFilter %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "webhook_id", "type", "mode", "match_type", "value"));
}
}

View File

@ -31,6 +31,7 @@
<bean id="webhookJoinDao" class="org.apache.cloudstack.mom.webhook.dao.WebhookJoinDaoImpl" />
<bean id="webhookDeliveryDao" class="org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryDaoImpl" />
<bean id="webhookDeliveryJoinDao" class="org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryJoinDaoImpl" />
<bean id="webhookFilterDao" class="org.apache.cloudstack.mom.webhook.dao.WebhookFilterDaoImpl" />
<bean id="webhookApiService" class="org.apache.cloudstack.mom.webhook.WebhookApiServiceImpl" />
<bean id="webhookService" class="org.apache.cloudstack.mom.webhook.WebhookServiceImpl" />

View File

@ -0,0 +1,669 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.framework.async.AsyncCallbackDispatcher;
import org.apache.cloudstack.framework.events.Event;
import org.apache.cloudstack.framework.events.EventBusException;
import org.apache.cloudstack.mom.webhook.dao.WebhookDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookDeliveryDao;
import org.apache.cloudstack.mom.webhook.dao.WebhookFilterDao;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryVO;
import org.apache.cloudstack.mom.webhook.vo.WebhookVO;
import org.apache.cloudstack.utils.cache.LazyCache;
import org.apache.commons.lang3.StringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.api.query.vo.EventJoinVO;
import com.cloud.cluster.dao.ManagementServerHostDao;
import com.cloud.domain.dao.DomainDao;
import com.cloud.event.EventCategory;
import com.cloud.event.dao.EventJoinDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.Pair;
import com.cloud.utils.component.ComponentContext;
@RunWith(MockitoJUnitRunner.class)
public class WebhookServiceImplTest {
@Mock
EventJoinDao eventJoinDao;
@Mock
WebhookDao webhookDao;
@Mock
WebhookDeliveryDao webhookDeliveryDao;
@Mock
WebhookFilterDao webhookFilterDao;
@Mock
ManagementServerHostDao managementServerHostDao;
@Mock
DomainDao domainDao;
@Mock
AccountManager accountManager;
@Spy
@InjectMocks
private WebhookServiceImpl webhookServiceImpl;
MockedStatic<ComponentContext> componentContextMockedStatic;
@Before
public void setup() {
componentContextMockedStatic = Mockito.mockStatic(ComponentContext.class);
componentContextMockedStatic.when(() -> ComponentContext.inject(Mockito.any(WebhookDeliveryThread.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
webhookServiceImpl.initCaches();
}
@After
public void tearDown() {
componentContextMockedStatic.close();
}
@Test
public void getDeliveryJobReturnsProperlyConfiguredJob() {
Event event = Mockito.mock(Event.class);
Webhook webhook = Mockito.mock(Webhook.class);
Pair<Integer, Integer> configs = new Pair<>(4, 5000);
Mockito.when(event.getEventId()).thenReturn(123L);
Mockito.when(webhook.getId()).thenReturn(456L);
WebhookDeliveryThread job = webhookServiceImpl.getDeliveryJob(event, webhook, configs);
Assert.assertNotNull(job);
Assert.assertEquals(4, ReflectionTestUtils.getField(job, "deliveryTries"));
Assert.assertEquals(5000, ReflectionTestUtils.getField(job, "deliveryTimeout"));
}
@Test
public void getDeliveryJobInjectsDependencies() {
Event event = Mockito.mock(Event.class);
Webhook webhook = Mockito.mock(Webhook.class);
Pair<Integer, Integer> configs = new Pair<>(1, 1000);
WebhookDeliveryThread job = webhookServiceImpl.getDeliveryJob(event, webhook, configs);
Mockito.verify(webhookServiceImpl, Mockito.times(1)).getDeliveryJob(event, webhook, configs);
componentContextMockedStatic.verify(() -> ComponentContext.inject(job), Mockito.times(1));
}
@Test
public void getEventValueByFilterTypeReturnsEventTypeWhenFilterTypeIsEventType() {
Event event = Mockito.mock(Event.class);
Mockito.when(event.getEventType()).thenReturn("USER.LOGIN");
String result = webhookServiceImpl.getEventValueByFilterType(event, WebhookFilter.Type.EventType);
Assert.assertEquals("USER.LOGIN", result);
}
@Test
public void getEventValueByFilterTypeReturnsNullWhenFilterTypeIsNotEventType() {
Event event = Mockito.mock(Event.class);
String result = webhookServiceImpl.getEventValueByFilterType(event, null);
Assert.assertNull(result);
}
@Test
public void isValueMatchingFilterReturnsTrueForExactMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Exact, "USER.LOGIN");
Assert.assertTrue(result);
}
@Test
public void isValueMatchingFilterReturnsFalseForNonExactMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Exact, "USER.LOGOUT");
Assert.assertFalse(result);
}
@Test
public void isValueMatchingFilterReturnsTrueForPrefixMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Prefix, "USER");
Assert.assertTrue(result);
}
@Test
public void isValueMatchingFilterReturnsFalseForNonPrefixMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Prefix, "ADMIN");
Assert.assertFalse(result);
}
@Test
public void isValueMatchingFilterReturnsTrueForSuffixMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Suffix, "LOGIN");
Assert.assertTrue(result);
}
@Test
public void isValueMatchingFilterReturnsFalseForNonSuffixMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Suffix, "LOGOUT");
Assert.assertFalse(result);
}
@Test
public void isValueMatchingFilterReturnsTrueForContainsMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Contains, "USER");
Assert.assertTrue(result);
}
@Test
public void isValueMatchingFilterReturnsFalseForNonContainsMatch() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Contains, "ADMIN");
Assert.assertFalse(result);
}
@Test
public void isValueMatchingFilterReturnsFalseForNullFilterValue() {
boolean result = webhookServiceImpl.isValueMatchingFilter("USER.LOGIN", WebhookFilter.MatchType.Exact, null);
Assert.assertFalse(result);
}
@Test
public void isEventMatchingFiltersReturnsTrueWhenFiltersAreEmpty() {
List<WebhookFilter> filters = new ArrayList<>();
boolean result = webhookServiceImpl.isEventMatchingFilters(Mockito.mock(Event.class), filters);
Assert.assertTrue(result);
}
@Test
public void isEventMatchingFiltersReturnsFalseWhenEventMatchesExcludeFilter() {
Event event = Mockito.mock(Event.class);
WebhookFilter excludeFilter = Mockito.mock(WebhookFilter.class);
Mockito.when(excludeFilter.getMode()).thenReturn(WebhookFilter.Mode.Exclude);
Mockito.when(excludeFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(excludeFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
Mockito.when(excludeFilter.getValue()).thenReturn("USER.LOGIN");
Mockito.when(event.getEventType()).thenReturn("USER.LOGIN");
List<WebhookFilter> filters = List.of(excludeFilter);
boolean result = webhookServiceImpl.isEventMatchingFilters(event, filters);
Assert.assertFalse(result);
}
@Test
public void isEventMatchingFiltersReturnsTrueWhenEventMatchesIncludeFilter() {
Event event = Mockito.mock(Event.class);
WebhookFilter includeFilter = Mockito.mock(WebhookFilter.class);
Mockito.when(includeFilter.getMode()).thenReturn(WebhookFilter.Mode.Include);
Mockito.when(includeFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(includeFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
Mockito.when(includeFilter.getValue()).thenReturn("USER.LOGIN");
Mockito.when(event.getEventType()).thenReturn("USER.LOGIN");
List<WebhookFilter> filters = List.of(includeFilter);
boolean result = webhookServiceImpl.isEventMatchingFilters(event, filters);
Assert.assertTrue(result);
}
@Test
public void isEventMatchingFiltersReturnsFalseWhenEventDoesNotMatchAnyIncludeFilter() {
Event event = Mockito.mock(Event.class);
WebhookFilter includeFilter = Mockito.mock(WebhookFilter.class);
Mockito.when(includeFilter.getMode()).thenReturn(WebhookFilter.Mode.Include);
Mockito.when(includeFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(includeFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
Mockito.when(includeFilter.getValue()).thenReturn("USER.LOGOUT");
Mockito.when(event.getEventType()).thenReturn("USER.LOGIN");
List<WebhookFilter> filters = List.of(includeFilter);
boolean result = webhookServiceImpl.isEventMatchingFilters(event, filters);
Assert.assertFalse(result);
}
@Test
public void isEventMatchingFiltersReturnsTrueWhenEventMatchesAtLeastOneIncludeFilter() {
Event event = Mockito.mock(Event.class);
WebhookFilter includeFilter1 = Mockito.mock(WebhookFilter.class);
WebhookFilter includeFilter2 = Mockito.mock(WebhookFilter.class);
Mockito.when(includeFilter1.getMode()).thenReturn(WebhookFilter.Mode.Include);
Mockito.when(includeFilter1.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(includeFilter1.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
Mockito.when(includeFilter1.getValue()).thenReturn("USER.LOGOUT");
Mockito.when(includeFilter2.getMode()).thenReturn(WebhookFilter.Mode.Include);
Mockito.when(includeFilter2.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(includeFilter2.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
Mockito.when(includeFilter2.getValue()).thenReturn("USER.LOGIN");
Mockito.when(event.getEventType()).thenReturn("USER.LOGIN");
List<WebhookFilter> filters = List.of(includeFilter1, includeFilter2);
boolean result = webhookServiceImpl.isEventMatchingFilters(event, filters);
Assert.assertTrue(result);
}
@Test
public void isEventMatchingFiltersReturnsFalseWhenEventMatchesExcludeFilterEvenWithIncludeFilters() {
Event event = Mockito.mock(Event.class);
WebhookFilter excludeFilter = Mockito.mock(WebhookFilter.class);
WebhookFilter includeFilter = Mockito.mock(WebhookFilter.class);
Mockito.when(includeFilter.getMode()).thenReturn(WebhookFilter.Mode.Include);
Mockito.when(includeFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(includeFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Prefix);
Mockito.when(includeFilter.getValue()).thenReturn("USER.");
Mockito.when(excludeFilter.getMode()).thenReturn(WebhookFilter.Mode.Exclude);
Mockito.when(excludeFilter.getType()).thenReturn(WebhookFilter.Type.EventType);
Mockito.when(excludeFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact);
Mockito.when(excludeFilter.getValue()).thenReturn("USER.LOGIN");
Mockito.when(event.getEventType()).thenReturn("USER.LOGIN");
List<WebhookFilter> filters = List.of(includeFilter, excludeFilter);
boolean result = webhookServiceImpl.isEventMatchingFilters(event, filters);
Assert.assertFalse(result);
}
@Test
public void getDeliveryJobsReturnsEmptyListWhenEventCategoryIsNotActionEvent() throws EventBusException {
Event event = Mockito.mock(Event.class);
Mockito.when(event.getEventCategory()).thenReturn("NON_ACTION_EVENT");
List<Runnable> jobs = webhookServiceImpl.getDeliveryJobs(event);
Assert.assertTrue(jobs.isEmpty());
}
@Test
public void getDeliveryJobsThrowsExceptionWhenEventAccountIdIsNull() {
Event event = Mockito.mock(Event.class);
Mockito.when(event.getEventCategory()).thenReturn(EventCategory.ACTION_EVENT.getName());
Mockito.when(event.getResourceAccountId()).thenReturn(null);
Assert.assertThrows(EventBusException.class, () -> webhookServiceImpl.getDeliveryJobs(event));
}
@Test
public void getDeliveryJobsReturnsEmptyListWhenNoWebhooksMatchFilters() throws EventBusException {
Event event = Mockito.mock(Event.class);
Mockito.when(event.getEventCategory()).thenReturn(EventCategory.ACTION_EVENT.getName());
Mockito.when(event.getResourceAccountId()).thenReturn(1L);
Mockito.when(event.getResourceDomainId()).thenReturn(2L);
Mockito.when(domainDao.getDomainParentIds(2L)).thenReturn(Set.of(3L));
WebhookVO webhook = Mockito.mock(WebhookVO.class);
Mockito.when(webhook.getId()).thenReturn(1L);
Mockito.when(webhookDao.listByEnabledForDelivery(Mockito.anyLong(), Mockito.anyList())).thenReturn(List.of(webhook));
Mockito.when(webhookFilterDao.listByWebhook(Mockito.anyLong())).thenReturn(List.of());
Mockito.doReturn(false).when(webhookServiceImpl).isEventMatchingFilters(Mockito.any(), Mockito.anyList());
List<Runnable> jobs = webhookServiceImpl.getDeliveryJobs(event);
Assert.assertTrue(jobs.isEmpty());
}
@Test
public void getDeliveryJobsCreatesJobsForMatchingWebhooks() throws EventBusException {
Event event = Mockito.mock(Event.class);
Mockito.when(event.getEventCategory()).thenReturn(EventCategory.ACTION_EVENT.getName());
Mockito.when(event.getResourceAccountId()).thenReturn(1L);
Mockito.when(event.getResourceDomainId()).thenReturn(2L);
Mockito.when(domainDao.getDomainParentIds(2L)).thenReturn(Set.of(3L));
WebhookVO webhook = Mockito.mock(WebhookVO.class);
Mockito.when(webhook.getId()).thenReturn(1L);
Mockito.when(webhook.getDomainId()).thenReturn(2L);
Mockito.when(webhookDao.listByEnabledForDelivery(Mockito.anyLong(), Mockito.anyList())).thenReturn(List.of(webhook));
Mockito.when(webhookFilterDao.listByWebhook(Mockito.anyLong())).thenReturn(List.of());
Mockito.doReturn(true).when(webhookServiceImpl).isEventMatchingFilters(Mockito.any(), Mockito.anyList());
Mockito.doReturn(Mockito.mock(WebhookDeliveryThread.class)).when(webhookServiceImpl).getDeliveryJob(Mockito.any(), Mockito.any(), Mockito.any());
List<Runnable> jobs = webhookServiceImpl.getDeliveryJobs(event);
Assert.assertEquals(1, jobs.size());
}
@Test
public void getDeliveryJobsUsesCachedDomainConfigs() throws EventBusException {
Event event = Mockito.mock(Event.class);
Mockito.when(event.getEventCategory()).thenReturn(EventCategory.ACTION_EVENT.getName());
Mockito.when(event.getResourceAccountId()).thenReturn(1L);
Mockito.when(event.getResourceDomainId()).thenReturn(2L);
Mockito.when(domainDao.getDomainParentIds(2L)).thenReturn(Set.of(3L));
WebhookVO webhook1 = Mockito.mock(WebhookVO.class);
Mockito.when(webhook1.getId()).thenReturn(1L);
Mockito.when(webhook1.getDomainId()).thenReturn(2L);
WebhookVO webhook2 = Mockito.mock(WebhookVO.class);
Mockito.when(webhook2.getId()).thenReturn(2L);
Mockito.when(webhook2.getDomainId()).thenReturn(2L);
Mockito.when(webhookDao.listByEnabledForDelivery(Mockito.anyLong(), Mockito.anyList())).thenReturn(List.of(webhook1, webhook2));
Mockito.when(webhookFilterDao.listByWebhook(Mockito.anyLong())).thenReturn(List.of());
Mockito.doReturn(true).when(webhookServiceImpl).isEventMatchingFilters(Mockito.any(), Mockito.anyList());
Mockito.doReturn(Mockito.mock(WebhookDeliveryThread.class)).when(webhookServiceImpl).getDeliveryJob(Mockito.any(), Mockito.any(), Mockito.any());
List<Runnable> jobs = webhookServiceImpl.getDeliveryJobs(event);
Assert.assertEquals(2, jobs.size());
Mockito.verify(webhookServiceImpl, Mockito.times(1)).getDeliveryJob(Mockito.eq(event), Mockito.eq(webhook1), Mockito.any());
Mockito.verify(webhookServiceImpl, Mockito.times(1)).getDeliveryJob(Mockito.eq(event), Mockito.eq(webhook2), Mockito.any());
}
@Test
public void getManualDeliveryJobCreatesJobWithDefaultPayloadWhenPayloadIsBlank() {
Webhook webhook = Mockito.mock(Webhook.class);
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = Mockito.mock(CompletableFuture.class);
Account account = Mockito.mock(Account.class);
Mockito.when(webhook.getAccountId()).thenReturn(2L);
Mockito.when(accountManager.getAccount(Mockito.anyLong())).thenReturn(account);
Runnable job = webhookServiceImpl.getManualDeliveryJob(null, webhook, " ", future);
Assert.assertNotNull(job);
Event event = (Event) ReflectionTestUtils.getField(job, "event");
Assert.assertNotNull(event);
Assert.assertTrue(StringUtils.isNotBlank(event.getDescription()));
}
@Test
public void getManualDeliveryJobCreatesJobWithExistingDeliveryDetails() {
WebhookDelivery existingDelivery = Mockito.mock(WebhookDelivery.class);
Webhook webhook = Mockito.mock(Webhook.class);
EventJoinVO eventJoinVO = Mockito.mock(EventJoinVO.class);
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = Mockito.mock(CompletableFuture.class);
Mockito.when(existingDelivery.getEventId()).thenReturn(123L);
Mockito.when(eventJoinDao.findById(123L)).thenReturn(eventJoinVO);
Mockito.when(eventJoinVO.getId()).thenReturn(123L);
Mockito.when(eventJoinVO.getType()).thenReturn("TEST.EVENT");
Mockito.when(eventJoinVO.getUuid()).thenReturn("test-uuid");
Mockito.when(existingDelivery.getPayload()).thenReturn("test-payload");
Mockito.when(eventJoinVO.getAccountUuid()).thenReturn("account-uuid");
Runnable job = webhookServiceImpl.getManualDeliveryJob(existingDelivery, webhook, null, future);
Assert.assertNotNull(job);
Mockito.verify(eventJoinDao, Mockito.times(1)).findById(123L);
}
@Test
public void getManualDeliveryJobCreatesJobWithWebhookAccountDetailsWhenNoExistingDelivery() {
Webhook webhook = Mockito.mock(Webhook.class);
Account account = Mockito.mock(Account.class);
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = Mockito.mock(CompletableFuture.class);
Mockito.when(webhook.getAccountId()).thenReturn(1L);
Mockito.when(accountManager.getAccount(1L)).thenReturn(account);
Mockito.when(account.getUuid()).thenReturn("account-uuid");
Runnable job = webhookServiceImpl.getManualDeliveryJob(null, webhook, "test-payload", future);
Assert.assertNotNull(job);
Mockito.verify(accountManager, Mockito.times(1)).getAccount(1L);
}
@Test
public void getManualDeliveryJobSetsDeliveryTriesAndTimeoutFromWebhookDomain() {
Webhook webhook = Mockito.mock(Webhook.class);
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = Mockito.mock(CompletableFuture.class);
Account account = Mockito.mock(Account.class);
Mockito.when(webhook.getDomainId()).thenReturn(2L);
Mockito.when(webhook.getAccountId()).thenReturn(2L);
Mockito.when(accountManager.getAccount(Mockito.anyLong())).thenReturn(account);
WebhookDeliveryThread job = (WebhookDeliveryThread) webhookServiceImpl.getManualDeliveryJob(null, webhook, "test-payload", future);
Assert.assertEquals(3, ReflectionTestUtils.getField(job, "deliveryTries"));
Assert.assertEquals(10, ReflectionTestUtils.getField(job, "deliveryTimeout"));
}
@Test
public void deliveryCompleteCallbackPersistsDeliveryVO() {
WebhookDeliveryThread.WebhookDeliveryResult result = Mockito.mock(WebhookDeliveryThread.WebhookDeliveryResult.class);
WebhookDeliveryThread.WebhookDeliveryContext<Webhook> context = Mockito.mock(WebhookDeliveryThread.WebhookDeliveryContext.class);
Mockito.when(context.getEventId()).thenReturn(123L);
Mockito.when(context.getRuleId()).thenReturn(456L);
Mockito.when(result.getHeaders()).thenReturn("headers");
Mockito.when(result.getPayload()).thenReturn("payload");
Mockito.when(result.isSuccess()).thenReturn(true);
Mockito.when(result.getResult()).thenReturn("result");
AsyncCallbackDispatcher<WebhookServiceImpl, WebhookDeliveryThread.WebhookDeliveryResult> callback = Mockito.mock(AsyncCallbackDispatcher.class);
Mockito.when(callback.getResult()).thenReturn(result);
webhookServiceImpl.deliveryCompleteCallback(callback, context);
Mockito.verify(webhookDeliveryDao, Mockito.times(1)).persist(Mockito.any(WebhookDeliveryVO.class));
}
@Test
public void manualDeliveryCompleteCallbackCompletesFuture() {
WebhookDeliveryThread.WebhookDeliveryResult result = Mockito.mock(WebhookDeliveryThread.WebhookDeliveryResult.class);
WebhookServiceImpl.ManualDeliveryContext<WebhookDeliveryThread.WebhookDeliveryResult> context = Mockito.mock(WebhookServiceImpl.ManualDeliveryContext.class);
CompletableFuture<WebhookDeliveryThread.WebhookDeliveryResult> future = Mockito.mock(CompletableFuture.class);
Mockito.when(context.getFuture()).thenReturn(future);
AsyncCallbackDispatcher<WebhookServiceImpl, WebhookDeliveryThread.WebhookDeliveryResult> callback = Mockito.mock(AsyncCallbackDispatcher.class);
Mockito.when(callback.getResult()).thenReturn(result);
webhookServiceImpl.manualDeliveryCompleteCallback(callback, context);
Mockito.verify(future, Mockito.times(1)).complete(result);
}
@Test
public void cleanupOldWebhookDeliveriesProcessesAllWebhooks() {
WebhookVO webhook1 = Mockito.mock(WebhookVO.class);
WebhookVO webhook2 = Mockito.mock(WebhookVO.class);
Mockito.when(webhook1.getId()).thenReturn(1L);
Mockito.when(webhook2.getId()).thenReturn(2L);
List<WebhookVO> webhooks = List.of(webhook1, webhook2);
Pair<List<WebhookVO>, Integer> webhooksAndCount = new Pair<>(webhooks, 2);
Mockito.when(webhookDao.searchAndCount(Mockito.any(), Mockito.any())).thenReturn(webhooksAndCount);
long processed = webhookServiceImpl.cleanupOldWebhookDeliveries(10);
Assert.assertEquals(2, processed);
Mockito.verify(webhookDeliveryDao, Mockito.times(1)).removeOlderDeliveries(1L, 10);
Mockito.verify(webhookDeliveryDao, Mockito.times(1)).removeOlderDeliveries(2L, 10);
}
@Test
public void listWebhooksByAccountReturnsEmptyListWhenNoWebhooksExist() {
Mockito.when(webhookDao.listByAccount(1L)).thenReturn(new ArrayList<>());
List<? extends ControlledEntity> result = webhookServiceImpl.listWebhooksByAccount(1L);
Assert.assertTrue(result.isEmpty());
}
@Test
public void listWebhooksByAccountReturnsWebhooksForValidAccount() {
WebhookVO webhook = Mockito.mock(WebhookVO.class);
Mockito.when(webhookDao.listByAccount(1L)).thenReturn(List.of(webhook));
List<? extends ControlledEntity> result = webhookServiceImpl.listWebhooksByAccount(1L);
Assert.assertEquals(1, result.size());
Assert.assertEquals(webhook, result.get(0));
}
@Test
public void handleEventSubmitsJobsToExecutor() throws EventBusException {
Event event = Mockito.mock(Event.class);
Runnable job1 = Mockito.mock(Runnable.class);
Runnable job2 = Mockito.mock(Runnable.class);
ExecutorService webhookJobExecutor = Mockito.mock(ExecutorService.class);
ReflectionTestUtils.setField(webhookServiceImpl, "webhookJobExecutor", webhookJobExecutor);
Mockito.doReturn(List.of(job1, job2)).when(webhookServiceImpl).getDeliveryJobs(event);
webhookServiceImpl.handleEvent(event);
Mockito.verify(webhookJobExecutor, Mockito.times(1)).submit(job1);
Mockito.verify(webhookJobExecutor, Mockito.times(1)).submit(job2);
}
@Test
public void handleEventDoesNotSubmitJobsWhenNoJobsExist() throws EventBusException {
Event event = Mockito.mock(Event.class);
ExecutorService webhookJobExecutor = Mockito.mock(ExecutorService.class);
ReflectionTestUtils.setField(webhookServiceImpl, "webhookJobExecutor", webhookJobExecutor);
Mockito.doReturn(new ArrayList<>()).when(webhookServiceImpl).getDeliveryJobs(event);
webhookServiceImpl.handleEvent(event);
Mockito.verify(webhookJobExecutor, Mockito.never()).submit(Mockito.any(Runnable.class));
}
@Test
public void executeWebhookDeliveryPersistsDeliveryWhenDeliveryExists() {
WebhookDelivery delivery = Mockito.mock(WebhookDelivery.class);
Webhook webhook = Mockito.mock(Webhook.class);
WebhookDeliveryThread.WebhookDeliveryResult result = Mockito.mock(WebhookDeliveryThread.WebhookDeliveryResult.class);
Mockito.when(delivery.getEventId()).thenReturn(123L);
Mockito.when(delivery.getWebhookId()).thenReturn(456L);
Mockito.when(result.getHeaders()).thenReturn("headers");
Mockito.when(result.getPayload()).thenReturn("payload");
Mockito.when(result.isSuccess()).thenReturn(true);
Mockito.when(result.getResult()).thenReturn("result");
Mockito.when(result.getStarTime()).thenReturn(new Date(System.currentTimeMillis() - (2 * 1000L)));
Mockito.when(result.getEndTime()).thenReturn(new Date(System.currentTimeMillis()));
Mockito.when(eventJoinDao.findById(123L)).thenReturn(Mockito.mock(EventJoinVO.class));
ExecutorService executorService = Mockito.mock(ExecutorService.class);
Mockito.when(executorService.submit(Mockito.any(Runnable.class))).thenAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
WebhookDeliveryThread webhookDeliveryThread = (WebhookDeliveryThread) runnable;
webhookDeliveryThread.callback.complete(result);
return CompletableFuture.completedFuture(null);
});
ReflectionTestUtils.setField(webhookServiceImpl, "webhookJobExecutor", executorService);
WebhookDeliveryVO persistedDelivery = Mockito.mock(WebhookDeliveryVO.class);
Mockito.when(webhookDeliveryDao.persist(Mockito.any(WebhookDeliveryVO.class))).thenReturn(persistedDelivery);
WebhookDelivery returnedDelivery = webhookServiceImpl.executeWebhookDelivery(delivery, webhook, "payload");
Assert.assertEquals(persistedDelivery, returnedDelivery);
Mockito.verify(webhookDeliveryDao, Mockito.times(1)).persist(Mockito.any(WebhookDeliveryVO.class));
}
@Test
public void executeWebhookDeliveryCreatesAndReturnsNewDeliveryWhenDeliveryIsNull() {
Webhook webhook = Mockito.mock(Webhook.class);
WebhookDeliveryThread.WebhookDeliveryResult result =
Mockito.mock(WebhookDeliveryThread.WebhookDeliveryResult.class);
Account account = Mockito.mock(Account.class);
Mockito.when(webhook.getAccountId()).thenReturn(2L);
Mockito.when(accountManager.getAccount(Mockito.anyLong())).thenReturn(account);
Mockito.when(result.getHeaders()).thenReturn("headers");
Mockito.when(result.getPayload()).thenReturn("payload");
Mockito.when(result.isSuccess()).thenReturn(true);
Mockito.when(result.getResult()).thenReturn("result");
Mockito.when(result.getStarTime()).thenReturn(new Date(System.currentTimeMillis() - (2 * 1000L)));
Mockito.when(result.getEndTime()).thenReturn(new Date(System.currentTimeMillis()));
ExecutorService executorService = Mockito.mock(ExecutorService.class);
Mockito.when(executorService.submit(Mockito.any(Runnable.class))).thenAnswer(invocation -> {
System.out.println("Submitting runnable to executor");
Runnable runnable = invocation.getArgument(0);
WebhookDeliveryThread webhookDeliveryThread = (WebhookDeliveryThread) runnable;
webhookDeliveryThread.callback.complete(result);
return CompletableFuture.completedFuture(null);
});
ReflectionTestUtils.setField(webhookServiceImpl, "webhookJobExecutor", executorService);
WebhookDelivery returnedDelivery = webhookServiceImpl.executeWebhookDelivery(null, webhook, "payload");
Assert.assertNotNull(returnedDelivery);
Assert.assertEquals("headers", returnedDelivery.getHeaders());
Assert.assertEquals("payload", returnedDelivery.getPayload());
Assert.assertTrue(returnedDelivery.isSuccess());
Assert.assertEquals("result", returnedDelivery.getResponse());
}
@Test
public void invalidateWebhooksCacheClearsCache() {
LazyCache<?, ?> cache = Mockito.mock(LazyCache.class);
ReflectionTestUtils.setField(webhookServiceImpl, "webhooksCache", cache);
webhookServiceImpl.invalidateWebhooksCache();
Mockito.verify(cache, Mockito.times(1)).clear();
}
@Test
public void invalidateWebhookFiltersCacheInvalidatesSpecificCacheEntry() {
LazyCache<Long, ?> cache = Mockito.mock(LazyCache.class);
ReflectionTestUtils.setField(webhookServiceImpl, "webhookFiltersCache", cache);
webhookServiceImpl.invalidateWebhookFiltersCache(123L);
Mockito.verify(cache, Mockito.times(1)).invalidate(123L);
}
}

View File

@ -0,0 +1,110 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.command.user;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(MockitoJUnitRunner.class)
public class AddWebhookFilterCmdTest {
@Mock
WebhookApiService webhookApiService;
@Test
public void executeAddsWebhookFilterSuccessfully() {
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
cmd.webhookApiService = webhookApiService;
WebhookFilterResponse response = Mockito.mock(WebhookFilterResponse.class);
Mockito.when(webhookApiService.addWebhookFilter(cmd)).thenReturn(response);
cmd.execute();
Mockito.verify(webhookApiService, Mockito.times(1)).addWebhookFilter(cmd);
Assert.assertNotNull(cmd.getResponseObject());
Assert.assertEquals(response, cmd.getResponseObject());
}
@Test(expected = ServerApiException.class)
public void executeThrowsExceptionWhenServiceReturnsNull() {
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
cmd.webhookApiService = webhookApiService;
Mockito.when(webhookApiService.addWebhookFilter(cmd)).thenReturn(null);
cmd.execute();
}
@Test(expected = ServerApiException.class)
public void executeThrowsExceptionWhenServiceFails() {
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
cmd.webhookApiService = webhookApiService;
Mockito.doThrow(new CloudRuntimeException("Service failure")).when(webhookApiService).addWebhookFilter(cmd);
cmd.execute();
}
@Test
public void getEntityOwnerIdReturnsCorrectOwnerId() {
Account account = Mockito.mock(Account.class);
Mockito.when(account.getId()).thenReturn(123L);
CallContext.register(Mockito.mock(User.class), account);
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
Assert.assertEquals(123L, cmd.getEntityOwnerId());
}
@Test
public void getModeReturnsCorrectValue() {
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
ReflectionTestUtils.setField(cmd, "mode", "Include");
Assert.assertEquals("Include", cmd.getMode());
}
@Test
public void getMatchTypeReturnsCorrectValue() {
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
ReflectionTestUtils.setField(cmd, "matchType", "Exact");
Assert.assertEquals("Exact", cmd.getMatchType());
}
@Test
public void getValueReturnsCorrectValue() {
AddWebhookFilterCmd cmd = new AddWebhookFilterCmd();
ReflectionTestUtils.setField(cmd, "value", "testValue");
Assert.assertEquals("testValue", cmd.getValue());
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cloudstack.mom.webhook.api.command.user;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.UUID;
import org.apache.cloudstack.api.ServerApiException;
@ -105,4 +106,38 @@ public class DeleteWebhookDeliveryCmdTest {
cmd.execute();
Assert.assertNotNull(cmd.getResponseObject());
}
@Test
public void getStartDateReturnsCorrectValue() {
DeleteWebhookDeliveryCmd cmd = new DeleteWebhookDeliveryCmd();
Date date = new Date();
ReflectionTestUtils.setField(cmd, "startDate", date);
Assert.assertEquals(date, cmd.getStartDate());
}
@Test
public void getStartDateReturnsNullWhenNotSet() {
DeleteWebhookDeliveryCmd cmd = new DeleteWebhookDeliveryCmd();
ReflectionTestUtils.setField(cmd, "startDate", null);
Assert.assertNull(cmd.getStartDate());
}
@Test
public void getEndDateReturnsCorrectValue() {
DeleteWebhookDeliveryCmd cmd = new DeleteWebhookDeliveryCmd();
Date date = new Date();
ReflectionTestUtils.setField(cmd, "endDate", date);
Assert.assertEquals(date, cmd.getEndDate());
}
@Test
public void getEndDateReturnsNullWhenNotSet() {
DeleteWebhookDeliveryCmd cmd = new DeleteWebhookDeliveryCmd();
ReflectionTestUtils.setField(cmd, "endDate", null);
Assert.assertNull(cmd.getEndDate());
}
}

View File

@ -0,0 +1,113 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.command.user;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.user.Account;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.UserVO;
import com.cloud.utils.exception.CloudRuntimeException;
@RunWith(MockitoJUnitRunner.class)
public class DeleteWebhookFilterCmdTest {
@Mock
WebhookApiService webhookApiService;
private Object getCommandMethodValue(Object obj, String methodName) {
Object result = null;
try {
Method method = obj.getClass().getMethod(methodName);
result = method.invoke(obj);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
Assert.fail(String.format("Failed to get method %s value", methodName));
}
return result;
}
private void runLongMemberTest(String memberName) {
String methodName = "get" + memberName.substring(0, 1).toUpperCase() + memberName.substring(1);
DeleteWebhookDeliveryCmd cmd = new DeleteWebhookDeliveryCmd();
ReflectionTestUtils.setField(cmd, memberName, null);
Assert.assertNull(getCommandMethodValue(cmd, methodName));
Long value = 100L;
ReflectionTestUtils.setField(cmd, memberName, value);
Assert.assertEquals(value, getCommandMethodValue(cmd, methodName));
}
@Test
public void testGetId() {
runLongMemberTest("id");
}
@Test
public void testGetWebhookId() {
runLongMemberTest("webhookId");
}
@Test
public void executeDeletesWebhookFilterSuccessfully() {
DeleteWebhookFilterCmd cmd = new DeleteWebhookFilterCmd();
cmd.webhookApiService = webhookApiService;
Mockito.when(webhookApiService.deleteWebhookFilter(cmd)).thenReturn(1);
cmd.execute();
Mockito.verify(webhookApiService, Mockito.times(1)).deleteWebhookFilter(cmd);
Assert.assertNotNull(cmd.getResponseObject());
Assert.assertTrue(cmd.getResponseObject() instanceof SuccessResponse);
Assert.assertEquals(cmd.getCommandName(), ((SuccessResponse) cmd.getResponseObject()).getResponseName());
}
@Test(expected = ServerApiException.class)
public void executeThrowsExceptionWhenServiceFails() {
DeleteWebhookFilterCmd cmd = new DeleteWebhookFilterCmd();
cmd.webhookApiService = webhookApiService;
Mockito.doThrow(new CloudRuntimeException("Service failure")).when(webhookApiService).deleteWebhookFilter(cmd);
cmd.execute();
}
@Test
public void getEntityOwnerIdReturnsCallingAccountId() {
Account account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid");
UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone", UUID.randomUUID().toString(), User.Source.UNKNOWN);
CallContext.register(user, account);
DeleteWebhookFilterCmd cmd = new DeleteWebhookFilterCmd();
Assert.assertEquals(account.getId(), cmd.getEntityOwnerId());
}
}

View File

@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.api.command.user;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.mom.webhook.WebhookApiService;
import org.apache.cloudstack.mom.webhook.api.response.WebhookFilterResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
public class ListWebhookFiltersCmdTest {
@Mock
WebhookApiService webhookApiService;
@Test
public void executeSetsResponseNameCorrectly() {
ListWebhookFiltersCmd cmd = new ListWebhookFiltersCmd();
cmd.webhookApiService = webhookApiService;
ListResponse<WebhookFilterResponse> response = new ListResponse<>();
Mockito.when(webhookApiService.listWebhookFilters(cmd)).thenReturn(response);
cmd.execute();
Assert.assertNotNull(cmd.getResponseObject());
}
@Test(expected = ServerApiException.class)
public void executeThrowsExceptionWhenServiceFails() {
ListWebhookFiltersCmd cmd = new ListWebhookFiltersCmd();
cmd.webhookApiService = webhookApiService;
Mockito.doThrow(new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Service failure")).when(webhookApiService).listWebhookFilters(cmd);
cmd.execute();
}
@Test
public void getIdReturnsCorrectValue() {
ListWebhookFiltersCmd cmd = new ListWebhookFiltersCmd();
ReflectionTestUtils.setField(cmd, "id", 123L);
Assert.assertEquals(Long.valueOf(123L), cmd.getId());
}
@Test
public void getWebhookIdReturnsCorrectValue() {
ListWebhookFiltersCmd cmd = new ListWebhookFiltersCmd();
ReflectionTestUtils.setField(cmd, "webhookId", 456L);
Assert.assertEquals(Long.valueOf(456L), cmd.getWebhookId());
}
}

View File

@ -0,0 +1,170 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import org.apache.cloudstack.mom.webhook.Webhook;
import org.apache.cloudstack.mom.webhook.vo.WebhookVO;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class WebhookDaoImplTest {
@Spy
@InjectMocks
private WebhookDaoImpl webhookDao;
@Mock
private WebhookVO mockWebhookVO;
@Mock
private SearchBuilder<WebhookVO> mockSearchBuilder;
@Mock
private SearchCriteria<WebhookVO> mockSearchCriteria;
@Before
public void setUp() {
when(mockSearchBuilder.entity()).thenReturn(mockWebhookVO);
when(mockSearchBuilder.and()).thenReturn(mockSearchBuilder);
when(mockSearchBuilder.or()).thenReturn(mockSearchBuilder);
when(mockSearchBuilder.create()).thenReturn(mockSearchCriteria);
doReturn(mockSearchBuilder).when(webhookDao).createSearchBuilder();
webhookDao.accountIdSearch = mockSearchBuilder;
}
@Test
public void listByEnabledForDeliveryReturnsWebhooksWhenAccountIdAndDomainIdsMatch() {
Long accountId = 1L;
List<Long> domainIds = List.of(2L, 3L);
doReturn(List.of(mockWebhookVO)).when(webhookDao).listBy(any(SearchCriteria.class));
List<WebhookVO> result = webhookDao.listByEnabledForDelivery(accountId, domainIds);
assertNotNull(result);
assertFalse(result.isEmpty());
verify(mockSearchCriteria).setParameters("state", Webhook.State.Enabled.name());
verify(mockSearchCriteria).setParameters("scopeGlobal", Webhook.Scope.Global.name());
verify(mockSearchCriteria).setParameters("scopeLocal", Webhook.Scope.Local.name());
verify(mockSearchCriteria).setParameters("scopeDomain", Webhook.Scope.Domain.name());
verify(mockSearchCriteria).setParameters("domainId", 2L, 3L);
}
@Test
public void listByEnabledForDeliveryReturnsEmptyWhenNoMatchFound() {
Long accountId = 100L;
List<Long> domainIds = Collections.emptyList();
doReturn(Collections.emptyList()).when(webhookDao).listBy(any(SearchCriteria.class));
List<WebhookVO> result = webhookDao.listByEnabledForDelivery(accountId, domainIds);
assertNotNull(result);
assertTrue(result.isEmpty());
verify(mockSearchCriteria, never()).setParameters("scopeDomain", Webhook.Scope.Domain.name());
verify(mockSearchCriteria, never()).setParameters(eq("domainId"), any());
}
@Test
public void deleteByAccountRemovesWebhooksForGivenAccountId() {
long accountId = 1L;
doReturn(1).when(webhookDao).remove(any(SearchCriteria.class));
webhookDao.deleteByAccount(accountId);
verify(webhookDao, times(1)).remove(any(SearchCriteria.class));
verify(mockSearchCriteria).setParameters("accountId", accountId);
}
@Test
public void listByAccountReturnsWebhooksForGivenAccountId() {
long accountId = 1L;
doReturn(List.of(mockWebhookVO)).when(webhookDao).listBy(any(SearchCriteria.class));
List<WebhookVO> result = webhookDao.listByAccount(accountId);
assertNotNull(result);
assertFalse(result.isEmpty());
verify(mockSearchCriteria).setParameters("accountId", accountId);
}
@Test
public void listByAccountReturnsEmptyWhenNoWebhooksExistForAccountId() {
long accountId = 1L;
doReturn(Collections.emptyList()).when(webhookDao).listBy(any(SearchCriteria.class));
List<WebhookVO> result = webhookDao.listByAccount(accountId);
assertNotNull(result);
assertTrue(result.isEmpty());
verify(mockSearchCriteria).setParameters("accountId", accountId);
}
@Test
public void findByAccountAndPayloadUrlReturnsWebhookWhenMatchFound() {
long accountId = 1L;
String payloadUrl = "http://example.com";
doReturn(mockWebhookVO).when(webhookDao).findOneBy(any());
WebhookVO result = webhookDao.findByAccountAndPayloadUrl(accountId, payloadUrl);
assertNotNull(result);
verify(mockSearchCriteria).setParameters("accountId", accountId);
verify(mockSearchCriteria).setParameters("payloadUrl", payloadUrl);
}
@Test
public void findByAccountAndPayloadUrlReturnsNullWhenNoMatchFound() {
long accountId = 1L;
String payloadUrl = "http://example.com";
doReturn(null).when(webhookDao).findOneBy(any());
WebhookVO result = webhookDao.findByAccountAndPayloadUrl(accountId, payloadUrl);
assertNull(result);
verify(mockSearchCriteria).setParameters("accountId", accountId);
verify(mockSearchCriteria).setParameters("payloadUrl", payloadUrl);
}
}

View File

@ -0,0 +1,126 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryVO;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class WebhookDeliveryDaoImplTest {
@Spy
@InjectMocks
private WebhookDeliveryDaoImpl webhookDeliveryDao;
@Mock
private WebhookDeliveryVO mockWebhookDeliveryVO;
@Mock
private SearchBuilder<WebhookDeliveryVO> mockSearchBuilder;
@Mock
private SearchCriteria<WebhookDeliveryVO> mockSearchCriteria;
@Before
public void setUp() {
when(mockSearchBuilder.entity()).thenReturn(mockWebhookDeliveryVO);
when(mockSearchBuilder.create()).thenReturn(mockSearchCriteria);
doReturn(mockSearchBuilder).when(webhookDeliveryDao).createSearchBuilder();
}
@Test
public void deleteByDeleteApiParamsDeletesWhenParametersMatch() {
Long webhookId = 2L;
Date startDate = new Date(System.currentTimeMillis() - 10000);
doReturn(1).when(webhookDeliveryDao).remove(any(SearchCriteria.class));
int result = webhookDeliveryDao.deleteByDeleteApiParams(null, webhookId, null, startDate, null);
assertEquals(1, result);
verify(webhookDeliveryDao).remove(any(SearchCriteria.class));
verify(mockSearchBuilder).and(eq("webhookId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("webhookId", webhookId);
}
@Test
public void deleteByDeleteApiParamsReturnsZeroWhenNoMatchFound() {
Long id = 999L;
Long webhookId = 999L;
Long managementServerId = 999L;
Date startDate = new Date(System.currentTimeMillis() - 10000);
Date endDate = new Date();
doReturn(0).when(webhookDeliveryDao).remove(any(SearchCriteria.class));
int result = webhookDeliveryDao.deleteByDeleteApiParams(id, webhookId, managementServerId, startDate, endDate);
assertEquals(0, result);
}
@Test
public void removeOlderDeliveriesWhenParametersMatch() {
long webhookId = 2L;
WebhookDeliveryVO d1 = mock(WebhookDeliveryVO.class);
when(d1.getId()).thenReturn(1L);
WebhookDeliveryVO d2 = mock(WebhookDeliveryVO.class);
when(d2.getId()).thenReturn(2L);
List<WebhookDeliveryVO> list = List.of(d1, d2);
doReturn(list).when(webhookDeliveryDao).listBy(any(SearchCriteria.class), any());
doReturn(10).when(webhookDeliveryDao).remove(any(SearchCriteria.class));
webhookDeliveryDao.removeOlderDeliveries(webhookId, 10);
verify(webhookDeliveryDao).remove(any(SearchCriteria.class));
verify(mockSearchBuilder).and(eq("webhookId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("webhookId", webhookId);
verify(mockSearchBuilder).and(eq("id"), any(), eq(SearchCriteria.Op.NOTIN));
verify(mockSearchCriteria).setParameters("id", 1L, 2L);
}
@Test
public void removeOlderDeliveriesWhenNoKeepDeliveries() {
long webhookId = 2L;
doReturn(Collections.emptyList()).when(webhookDeliveryDao).listBy(any(SearchCriteria.class), any());
webhookDeliveryDao.removeOlderDeliveries(webhookId, 10);
verify(webhookDeliveryDao, never()).remove(any(SearchCriteria.class));
verify(mockSearchBuilder).and(eq("webhookId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("webhookId", webhookId);
verify(mockSearchBuilder, never()).and(eq("id"), any(), eq(SearchCriteria.Op.NOTIN));
}
}

View File

@ -0,0 +1,129 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Date;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookDeliveryJoinVO;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class WebhookDeliveryJoinDaoImplTest {
@Spy
@InjectMocks
private WebhookDeliveryJoinDaoImpl webhookDeliveryJoinDao;
@Mock
private WebhookDeliveryJoinVO mockWebhookDeliveryJoinVO;
@Mock
private SearchBuilder<WebhookDeliveryJoinVO> mockSearchBuilder;
@Mock
private SearchCriteria<WebhookDeliveryJoinVO> mockSearchCriteria;
@Before
public void setUp() {
when(mockSearchBuilder.entity()).thenReturn(mockWebhookDeliveryJoinVO);
when(mockSearchBuilder.create()).thenReturn(mockSearchCriteria);
doReturn(mockSearchBuilder).when(webhookDeliveryJoinDao).createSearchBuilder();
}
@Test
public void searchAndCountByListApiParametersId() {
long id = 1L;
doReturn(new Pair(List.of(mockWebhookDeliveryJoinVO), 1)).when(webhookDeliveryJoinDao)
.searchAndCount(any(), any());
Pair<List<WebhookDeliveryJoinVO>, Integer> result =
webhookDeliveryJoinDao.searchAndCountByListApiParameters(id, null, null,
null, null, null, null,null);
assertNotNull(result);
assertTrue(result.second() > 0);
assertFalse(result.first().isEmpty());
verify(mockSearchBuilder).and(eq("id"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("id", id);
}
@Test
public void searchAndCountByListApiParametersWebhookId() {
long webhookId = 1L;
doReturn(new Pair(List.of(mockWebhookDeliveryJoinVO), 1)).when(webhookDeliveryJoinDao)
.searchAndCount(any(), any());
Pair<List<WebhookDeliveryJoinVO>, Integer> result =
webhookDeliveryJoinDao.searchAndCountByListApiParameters(null, List.of(webhookId),
null, null, null, null, null, null);
assertNotNull(result);
assertTrue(result.second() > 0);
assertFalse(result.first().isEmpty());
verify(mockSearchBuilder).and(eq("webhookId"), any(), eq(SearchCriteria.Op.IN));
verify(mockSearchCriteria).setParameters("webhookId", 1L);
}
@Test
public void searchAndCountByListApiParametersMgmtKeywordStartEnd() {
long managementServerId = 1L;
String keyword = "error";
Date start = new Date(System.currentTimeMillis() - 10000);
Date end = new Date();
Filter searchFilter = new Filter(WebhookDeliveryJoinVO.class, "id", false, 10L, 10L);
doReturn(new Pair(List.of(mockWebhookDeliveryJoinVO), 1)).when(webhookDeliveryJoinDao)
.searchAndCount(any(), eq(searchFilter));
Pair<List<WebhookDeliveryJoinVO>, Integer> result =
webhookDeliveryJoinDao.searchAndCountByListApiParameters(null, null,
managementServerId, keyword, start, end, null, searchFilter);
assertNotNull(result);
assertTrue(result.second() > 0);
assertFalse(result.first().isEmpty());
verify(mockSearchBuilder).and(eq("managementServerId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("managementServerId", managementServerId);
verify(mockSearchBuilder).and(eq("keyword"), any(), eq(SearchCriteria.Op.LIKE));
verify(mockSearchCriteria).setParameters("keyword", "%" + keyword + "%");
verify(mockSearchBuilder).and(eq("startDate"), any(), eq(SearchCriteria.Op.GTEQ));
verify(mockSearchCriteria).setParameters("startDate", start);
verify(mockSearchBuilder).and(eq("endDate"), any(), eq(SearchCriteria.Op.LTEQ));
verify(mockSearchCriteria).setParameters("endDate", end);
}
}

View File

@ -0,0 +1,130 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookFilterVO;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.utils.Pair;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class WebhookFilterDaoImplTest {
@Spy
@InjectMocks
private WebhookFilterDaoImpl webhookFilterDaoImpl;
@Before
public void setUp() {
SearchBuilder<WebhookFilterVO> sb = Mockito.mock(SearchBuilder.class);
Mockito.when(sb.create()).thenReturn(Mockito.mock(SearchCriteria.class));
webhookFilterDaoImpl.IdWebhookIdSearch = sb;
}
@Test
public void searchByReturnsResultsWhenIdAndWebhookIdMatch() {
Long id = 1L;
Long webhookId = 2L;
Long startIndex = 0L;
Long pageSize = 10L;
Mockito.doReturn(new Pair(List.of(Mockito.mock(WebhookFilterVO.class)), 1))
.when(webhookFilterDaoImpl).searchAndCount(Mockito.any(), Mockito.any());
Pair<List<WebhookFilterVO>, Integer> result = webhookFilterDaoImpl.searchBy(id, webhookId, startIndex, pageSize);
assertNotNull(result);
assertTrue(result.first().size() >= 0);
}
@Test
public void searchByReturnsEmptyWhenNoMatch() {
Long id = 999L;
Long webhookId = 999L;
Long startIndex = 0L;
Long pageSize = 10L;
Mockito.doReturn(new Pair(List.of(), 0))
.when(webhookFilterDaoImpl).searchAndCount(Mockito.any(), Mockito.any());
Pair<List<WebhookFilterVO>, Integer> result = webhookFilterDaoImpl.searchBy(id, webhookId, startIndex, pageSize);
assertNotNull(result);
assertEquals(0, result.first().size());
}
@Test
public void listByWebhookReturnsResultsWhenWebhookIdExists() {
Long webhookId = 2L;
Mockito.doReturn(List.of(Mockito.mock(WebhookFilterVO.class)))
.when(webhookFilterDaoImpl).listBy(Mockito.any(SearchCriteria.class));
List<WebhookFilterVO> result = webhookFilterDaoImpl.listByWebhook(webhookId);
assertNotNull(result);
assertTrue(result.size() >= 0);
}
@Test
public void listByWebhookReturnsEmptyWhenWebhookIdDoesNotExist() {
Long webhookId = 999L;
Mockito.doReturn(List.of())
.when(webhookFilterDaoImpl).listBy(Mockito.any(SearchCriteria.class));
List<WebhookFilterVO> result = webhookFilterDaoImpl.listByWebhook(webhookId);
assertNotNull(result);
assertEquals(0, result.size());
}
@Test
public void deleteReturnsZeroWhenIdAndWebhookIdAreNull() {
int result = webhookFilterDaoImpl.delete(null, null);
assertEquals(0, result);
}
@Test
public void deleteReturnsNonZeroWhenIdOrWebhookIdExists() {
Long id = 1L;
Long webhookId = 2L;
Mockito.doReturn(1)
.when(webhookFilterDaoImpl).remove(Mockito.any(SearchCriteria.class));
int result = webhookFilterDaoImpl.delete(id, webhookId);
assertTrue(result >= 0);
}
}

View File

@ -0,0 +1,115 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.mom.webhook.dao;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import org.apache.cloudstack.mom.webhook.vo.WebhookJoinVO;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@RunWith(MockitoJUnitRunner.class)
public class WebhookJoinDaoImplTest {
@Spy
@InjectMocks
private WebhookJoinDaoImpl webhookJoinDao;
@Mock
private WebhookJoinVO mockWebhookVO;
@Mock
private SearchBuilder<WebhookJoinVO> mockSearchBuilder;
@Mock
private SearchCriteria<WebhookJoinVO> mockSearchCriteria;
@Before
public void setUp() {
when(mockSearchBuilder.entity()).thenReturn(mockWebhookVO);
when(mockSearchBuilder.and()).thenReturn(mockSearchBuilder);
when(mockSearchBuilder.create()).thenReturn(mockSearchCriteria);
doReturn(mockSearchBuilder).when(webhookJoinDao).createSearchBuilder();
}
@Test
public void listByAccountOrDomainReturnsResultsWhenAccountIdMatches() {
long accountId = 1L;
doReturn(List.of(mockWebhookVO)).when(webhookJoinDao).listBy(any(SearchCriteria.class));
List<WebhookJoinVO> result = webhookJoinDao.listByAccountOrDomain(accountId, null);
assertNotNull(result);
assertFalse(result.isEmpty());
verify(mockSearchBuilder).op(eq("accountId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("accountId", accountId);
verify(mockSearchBuilder, never()).or(eq("domainPath"), any(), eq(SearchCriteria.Op.LIKE));
verify(mockSearchCriteria, never()).setParameters(eq("domainPath"), any());
}
@Test
public void listByAccountOrDomainReturnsResultsWhenBothAccountIdAndDomainPathMatch() {
long accountId = 10L;
String domainPath = "domain/path";
doReturn(List.of(mockWebhookVO)).when(webhookJoinDao).listBy(any(SearchCriteria.class));
List<WebhookJoinVO> result = webhookJoinDao.listByAccountOrDomain(accountId, domainPath);
assertNotNull(result);
assertFalse(result.isEmpty());
verify(mockSearchBuilder).op(eq("accountId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("accountId", accountId);
verify(mockSearchBuilder).or(eq("domainPath"), any(), eq(SearchCriteria.Op.LIKE));
verify(mockSearchCriteria).setParameters("domainPath", domainPath);
}
@Test
public void listByAccountOrDomainReturnsEmptyWhenNoMatchFound() {
long accountId = 999L;
String domainPath = "nonexistent/path";
doReturn(Collections.emptyList()).when(webhookJoinDao).listBy(any(SearchCriteria.class));
List<WebhookJoinVO> result = webhookJoinDao.listByAccountOrDomain(accountId, domainPath);
assertNotNull(result);
assertTrue(result.isEmpty());
verify(mockSearchBuilder).op(eq("accountId"), any(), eq(SearchCriteria.Op.EQ));
verify(mockSearchCriteria).setParameters("accountId", accountId);
verify(mockSearchBuilder).or(eq("domainPath"), any(), eq(SearchCriteria.Op.LIKE));
verify(mockSearchCriteria).setParameters("domainPath", domainPath);
}
}

View File

@ -283,7 +283,7 @@ class TestWebhooks(cloudstackTestCase):
description=description,
secretkey=secretkey,
state=state
)['webhook']
)
self.assertNotEqual(
updated_webhook,
None,

View File

@ -71,7 +71,7 @@
"label.action.cancel.maintenance.mode": "Cancel maintenance mode",
"label.action.change.password": "Change password",
"label.action.clear.webhook.deliveries": "Clear deliveries",
"label.action.delete.webhook.deliveries": "Delete deliveries",
"label.action.clear.webhook.filters": "Clear filters",
"label.action.change.primary.storage.scope": "Change Primary Storage scope",
"label.action.configure.stickiness": "Stickiness",
"label.action.configure.storage.access.group": "Update storage access group",
@ -115,6 +115,8 @@
"label.action.delete.user": "Delete User",
"label.action.delete.vgpu.profile": "Delete vGPU profile",
"label.action.delete.volume": "Delete Volume",
"label.action.delete.webhook.deliveries": "Delete Deliveries",
"label.action.delete.webhook.filters": "Delete Filters",
"label.action.delete.zone": "Delete Zone",
"label.action.destroy.instance": "Destroy Instance",
"label.action.destroy.systemvm": "Destroy System VM",
@ -355,6 +357,7 @@
"label.add.vpn.customer.gateway": "Add VPN Customer Gateway",
"label.add.vpn.gateway": "Add VPN Gateway",
"label.add.vpn.user": "Add VPN User",
"label.add.webhook.filter": "Add Webhook Filter",
"label.add.zone": "Add Zone",
"label.adding": "Adding",
"label.adding.user": "Adding User...",
@ -633,6 +636,7 @@
"label.consoleproxy": "Console proxy",
"label.console.proxy": "Console proxy",
"label.console.proxy.vm": "Console proxy VM",
"label.contains": "Contains",
"label.continue": "Continue",
"label.continue.install": "Continue with installation",
"label.controlnodes": "Control nodes",
@ -807,6 +811,7 @@
"label.delete.vpn.user": "Delete VPN User",
"label.delete.webhook": "Delete Webhook",
"label.delete.webhook.delivery": "Delete Webhook Delivery",
"label.delete.webhook.filter": "Delete Webhook Filter",
"label.deleteconfirm": "Please confirm that you would like to delete this",
"label.deleting": "Deleting",
"label.deleting.failed": "Deleting failed",
@ -1036,8 +1041,10 @@
"label.event.timeline": "Event timeline",
"label.events": "Events",
"label.every": "Every",
"label.exact": "Exact",
"label.example": "Example",
"label.example.plugin": "ExamplePlugin",
"label.exclude": "Exclude",
"label.existing": "Existing",
"label.execute": "Execute",
"label.expunge": "Expunge",
@ -1076,6 +1083,7 @@
"label.shared.filesystems": "Shared FileSystems",
"label.filesystem": "Filesystem",
"label.filter": "Filter",
"label.filters": "Filters",
"label.filter.annotations.all": "All comments",
"label.filter.annotations.self": "Created by me",
"label.filterby": "Filter by",
@ -1254,6 +1262,7 @@
"label.import.volume": "Import Volume",
"label.inactive": "Inactive",
"label.inbuilt": "Inbuilt",
"label.include": "Include",
"label.in.progress": "in progress",
"label.in.progress.for": "in progress for",
"label.info": "Info",
@ -1520,6 +1529,7 @@
"label.management.server.peers": "Peers",
"label.managementservers": "Number of management servers",
"label.matchall": "Match all",
"label.matchtype": "Match Type",
"label.max": "Max.",
"label.max.primary.storage": "Max. primary (GiB)",
"label.max.secondary.storage": "Max. secondary (GiB)",
@ -2436,6 +2446,7 @@
"label.success.migrations": "Successful migrations",
"label.success.set": "Successfully set",
"label.success.updated": "Successfully updated",
"label.suffix": "Suffix",
"label.suitability": "Suitability",
"label.suitable": "Suitable",
"label.summary": "Summary",
@ -3286,6 +3297,7 @@
"message.delete.vpn.gateway.failed": "Failed to delete VPN Gateway.",
"message.delete.webhook": "Please confirm that you want to delete this Webhook.",
"message.delete.webhook.delivery": "Please confirm that you want to delete this Webhook delivery.",
"message.delete.webhook.filter": "Please confirm that you want to delete this Webhook filter.",
"message.deleting.firewall.policy": "Deleting Firewall Policy",
"message.deleting.node": "Deleting Node",
"message.deleting.vm": "Deleting Instance",
@ -3823,6 +3835,7 @@
"message.success.add.vpc.network": "Successfully added a VPC network",
"message.success.add.vpn.customer.gateway": "Successfully added VPN customer gateway",
"message.success.add.vpn.gateway": "Successfully added VPN gateway",
"message.success.add.webhook.filter": "Successfully added Webhook Filter",
"message.success.assign.sslcert": "Successfully assigned SSL certificate",
"message.success.assign.vm": "Successfully assigned Instance",
"message.success.apply.network.policy": "Successfully applied Network Policy",
@ -3835,6 +3848,7 @@
"message.success.change.password": "Successfully changed password for User",
"message.success.change.host.password": "Successfully changed password for host \"{name}\"",
"message.success.clear.webhook.deliveries": "Successfully cleared webhook deliveries",
"message.success.clear.webhook.filters": "Successfully cleared webhook filters",
"message.success.change.scope": "Successfully changed scope for storage pool",
"message.success.config.backup.schedule": "Successfully configured Instance backup schedule",
"message.success.config.health.monitor": "Successfully Configure Health Monitor",

View File

@ -924,6 +924,16 @@
@pressEnter="saveValue(record)"
>
</a-input>
<template v-else-if="['webhook'].includes($route.path.split('/')[1])">
<span style="word-break: break-all">{{ text }}</span>
<QuickView
style="margin-left: 5px"
:actions="actions"
:resource="record"
:enabled="quickViewEnabled() && actions.length > 0"
@exec-action="$parent.execAction"
/>
</template>
<div
v-else
style="width: 200px; word-break: break-all"
@ -1187,7 +1197,7 @@ export default {
'/project', '/account', 'buckets', 'objectstore',
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', '/quotatariff', '/sharedfs',
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 'webhookfilters', '/quotatariff', '/sharedfs',
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule'].join('|'))
.test(this.$route.path)
},

View File

@ -0,0 +1,416 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<div class="add-row">
<a-form
:ref="addFormRef"
:model="addFilterForm"
:rules="addFormRules"
@finish="addFilter"
layout="vertical"
class="add-filter-form">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item name="mode" ref="mode">
<template #label>
<tooltip-label :title="$t('label.mode')" :tooltip="addFilterApiParams.mode.description"/>
</template>
<a-radio-group v-model:value="addFilterForm.mode" button-style="solid">
<a-radio-button value="include">{{ $t('label.include') }}</a-radio-button>
<a-radio-button value="exclude">{{ $t('label.exclude') }}</a-radio-button>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item name="matchtype" ref="matchtype">
<template #label>
<tooltip-label :title="$t('label.matchtype')" :tooltip="addFilterApiParams.matchtype.description"/>
</template>
<a-select
style="margin-left: 0"
v-model:value="addFilterForm.matchtype"
placeholder="Select match type"
allow-clear>
<a-select-option value="exact">{{ $t('label.exact') }}</a-select-option>
<a-select-option value="prefix">{{ $t('label.prefix') }}</a-select-option>
<a-select-option value="suffix">{{ $t('label.suffix') }}</a-select-option>
<a-select-option value="contains">{{ $t('label.contains') }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 8px;">
<a-col :span="24">
<a-form-item name="value" ref="value">
<template #label>
<tooltip-label :title="$t('label.value')" :tooltip="addFilterApiParams.value.description"/>
</template>
<a-input v-model:value="addFilterForm.value" placeholder="Enter filter value" />
</a-form-item>
</a-col>
</a-row>
<a-row style="margin-top: 16px;">
<a-col :span="24" style="text-align: right;">
<a-space>
<a-button @click="resetAddFilterForm">{{ $t('label.reset') }}</a-button>
<a-button type="primary" ref="submit" @click="addFilter">{{ $t('label.add') }}</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</div>
<a-divider />
<a-button
v-if="('deleteWebhookFilter' in $store.getters.apis) && (selectedRowKeys && selectedRowKeys.length > 0)"
type="danger"
danger
style="width: 100%; margin-bottom: 15px"
@click="clearOrDeleteFiltersConfirmation()">
<template #icon><delete-outlined /></template>
{{ (selectedRowKeys && selectedRowKeys.length > 0) ? $t('label.action.delete.webhook.filters') : $t('label.action.clear.webhook.filters') }}
</a-button>
<list-view
:tabLoading="tabLoading"
:columns="columns"
:items="filters"
:actions="actions"
:columnKeys="columnKeys"
:explicitlyAllowRowSelection="true"
:selectedColumns="selectedColumnKeys"
ref="listview"
@update-selected-columns="updateSelectedColumns"
@refresh="this.fetchData"
@selection-change="updateSelectedRows"/>
<a-pagination
class="row-element"
style="margin-top: 10px"
size="small"
:current="page"
:pageSize="pageSize"
:total="totalCount"
:showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page-1)*pageSize))}-${Math.min(page*pageSize, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
:pageSizeOptions="pageSizeOptions"
@change="changePage"
@showSizeChange="changePage"
showSizeChanger
showQuickJumper>
<template #buildOptionText="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
</div>
</template>
<script>
import { ref, reactive, toRaw } from 'vue'
import { getAPI, postAPI } from '@/api'
import { mixinForm } from '@/utils/mixin'
import { genericCompare } from '@/utils/sort.js'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import ListView from '@/components/view/ListView'
export default {
name: 'WebhookFiltersTab',
mixins: [mixinForm],
components: {
TooltipLabel,
ListView
},
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
required: true
}
},
data () {
return {
tabLoading: false,
columnKeys: ['value', 'type', 'mode', 'matchtype'],
selectedColumnKeys: ['value', 'mode', 'matchtype'],
selectedRowKeys: [],
columns: [],
cols: [],
filters: [],
actions: [
{
api: 'deleteWebhookFilter',
icon: 'delete-outlined',
label: 'label.delete.webhook.filter',
message: 'message.delete.webhook.filter',
dataView: true,
popup: true
}
],
page: 1,
pageSize: 20,
totalCount: 0
}
},
computed: {
pageSizeOptions () {
var sizes = [20, 50, 100, 200, this.$store.getters.defaultListViewPageSize]
if (this.device !== 'desktop') {
sizes.unshift(10)
}
return [...new Set(sizes)].sort(function (a, b) {
return a - b
}).map(String)
}
},
beforeCreate () {
this.addFilterApiParams = this.$getApiParams('addWebhookFilter')
},
created () {
this.updateColumns()
this.pageSize = this.pageSizeOptions[0] * 1
this.initAddFilterForm()
this.fetchData()
},
watch: {
resource: {
handler () {
this.fetchData()
}
}
},
methods: {
fetchData () {
if ('listview' in this.$refs && this.$refs.listview) {
this.$refs.listview.resetSelection()
}
this.fetchFilters()
},
fetchFilters () {
this.filters = []
if (!this.resource.id) {
return
}
const params = {
page: this.page,
pagesize: this.pageSize,
webhookid: this.resource.id,
listall: true
}
this.tabLoading = true
getAPI('listWebhookFilters', params).then(json => {
this.filters = []
this.totalCount = json?.listwebhookfiltersresponse?.count || 0
this.filters = json?.listwebhookfiltersresponse?.webhookfilter || []
this.tabLoading = false
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchFilters()
},
updateSelectedColumns (key) {
if (this.selectedColumnKeys.includes(key)) {
this.selectedColumnKeys = this.selectedColumnKeys.filter(x => x !== key)
} else {
this.selectedColumnKeys.push(key)
}
this.updateColumns()
},
updateColumns () {
this.columns = []
for (var columnKey of this.columnKeys) {
const key = columnKey
if (!this.selectedColumnKeys.includes(key)) continue
var title = this.$t('label.' + String(key).toLowerCase())
this.columns.push({
key: key,
title: title,
dataIndex: key,
sorter: (a, b) => { return genericCompare(a[key] || '', b[key] || '') }
})
}
if (this.columns.length > 0) {
this.columns[this.columns.length - 1].customFilterDropdown = true
}
},
initAddFilterForm () {
this.addFormRef = ref()
this.addFilterForm = reactive({
mode: 'include',
matchtype: 'exact',
value: null
})
this.addFormRules = reactive({
value: [{ required: true, message: this.$t('message.error.required.input') }]
})
},
resetAddFilterForm () {
if (this.addFormRef.value) {
this.addFormRef.value.resetFields()
}
},
addFilter (e) {
e.preventDefault()
if (this.tabLoading) return
console.log('Adding webhook filter with form:', this.addFilterForm)
this.addFormRef.value.validate().then(() => {
const formRaw = toRaw(this.addFilterForm)
const values = this.handleRemoveFields(formRaw)
const params = {
webhookid: this.resource.id,
mode: values.mode,
matchtype: values.matchtype,
value: values.value
}
console.log('Adding webhook filter with params:', params)
this.tabLoading = true
postAPI('addWebhookFilter', params).then(json => {
this.$notification.success({
message: this.$t('label.add.webhook.filter'),
description: this.$t('message.success.add.webhook.filter')
})
setTimeout(() => {
this.resetAddFilterForm()
}, 250)
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.tabLoading = false
this.fetchFilters()
})
})
},
updateSelectedRows (keys) {
this.selectedRowKeys = keys
},
clearOrDeleteFiltersConfirmation () {
const self = this
const title = (this.selectedRowKeys && this.selectedRowKeys.length > 0)
? this.$t('label.action.delete.webhook.filters')
: this.$t('label.action.clear.webhook.filters')
this.$confirm({
title: title,
okText: this.$t('label.ok'),
okType: 'danger',
cancelText: this.$t('label.cancel'),
onOk () {
if (self.selectedRowKeys && self.selectedRowKeys.length > 0) {
self.deletedSelectedFilters()
return
}
self.clearFilters()
}
})
},
deletedSelectedFilters () {
const promises = []
this.selectedRowKeys.forEach(id => {
const params = {
id: id
}
promises.push(new Promise((resolve, reject) => {
postAPI('deleteWebhookFilter', params).then(json => {
return resolve(id)
}).catch(error => {
return reject(error)
})
}))
})
const msg = this.$t('label.action.delete.webhook.filters')
this.$message.info({
content: msg,
duration: 3
})
this.tabLoading = true
Promise.all(promises).finally(() => {
this.tabLoading = false
this.fetchData()
})
},
clearFilters () {
const params = {
webhookid: this.resource.id
}
this.tabLoading = true
postAPI('deleteWebhookFilter', params).then(json => {
this.$message.success(this.$t('message.success.clear.webhook.filters'))
this.fetchData()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.tabLoading = false
})
},
deleteFilterConfirmation (item) {
const self = this
this.$confirm({
title: this.$t('label.delete.webhook.filter'),
okText: this.$t('label.ok'),
okType: 'primary',
cancelText: this.$t('label.cancel'),
onOk () {
self.deleteFilter(item)
}
})
},
deleteFilter (item) {
const params = {
id: item.id
}
this.tabLoading = true
postAPI('deleteWebhookFilter', params).then(json => {
const message = `${this.$t('message.success.delete')} ${this.$t('label.webhook.filter')}`
this.$message.success(message)
this.fetchData()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.tabLoading = false
})
},
execAction (action) {
if (action.api === 'deleteWebhookFilter') {
this.deleteFilterConfirmation(action.resource)
}
}
}
}
</script>
<style lang="scss" scoped>
.ant-tag {
padding: 0 7px 0 0;
}
.ant-select {
margin-left: 10px;
}
.info-icon {
margin: 0 10px 0 5px;
}
.filter-row {
margin-bottom: 2.5%;
}
.filter-row-inner {
margin-top: 3%;
}
</style>

View File

@ -116,6 +116,10 @@ export default {
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
},
{
name: 'filters',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/WebhookFiltersTab.vue')))
},
{
name: 'recent.deliveries',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/WebhookDeliveriesTab.vue')))