diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 8fca652518f2..3e36d933772b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -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"; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql index c1f1bb2c094d..913ee56c7022 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql @@ -18,3 +18,19 @@ --; -- Schema upgrade from 4.22.0.0 to 4.23.0.0 --; + +-- 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; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/Webhook.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/Webhook.java index 1cc73ae31df3..4216f702d8a6 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/Webhook.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/Webhook.java @@ -24,8 +24,8 @@ 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; }; diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java index edd77e5b414c..de48b8d037cf 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiService.java @@ -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 listWebhookDeliveries(ListWebhookDeliveriesCmd cmd); int deleteWebhookDelivery(DeleteWebhookDeliveryCmd cmd) throws CloudRuntimeException; WebhookDeliveryResponse executeWebhookDelivery(ExecuteWebhookDeliveryCmd cmd) throws CloudRuntimeException; + ListResponse listWebhookFilters(ListWebhookFiltersCmd cmd) throws CloudRuntimeException; + WebhookFilterResponse addWebhookFilter(AddWebhookFilterCmd cmd) throws CloudRuntimeException; + int deleteWebhookFilter(DeleteWebhookFilterCmd cmd) throws CloudRuntimeException; } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java index 187b140d5d84..a484f29e8d29 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImpl.java @@ -29,23 +29,30 @@ 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.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 @@ protected void validateWebhookOwnerPayloadUrl(Account owner, String payloadUrl, 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 listWebhooks(ListWebhooksCmd cmd) { final CallContext ctx = CallContext.current(); @@ -234,6 +263,25 @@ public ListResponse listWebhooks(ListWebhooksCmd cmd) { 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 responsesList = new ArrayList<>(); List permittedAccounts = new ArrayList<>(); Ternary domainIdRecursiveListProject = @@ -258,27 +306,6 @@ public ListResponse listWebhooks(ListWebhooksCmd cmd) { SearchCriteria 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 WebhookResponse createWebhook(CreateWebhookCmd cmd) throws CloudRuntimeEx 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 WebhookResponse createWebhook(CreateWebhookCmd cmd) throws CloudRuntimeEx } 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 WebhookResponse createWebhook(CreateWebhookCmd cmd) throws CloudRuntimeEx 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 boolean deleteWebhook(DeleteWebhookCmd cmd) throws CloudRuntimeException 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,29 +424,27 @@ public WebhookResponse updateWebhook(UpdateWebhookCmd cmd) throws CloudRuntimeEx 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); - 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()))) { - throw new InvalidParameterValueException( - String.format("Scope %s can not be specified for owner %s", scope, owner.getName())); - } - webhook.setScope(scope); - updateNeeded = true; - } catch (IllegalArgumentException iae) { + 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()))) { + throw new InvalidParameterValueException( + String.format("Scope %s can not be specified for owner %s", scope, owner.getName())); + } + webhook.setScope(scope); + updateNeeded = true; } URI uri = URI.create(webhook.getPayloadUrl()); if (StringUtils.isNotEmpty(payloadUrl)) { @@ -427,7 +455,7 @@ public WebhookResponse updateWebhook(UpdateWebhookCmd cmd) throws CloudRuntimeEx 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 WebhookResponse updateWebhook(UpdateWebhookCmd cmd) throws CloudRuntimeEx if (updateNeeded && !webhookDao.update(id, webhook)) { return null; } + webhookService.invalidateWebhooksCache(); return createWebhookResponse(webhook.getId()); } @@ -455,8 +484,7 @@ public WebhookResponse createWebhookResponse(long webhookId) { @Override public ListResponse 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 int deleteWebhookDelivery(DeleteWebhookDeliveryCmd cmd) throws CloudRunti @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 WebhookDeliveryResponse executeWebhookDelivery(ExecuteWebhookDeliveryCmd 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 WebhookDeliveryResponse executeWebhookDelivery(ExecuteWebhookDeliveryCmd return createTestWebhookDeliveryResponse(webhookDelivery, webhook); } + @Override + public ListResponse listWebhookFilters(ListWebhookFiltersCmd cmd) throws CloudRuntimeException { + Pair, Integer> filtersAndCount = webhookFilterDao.searchBy(cmd.getId(), cmd.getWebhookId(), + cmd.getStartIndex(), cmd.getPageSizeVal()); + List 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 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 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, 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> getCommands() { List> cmdList = new ArrayList<>(); @@ -569,6 +684,9 @@ public List> getCommands() { 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; } } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookFilter.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookFilter.java new file mode 100644 index 000000000000..b8bcd6c4a287 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookFilter.java @@ -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; + +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 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 (Mode.Exclude.equals(f.getMode()) + && Mode.Include.equals(this.getMode())) { + String oldVal = f.getValue().toUpperCase(); + String newVal = this.getValue().toUpperCase(); + + if (overlaps(f.getMatchType(), oldVal, this.getMatchType(), newVal)) { + return f; + } + } + } + return null; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java index 5a5aced288d1..7d8f9a0a7443 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookService.java @@ -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); } diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java index 97d00c45e4d4..624de54d41bb 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookServiceImpl.java @@ -22,6 +22,7 @@ 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.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.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>, List> webhooksCache; + protected LazyCache> webhookFiltersCache; + protected WebhookDeliveryThread getDeliveryJob(Event event, Webhook webhook, Pair configs) { WebhookDeliveryThread.WebhookDeliveryContext context = new WebhookDeliveryThread.WebhookDeliveryContext<>(null, event.getEventId(), webhook.getId()); @@ -97,13 +106,74 @@ protected WebhookDeliveryThread getDeliveryJob(Event event, Webhook webhook, Pai 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 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 getDeliveryJobs(Event event) throws EventBusException { List 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 domainIds = new ArrayList<>(); @@ -112,9 +182,14 @@ protected List getDeliveryJobs(Event event) throws EventBusException { domainIds.addAll(domainDao.getDomainParentIds(event.getResourceDomainId())); } List webhooks = - webhookDao.listByEnabledForDelivery(event.getResourceAccountId(), domainIds); + webhooksCache.get(org.apache.commons.lang3.tuple.Pair.of(event.getResourceAccountId(), domainIds)); Map> domainConfigs = new HashMap<>(); for (WebhookVO webhook : webhooks) { + List 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 @@ protected List getDeliveryJobs(Event event) throws EventBusException { } protected Runnable getManualDeliveryJob(WebhookDelivery existingDelivery, Webhook webhook, String payload, - AsyncCallFuture future) { + CompletableFuture future) { if (StringUtils.isBlank(payload)) { payload = "{ \"CloudStack\": \"works!\" }"; } @@ -155,7 +230,7 @@ protected Runnable getManualDeliveryJob(WebhookDelivery existingDelivery, Webhoo event.setDescription(description); event.setResourceAccountUuid(resourceAccountUuid); ManualDeliveryContext context = - new ManualDeliveryContext<>(null, webhook, future); + new ManualDeliveryContext<>(null, future); AsyncCallbackDispatcher caller = AsyncCallbackDispatcher.create(this); caller.setCallback(caller.getTarget().manualDeliveryCompleteCallback(null, null)) @@ -181,7 +256,7 @@ protected Void manualDeliveryCompleteCallback( AsyncCallbackDispatcher callback, ManualDeliveryContext context) { WebhookDeliveryThread.WebhookDeliveryResult result = callback.getResult(); - context.future.complete(result); + context.getFuture().complete(result); return null; } @@ -205,8 +280,20 @@ protected long cleanupOldWebhookDeliveries(long deliveriesLimit) { 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 params) throws ConfigurationException { + initCaches(); try { webhookJobExecutor = Executors.newFixedThreadPool(WebhookDeliveryThreadPoolSize.value(), new NamedThreadFactory(WEBHOOK_JOB_POOL_THREAD_PREFIX)); @@ -273,7 +360,7 @@ public void handleEvent(Event event) throws EventBusException { @Override public WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook webhook, String payload) throws CloudRuntimeException { - AsyncCallFuture future = new AsyncCallFuture<>(); + CompletableFuture future = new CompletableFuture<>(); Runnable job = getManualDeliveryJob(delivery, webhook, payload, future); webhookJobExecutor.submit(job); WebhookDeliveryThread.WebhookDeliveryResult result = null; @@ -297,22 +384,33 @@ public WebhookDelivery executeWebhookDelivery(WebhookDelivery delivery, Webhook return webhookDeliveryVO; } + @Override + public void invalidateWebhooksCache() { + webhooksCache.clear(); + } + + @Override + public void invalidateWebhookFiltersCache(long webhookId) { + webhookFiltersCache.invalidate(webhookId); + } + @Override public List> getCommands() { return new ArrayList<>(); } - static public class ManualDeliveryContext extends AsyncRpcContext { - final Webhook webhook; - final AsyncCallFuture future; + protected static class ManualDeliveryContext extends AsyncRpcContext { + private final CompletableFuture future; + + public CompletableFuture getFuture() { + return future; + } - public ManualDeliveryContext(AsyncCompletionCallback callback, Webhook webhook, - AsyncCallFuture future) { + public ManualDeliveryContext(AsyncCompletionCallback callback, + CompletableFuture future) { super(callback); - this.webhook = webhook; this.future = future; } - } public class WebhookDeliveryCleanupWorker extends ManagedContextRunnable { diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/AddWebhookFilterCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/AddWebhookFilterCmd.java new file mode 100644 index 000000000000..ba71cc1a2e83 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/AddWebhookFilterCmd.java @@ -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()); + } + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookCmd.java index d3d2cf18e1fd..12da15b3d673 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/CreateWebhookCmd.java @@ -28,13 +28,12 @@ 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 @@ @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 { diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookFilterCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookFilterCmd.java new file mode 100644 index 000000000000..80812c9b230b --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookFilterCmd.java @@ -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()); + } + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ExecuteWebhookDeliveryCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ExecuteWebhookDeliveryCmd.java index f31a5481376d..c3dfe8500531 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ExecuteWebhookDeliveryCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ExecuteWebhookDeliveryCmd.java @@ -39,8 +39,6 @@ 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 { diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDeliveriesCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDeliveriesCmd.java index 466dad0d1224..cf9c046b2b22 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDeliveriesCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookDeliveriesCmd.java @@ -27,7 +27,6 @@ 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 @@ @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") diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookFiltersCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookFiltersCmd.java new file mode 100644 index 000000000000..1641ea674bbc --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookFiltersCmd.java @@ -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 response = webhookApiService.listWebhookFilters(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhooksCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhooksCmd.java index 6510c308f6e7..2719e0aaa991 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhooksCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhooksCmd.java @@ -25,17 +25,15 @@ 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") diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookCmd.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookCmd.java index c2be1d3f4fa2..e27fe1a79191 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookCmd.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/command/user/UpdateWebhookCmd.java @@ -26,17 +26,16 @@ 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") diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookFilterResponse.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookFilterResponse.java new file mode 100644 index 000000000000..f1ed158b6790 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/api/response/WebhookFilterResponse.java @@ -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; + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImpl.java index 088ed53772a3..5b8f6341303c 100644 --- a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImpl.java +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImpl.java @@ -21,6 +21,7 @@ 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 void removeOlderDeliveries(long webhookId, long limit) { SearchCriteria sc = sb.create(); sc.setParameters("webhookId", webhookId); List keep = listBy(sc, searchFilter); + if (CollectionUtils.isEmpty(keep)) { + return; + } SearchBuilder sbDelete = createSearchBuilder(); sbDelete.and("id", sbDelete.entity().getId(), SearchCriteria.Op.NOTIN); SearchCriteria scDelete = sbDelete.create(); diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDao.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDao.java new file mode 100644 index 000000000000..753122db7ecd --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDao.java @@ -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 { + Pair, Integer> searchBy(Long id, Long webhookId, Long startIndex, Long pageSize); + List listByWebhook(Long webhookId); + int delete(Long id, Long webhookId); +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDaoImpl.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDaoImpl.java new file mode 100644 index 000000000000..84446a263cf7 --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDaoImpl.java @@ -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 implements WebhookFilterDao { + + SearchBuilder 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, Integer> searchBy(Long id, Long webhookId, Long startIndex, Long pageSize) { + Filter searchFilter = new Filter(WebhookFilterVO.class, "id", false, startIndex, + pageSize); + SearchCriteria sc = IdWebhookIdSearch.create(); + if (id != null) { + sc.setParameters("id", id); + } + if (webhookId != null) { + sc.setParameters("webhookId", webhookId); + } + return searchAndCount(sc, searchFilter); + } + + @Override + public List listByWebhook(Long webhookId) { + SearchCriteria sc = IdWebhookIdSearch.create(); + if (webhookId != null) { + sc.setParameters("webhookId", webhookId); + } + return listBy(sc); + } + + @Override + public int delete(Long id, Long webhookId) { + SearchCriteria 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); + } +} diff --git a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookFilterVO.java b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookFilterVO.java new file mode 100644 index 000000000000..75e18f19516e --- /dev/null +++ b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/vo/WebhookFilterVO.java @@ -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")); + } +} diff --git a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml index 22f688c781fe..e00697204be6 100644 --- a/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml +++ b/plugins/event-bus/webhook/src/main/resources/META-INF/cloudstack/webhook/spring-event-webhook-context.xml @@ -31,6 +31,7 @@ + diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java index dff358069840..bcfc29486abd 100644 --- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookApiServiceImplTest.java @@ -16,34 +16,61 @@ // under the License. package org.apache.cloudstack.mom.webhook; +import java.util.Date; import java.util.List; import org.apache.cloudstack.acl.SecurityChecker; +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.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.ApiResponseHelper; +import com.cloud.cluster.ManagementServerHostVO; +import com.cloud.cluster.dao.ManagementServerHostDao; import com.cloud.domain.DomainVO; import com.cloud.domain.dao.DomainDao; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; import com.cloud.user.Account; import com.cloud.user.AccountManager; +import com.cloud.utils.Pair; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; @RunWith(MockitoJUnitRunner.class) public class WebhookApiServiceImplTest { @@ -53,14 +80,43 @@ public class WebhookApiServiceImplTest { @Mock WebhookJoinDao webhookJoinDao; @Mock + WebhookDeliveryDao webhookDeliveryDao; + @Mock + WebhookDeliveryJoinDao webhookDeliveryJoinDao; + @Mock + WebhookFilterDao webhookFilterDao; + @Mock AccountManager accountManager; - @Mock DomainDao domainDao; + @Mock + WebhookService webhookService; + @Mock + ManagementServerHostDao managementServerHostDao; + @Mock + Account caller; + + @Spy @InjectMocks WebhookApiServiceImpl webhookApiServiceImpl = new WebhookApiServiceImpl(); + MockedStatic callContextMocked; + + @Before + public void setup() { + callContextMocked = Mockito.mockStatic(CallContext.class); + CallContext callContextMock = Mockito.mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + Mockito.when(callContextMock.getCallingAccount()).thenReturn(caller); + } + + @After + public void cleanup() { + callContextMocked.close(); + } + + private WebhookJoinVO prepareTestWebhookJoinVO() { String name = "webhook"; String description = "webhook-description"; @@ -152,48 +208,32 @@ public void testGetIdsOfAccessibleWebhooksNormalUser() { @Test(expected = InvalidParameterValueException.class) public void testDeleteWebhookInvalidWebhook() { - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - DeleteWebhookCmd cmd = Mockito.mock(DeleteWebhookCmd.class); - Mockito.when(cmd.getId()).thenReturn(1L); - CallContext callContextMock = Mockito.mock(CallContext.class); - callContextMocked.when(CallContext::current).thenReturn(callContextMock); - webhookApiServiceImpl.deleteWebhook(cmd); - } + DeleteWebhookCmd cmd = Mockito.mock(DeleteWebhookCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + webhookApiServiceImpl.deleteWebhook(cmd); } @Test(expected = PermissionDeniedException.class) public void testDeleteWebhookNoPermission() { - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - DeleteWebhookCmd cmd = Mockito.mock(DeleteWebhookCmd.class); - Mockito.when(cmd.getId()).thenReturn(1L); - WebhookVO webhookVO = Mockito.mock(WebhookVO.class); - Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); - CallContext callContextMock = Mockito.mock(CallContext.class); - Account account = Mockito.mock(Account.class); - Mockito.when(callContextMock.getCallingAccount()).thenReturn(account); - callContextMocked.when(CallContext::current).thenReturn(callContextMock); - Mockito.doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(account, - SecurityChecker.AccessType.OperateEntry, false, webhookVO); - webhookApiServiceImpl.deleteWebhook(cmd); - } + DeleteWebhookCmd cmd = Mockito.mock(DeleteWebhookCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + WebhookVO webhookVO = Mockito.mock(WebhookVO.class); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); + Mockito.doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(caller, + SecurityChecker.AccessType.OperateEntry, false, webhookVO); + webhookApiServiceImpl.deleteWebhook(cmd); } @Test public void testDeleteWebhook() { - try (MockedStatic callContextMocked = Mockito.mockStatic(CallContext.class)) { - DeleteWebhookCmd cmd = Mockito.mock(DeleteWebhookCmd.class); - Mockito.when(cmd.getId()).thenReturn(1L); - WebhookVO webhookVO = Mockito.mock(WebhookVO.class); - Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); - CallContext callContextMock = Mockito.mock(CallContext.class); - Account account = Mockito.mock(Account.class); - Mockito.when(callContextMock.getCallingAccount()).thenReturn(account); - callContextMocked.when(CallContext::current).thenReturn(callContextMock); - Mockito.doNothing().when(accountManager).checkAccess(account, - SecurityChecker.AccessType.OperateEntry, false, webhookVO); - Mockito.doReturn(true).when(webhookDao).remove(Mockito.anyLong()); - Assert.assertTrue(webhookApiServiceImpl.deleteWebhook(cmd)); - } + DeleteWebhookCmd cmd = Mockito.mock(DeleteWebhookCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + WebhookVO webhookVO = Mockito.mock(WebhookVO.class); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); + Mockito.doNothing().when(accountManager).checkAccess(caller, + SecurityChecker.AccessType.OperateEntry, false, webhookVO); + Mockito.doReturn(true).when(webhookDao).remove(Mockito.anyLong()); + Assert.assertTrue(webhookApiServiceImpl.deleteWebhook(cmd)); } @Test @@ -250,4 +290,1005 @@ public void testGetNormalizedPayloadUrl() { Assert.assertEquals("https://abc.com", webhookApiServiceImpl.getNormalizedPayloadUrl("https://abc.com")); } + + @Test(expected = InvalidParameterValueException.class) + public void basicWebhookDeliveryApiCheckThrowsExceptionForInvalidDeliveryId() { + Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(null); + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), 1L, null, null, null, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void basicWebhookDeliveryApiCheckThrowsExceptionForInvalidWebhookId() { + Mockito.when(webhookDao.findById(1L)).thenReturn(null); + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), null, 1L, null, null, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void basicWebhookDeliveryApiCheckThrowsExceptionForEndDateBeforeStartDate() { + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(Mockito.mock(Account.class), null, null, null, new Date(), new Date(System.currentTimeMillis() - 1000)); + } + + @Test(expected = PermissionDeniedException.class) + public void basicWebhookDeliveryApiCheckThrowsExceptionForNonAdminAccessToManagementServer() { + Account caller = Mockito.mock(Account.class); + Mockito.when(caller.getType()).thenReturn(Account.Type.NORMAL); + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void basicWebhookDeliveryApiCheckThrowsExceptionForInvalidManagementServerId() { + Account caller = Mockito.mock(Account.class); + Mockito.when(caller.getType()).thenReturn(Account.Type.ADMIN); + Mockito.when(managementServerHostDao.findById(1L)).thenReturn(null); + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null); + } + + @Test + public void basicWebhookDeliveryApiCheckReturnsManagementServerHostVOForValidInput() { + Account caller = Mockito.mock(Account.class); + Mockito.when(caller.getType()).thenReturn(Account.Type.ADMIN); + ManagementServerHostVO managementServerHostVO = Mockito.mock(ManagementServerHostVO.class); + Mockito.when(managementServerHostDao.findById(1L)).thenReturn(managementServerHostVO); + ManagementServerHostVO result = webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, null, 1L, null, null); + Assert.assertEquals(managementServerHostVO, result); + } + + @Test + public void basicWebhookDeliveryApiCheckValidatesWebhookDeliveryAccess() { + Account caller = Mockito.mock(Account.class); + WebhookDeliveryVO webhookDeliveryVO = Mockito.mock(WebhookDeliveryVO.class); + WebhookVO webhookVO = Mockito.mock(WebhookVO.class); + Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(webhookDeliveryVO); + Mockito.when(webhookDeliveryVO.getWebhookId()).thenReturn(1L); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); + Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO); + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, 1L, null, null, null, null); + Mockito.verify(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO); + } + + @Test + public void basicWebhookDeliveryApiCheckValidatesWebhookAccess() { + Account caller = Mockito.mock(Account.class); + WebhookVO webhookVO = Mockito.mock(WebhookVO.class); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); + Mockito.doNothing().when(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO); + webhookApiServiceImpl.basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null); + Mockito.verify(accountManager).checkAccess(caller, SecurityChecker.AccessType.OperateEntry, false, webhookVO); + } + + @Test + public void createWebhookDeliveryResponsePopulatesAllFieldsCorrectly() { + WebhookDeliveryJoinVO webhookDeliveryVO = Mockito.mock(WebhookDeliveryJoinVO.class); + Mockito.when(webhookDeliveryVO.getUuid()).thenReturn("uuid"); + Mockito.when(webhookDeliveryVO.getEventUuid()).thenReturn("eventUuid"); + Mockito.when(webhookDeliveryVO.getEventType()).thenReturn("eventType"); + Mockito.when(webhookDeliveryVO.getWebhookUuId()).thenReturn("webhookUuid"); + Mockito.when(webhookDeliveryVO.getWebhookName()).thenReturn("webhookName"); + Mockito.when(webhookDeliveryVO.getManagementServerUuId()).thenReturn("managementServerUuid"); + Mockito.when(webhookDeliveryVO.getManagementServerName()).thenReturn("managementServerName"); + Mockito.when(webhookDeliveryVO.getHeaders()).thenReturn("headers"); + Mockito.when(webhookDeliveryVO.getPayload()).thenReturn("payload"); + Mockito.when(webhookDeliveryVO.isSuccess()).thenReturn(true); + Mockito.when(webhookDeliveryVO.getResponse()).thenReturn("response"); + Mockito.when(webhookDeliveryVO.getStartTime()).thenReturn(new Date(1000)); + Mockito.when(webhookDeliveryVO.getEndTime()).thenReturn(new Date(2000)); + + WebhookDeliveryResponse response = webhookApiServiceImpl.createWebhookDeliveryResponse(webhookDeliveryVO); + + Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals("eventUuid", ReflectionTestUtils.getField(response, "eventId")); + Assert.assertEquals("eventType", ReflectionTestUtils.getField(response, "eventType")); + Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertEquals("managementServerUuid", ReflectionTestUtils.getField(response, "managementServerId")); + Assert.assertEquals("managementServerName", ReflectionTestUtils.getField(response, "managementServerName")); + Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers")); + Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload")); + Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success")); + Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime")); + Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime")); + } + + @Test(expected = NullPointerException.class) + public void createWebhookDeliveryResponseThrowsExceptionForNullInput() { + webhookApiServiceImpl.createWebhookDeliveryResponse(null); + } + + @Test + public void createTestWebhookDeliveryResponsePopulatesAllFieldsCorrectly() { + WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class); + Mockito.when(webhookDelivery.getUuid()).thenReturn("uuid"); + Mockito.when(webhookDelivery.getManagementServerId()).thenReturn(1L); + Mockito.when(webhookDelivery.getHeaders()).thenReturn("headers"); + Mockito.when(webhookDelivery.getPayload()).thenReturn("payload"); + Mockito.when(webhookDelivery.isSuccess()).thenReturn(true); + Mockito.when(webhookDelivery.getResponse()).thenReturn("response"); + Mockito.when(webhookDelivery.getStartTime()).thenReturn(new Date(1000)); + Mockito.when(webhookDelivery.getEndTime()).thenReturn(new Date(2000)); + + Webhook webhook = Mockito.mock(Webhook.class); + Mockito.when(webhook.getUuid()).thenReturn("webhookUuid"); + Mockito.when(webhook.getName()).thenReturn("webhookName"); + + ManagementServerHostVO msHost = Mockito.mock(ManagementServerHostVO.class); + Mockito.when(msHost.getUuid()).thenReturn("managementServerUuid"); + Mockito.when(msHost.getName()).thenReturn("managementServerName"); + Mockito.when(managementServerHostDao.findByMsid(1L)).thenReturn(msHost); + + WebhookDeliveryResponse response = webhookApiServiceImpl.createTestWebhookDeliveryResponse(webhookDelivery, webhook); + + Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals(WebhookDelivery.TEST_EVENT_TYPE, ReflectionTestUtils.getField(response, "eventType")); + Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertEquals("managementServerUuid", ReflectionTestUtils.getField(response, "managementServerId")); + Assert.assertEquals("managementServerName", ReflectionTestUtils.getField(response, "managementServerName")); + Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers")); + Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload")); + Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success")); + Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime")); + Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime")); + } + + @Test + public void createTestWebhookDeliveryResponseHandlesNullWebhook() { + WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class); + Mockito.when(webhookDelivery.getUuid()).thenReturn("uuid"); + Mockito.when(webhookDelivery.getManagementServerId()).thenReturn(1L); + Mockito.when(webhookDelivery.getHeaders()).thenReturn("headers"); + Mockito.when(webhookDelivery.getPayload()).thenReturn("payload"); + Mockito.when(webhookDelivery.isSuccess()).thenReturn(true); + Mockito.when(webhookDelivery.getResponse()).thenReturn("response"); + Mockito.when(webhookDelivery.getStartTime()).thenReturn(new Date(1000)); + Mockito.when(webhookDelivery.getEndTime()).thenReturn(new Date(2000)); + + ManagementServerHostVO msHost = Mockito.mock(ManagementServerHostVO.class); + Mockito.when(msHost.getUuid()).thenReturn("managementServerUuid"); + Mockito.when(msHost.getName()).thenReturn("managementServerName"); + Mockito.when(managementServerHostDao.findByMsid(1L)).thenReturn(msHost); + + WebhookDeliveryResponse response = webhookApiServiceImpl.createTestWebhookDeliveryResponse(webhookDelivery, null); + + Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals(WebhookDelivery.TEST_EVENT_TYPE, ReflectionTestUtils.getField(response, "eventType")); + Assert.assertNull(ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertNull(ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertEquals("managementServerUuid", ReflectionTestUtils.getField(response, "managementServerId")); + Assert.assertEquals("managementServerName", ReflectionTestUtils.getField(response, "managementServerName")); + Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers")); + Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload")); + Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success")); + Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime")); + Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime")); + } + + @Test + public void createTestWebhookDeliveryResponseHandlesNullManagementServer() { + WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class); + Mockito.when(webhookDelivery.getUuid()).thenReturn("uuid"); + Mockito.when(webhookDelivery.getManagementServerId()).thenReturn(1L); + Mockito.when(webhookDelivery.getHeaders()).thenReturn("headers"); + Mockito.when(webhookDelivery.getPayload()).thenReturn("payload"); + Mockito.when(webhookDelivery.isSuccess()).thenReturn(true); + Mockito.when(webhookDelivery.getResponse()).thenReturn("response"); + Mockito.when(webhookDelivery.getStartTime()).thenReturn(new Date(1000)); + Mockito.when(webhookDelivery.getEndTime()).thenReturn(new Date(2000)); + + Webhook webhook = Mockito.mock(Webhook.class); + Mockito.when(webhook.getUuid()).thenReturn("webhookUuid"); + Mockito.when(webhook.getName()).thenReturn("webhookName"); + + Mockito.when(managementServerHostDao.findByMsid(1L)).thenReturn(null); + + WebhookDeliveryResponse response = webhookApiServiceImpl.createTestWebhookDeliveryResponse(webhookDelivery, webhook); + + Assert.assertEquals("webhookdelivery", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("uuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals(WebhookDelivery.TEST_EVENT_TYPE, ReflectionTestUtils.getField(response, "eventType")); + Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertNull(ReflectionTestUtils.getField(response, "managementServerId")); + Assert.assertNull(ReflectionTestUtils.getField(response, "managementServerName")); + Assert.assertEquals("headers", ReflectionTestUtils.getField(response, "headers")); + Assert.assertEquals("payload", ReflectionTestUtils.getField(response, "payload")); + Assert.assertTrue((Boolean) ReflectionTestUtils.getField(response, "success")); + Assert.assertEquals("response", ReflectionTestUtils.getField(response, "response")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "startTime")); + Assert.assertEquals(new Date(2000), ReflectionTestUtils.getField(response, "endTime")); + } + + @Test + public void getOwnerReturnsFinalizedOwnerForValidInput() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + Account finalizedOwner = Mockito.mock(Account.class); + + Mockito.when(cmd.getAccountName()).thenReturn("accountName"); + Mockito.when(cmd.getDomainId()).thenReturn(1L); + Mockito.when(cmd.getProjectId()).thenReturn(2L); + Mockito.when(accountManager.finalizeOwner(caller, "accountName", 1L, 2L)).thenReturn(finalizedOwner); + + Account result = webhookApiServiceImpl.getOwner(cmd); + + Assert.assertEquals(finalizedOwner, result); + } + + @Test(expected = InvalidParameterValueException.class) + public void getOwnerThrowsExceptionForInvalidAccount() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + + Mockito.when(cmd.getAccountName()).thenReturn("invalidAccount"); + Mockito.when(cmd.getDomainId()).thenReturn(1L); + Mockito.when(cmd.getProjectId()).thenReturn(null); + Mockito.when(accountManager.finalizeOwner(caller, "invalidAccount", 1L, null)) + .thenThrow(new InvalidParameterValueException("Invalid account")); + + webhookApiServiceImpl.getOwner(cmd); + } + + @Test + public void createWebhookFilterResponsePopulatesAllFieldsCorrectly() { + WebhookFilter webhookFilter = Mockito.mock(WebhookFilter.class); + Mockito.when(webhookFilter.getUuid()).thenReturn("filterUuid"); + Mockito.when(webhookFilter.getType()).thenReturn(WebhookFilter.Type.EventType); + Mockito.when(webhookFilter.getMode()).thenReturn(WebhookFilter.Mode.Include); + Mockito.when(webhookFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact); + Mockito.when(webhookFilter.getValue()).thenReturn("value"); + Mockito.when(webhookFilter.getCreated()).thenReturn(new Date(1000)); + + WebhookVO webhookVO = Mockito.mock(WebhookVO.class); + Mockito.when(webhookVO.getUuid()).thenReturn("webhookUuid"); + Mockito.when(webhookVO.getName()).thenReturn("webhookName"); + + WebhookFilterResponse response = webhookApiServiceImpl.createWebhookFilterResponse(webhookFilter, webhookVO); + + Assert.assertEquals("webhookfilter", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("filterUuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertEquals("EventType", ReflectionTestUtils.getField(response, "type")); + Assert.assertEquals("Include", ReflectionTestUtils.getField(response, "mode")); + Assert.assertEquals("Exact", ReflectionTestUtils.getField(response, "matchType")); + Assert.assertEquals("value", ReflectionTestUtils.getField(response, "value")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "created")); + } + + @Test + public void createWebhookFilterResponseHandlesNullWebhookVO() { + WebhookFilter webhookFilter = Mockito.mock(WebhookFilter.class); + Mockito.when(webhookFilter.getUuid()).thenReturn("filterUuid"); + Mockito.when(webhookFilter.getWebhookId()).thenReturn(1L); + Mockito.when(webhookFilter.getType()).thenReturn(WebhookFilter.Type.EventType); + Mockito.when(webhookFilter.getMode()).thenReturn(WebhookFilter.Mode.Include); + Mockito.when(webhookFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact); + Mockito.when(webhookFilter.getValue()).thenReturn("value"); + Mockito.when(webhookFilter.getCreated()).thenReturn(new Date(1000)); + + WebhookVO webhookVO = Mockito.mock(WebhookVO.class); + Mockito.when(webhookVO.getUuid()).thenReturn("webhookUuid"); + Mockito.when(webhookVO.getName()).thenReturn("webhookName"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhookVO); + + WebhookFilterResponse response = webhookApiServiceImpl.createWebhookFilterResponse(webhookFilter, null); + + Assert.assertEquals("webhookfilter", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("filterUuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertEquals("webhookUuid", ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertEquals("webhookName", ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertEquals("EventType", ReflectionTestUtils.getField(response, "type")); + Assert.assertEquals("Include", ReflectionTestUtils.getField(response, "mode")); + Assert.assertEquals("Exact", ReflectionTestUtils.getField(response, "matchType")); + Assert.assertEquals("value", ReflectionTestUtils.getField(response, "value")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "created")); + } + + @Test + public void createWebhookFilterResponseHandlesNullWebhookFromDao() { + WebhookFilter webhookFilter = Mockito.mock(WebhookFilter.class); + Mockito.when(webhookFilter.getUuid()).thenReturn("filterUuid"); + Mockito.when(webhookFilter.getWebhookId()).thenReturn(1L); + Mockito.when(webhookFilter.getType()).thenReturn(WebhookFilter.Type.EventType); + Mockito.when(webhookFilter.getMode()).thenReturn(WebhookFilter.Mode.Include); + Mockito.when(webhookFilter.getMatchType()).thenReturn(WebhookFilter.MatchType.Exact); + Mockito.when(webhookFilter.getValue()).thenReturn("value"); + Mockito.when(webhookFilter.getCreated()).thenReturn(new Date(1000)); + + Mockito.when(webhookDao.findById(1L)).thenReturn(null); + + WebhookFilterResponse response = webhookApiServiceImpl.createWebhookFilterResponse(webhookFilter, null); + + Assert.assertEquals("webhookfilter", ReflectionTestUtils.getField(response, "objectName")); + Assert.assertEquals("filterUuid", ReflectionTestUtils.getField(response, "id")); + Assert.assertNull(ReflectionTestUtils.getField(response, "webhookId")); + Assert.assertNull(ReflectionTestUtils.getField(response, "webhookName")); + Assert.assertEquals("EventType", ReflectionTestUtils.getField(response, "type")); + Assert.assertEquals("Include", ReflectionTestUtils.getField(response, "mode")); + Assert.assertEquals("Exact", ReflectionTestUtils.getField(response, "matchType")); + Assert.assertEquals("value", ReflectionTestUtils.getField(response, "value")); + Assert.assertEquals(new Date(1000), ReflectionTestUtils.getField(response, "created")); + } + + @Test + public void listWebhooksReturnsEmptyResponseForNoWebhooks() { + ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getState()).thenReturn(null); + Mockito.when(cmd.getName()).thenReturn(null); + Mockito.when(cmd.getKeyword()).thenReturn(null); + Mockito.when(cmd.getScope()).thenReturn(null); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(WebhookJoinVO.class)); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(webhookJoinDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(webhookJoinDao.searchAndCount(Mockito.any(), Mockito.any())).thenReturn(new Pair<>(List.of(), 0)); + + ListResponse response = webhookApiServiceImpl.listWebhooks(cmd); + + Assert.assertTrue(response.getResponses().isEmpty()); + Assert.assertEquals(0, (int) response.getCount()); + } + + @Test(expected = InvalidParameterValueException.class) + public void listWebhooksThrowsExceptionForInvalidScope() { + ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class); + + Mockito.when(cmd.getScope()).thenReturn("InvalidScope"); + + webhookApiServiceImpl.listWebhooks(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void listWebhooksThrowsExceptionForInvalidState() { + ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class); + + Mockito.when(cmd.getState()).thenReturn("InvalidState"); + + webhookApiServiceImpl.listWebhooks(cmd); + } + + @Test + public void listWebhooksFiltersByValidScopeAndState() { + ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class); + Mockito.when(caller.getType()).thenReturn(Account.Type.DOMAIN_ADMIN); + WebhookJoinVO webhook = Mockito.mock(WebhookJoinVO.class); + Mockito.when(webhook.getState()).thenReturn(Webhook.State.Enabled); + Mockito.when(webhook.getScope()).thenReturn(Webhook.Scope.Domain); + + Mockito.when(cmd.getScope()).thenReturn("Domain"); + Mockito.when(cmd.getState()).thenReturn("Enabled"); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + Mockito.when(sb.entity()).thenReturn(Mockito.mock(WebhookJoinVO.class)); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + Mockito.when(sb.create()).thenReturn(sc); + Mockito.when(webhookJoinDao.createSearchBuilder()).thenReturn(sb); + Mockito.when(webhookJoinDao.searchAndCount(Mockito.any(), Mockito.any())).thenReturn(new Pair<>(List.of(webhook), 1)); + + ListResponse response = webhookApiServiceImpl.listWebhooks(cmd); + + Assert.assertEquals(1, (int) response.getCount()); + Assert.assertEquals(1, response.getResponses().size()); + } + + @Test(expected = InvalidParameterValueException.class) + public void listWebhooksThrowsExceptionForUnauthorizedScope() { + ListWebhooksCmd cmd = Mockito.mock(ListWebhooksCmd.class); + + Mockito.when(cmd.getScope()).thenReturn("Global"); + Mockito.when(caller.getType()).thenReturn(Account.Type.NORMAL); + + webhookApiServiceImpl.listWebhooks(cmd); + } + + @Test + public void createWebhookCreatesWebhookSuccessfully() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookJoinVO webhookJoinVO = Mockito.mock(WebhookJoinVO.class); + Mockito.when(webhookJoinVO.getState()).thenReturn(Webhook.State.Enabled); + Mockito.when(webhookJoinVO.getScope()).thenReturn(Webhook.Scope.Local); + + Mockito.when(cmd.getName()).thenReturn("webhookName"); + Mockito.when(cmd.getDescription()).thenReturn("webhookDescription"); + Mockito.when(cmd.getPayloadUrl()).thenReturn("https://example.com"); + Mockito.when(cmd.getSecretKey()).thenReturn("secretKey"); + Mockito.when(cmd.isSslVerification()).thenReturn(true); + Mockito.when(cmd.getScope()).thenReturn("Local"); + Mockito.when(cmd.getState()).thenReturn("Enabled"); + Mockito.when(caller.getType()).thenReturn(Account.Type.ADMIN); + Mockito.when(caller.getDomainId()).thenReturn(1L); + Mockito.when(webhookDao.persist(Mockito.any(WebhookVO.class))).thenReturn(webhook); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(webhookJoinDao.findById(1L)).thenReturn(webhookJoinVO); + Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd); + + WebhookResponse response = webhookApiServiceImpl.createWebhook(cmd); + + Assert.assertNotNull(response); + Assert.assertEquals("webhook", ReflectionTestUtils.getField(response, "objectName")); + } + + @Test(expected = InvalidParameterValueException.class) + public void createWebhookThrowsExceptionForInvalidScope() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + + Mockito.when(cmd.getScope()).thenReturn("InvalidScope"); + + webhookApiServiceImpl.createWebhook(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void createWebhookThrowsExceptionForInvalidState() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + + Mockito.when(cmd.getState()).thenReturn("InvalidState"); + + webhookApiServiceImpl.createWebhook(cmd); + } + + @Test(expected = IllegalArgumentException.class) + public void createWebhookThrowsExceptionForInvalidUrl() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + + Mockito.when(cmd.getPayloadUrl()).thenReturn("invalid-url"); + Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd); + + webhookApiServiceImpl.createWebhook(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void createWebhookThrowsExceptionForNonHttpsWithSslVerification() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + + Mockito.when(cmd.getPayloadUrl()).thenReturn("http://example.com"); + Mockito.when(cmd.isSslVerification()).thenReturn(true); + Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd); + Mockito.doNothing().when(webhookApiServiceImpl) + .validateWebhookOwnerPayloadUrl(caller, "http://example.com", null); + + webhookApiServiceImpl.createWebhook(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void createWebhookThrowsExceptionForDuplicatePayloadUrl() { + CreateWebhookCmd cmd = Mockito.mock(CreateWebhookCmd.class); + WebhookVO existingWebhook = Mockito.mock(WebhookVO.class); + + Mockito.when(cmd.getPayloadUrl()).thenReturn("https://example.com"); + Mockito.when(caller.getId()).thenReturn(1L); + Mockito.doReturn(caller).when(webhookApiServiceImpl).getOwner(cmd); + Mockito.when(webhookDao.findByAccountAndPayloadUrl(1L, "https://example.com")).thenReturn(existingWebhook); + + webhookApiServiceImpl.createWebhook(cmd); + } + + @Test + public void updateWebhookUpdatesAllFieldsSuccessfully() { + UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(webhook.getPayloadUrl()).thenReturn("http://abc.xyz"); + String updatedUrl = "https://cloudstack.apache.org/"; + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getName()).thenReturn("Updated Name"); + Mockito.when(cmd.getDescription()).thenReturn("Updated Description"); + Mockito.when(cmd.getPayloadUrl()).thenReturn(updatedUrl); + Mockito.when(cmd.getSecretKey()).thenReturn("UpdatedSecretKey"); + Mockito.when(cmd.isSslVerification()).thenReturn(true); + Mockito.when(cmd.getScope()).thenReturn("Local"); + Mockito.when(cmd.getState()).thenReturn("Enabled"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.when(webhook.getAccountId()).thenReturn(1L); + Mockito.when(accountManager.getAccount(1L)).thenReturn(caller); + Mockito.when(webhookDao.update(1L, webhook)).thenReturn(true); + Mockito.doReturn(Mockito.mock(WebhookResponse.class)).when(webhookApiServiceImpl) + .createWebhookResponse(1L); + + WebhookResponse response = webhookApiServiceImpl.updateWebhook(cmd); + + + Assert.assertNotNull(response); + Mockito.verify(webhook).setName("Updated Name"); + Mockito.verify(webhook).setDescription("Updated Description"); + Mockito.verify(webhook).setPayloadUrl(updatedUrl); + Mockito.verify(webhook).setSecretKey("UpdatedSecretKey"); + Mockito.verify(webhook).setSslVerification(true); + Mockito.verify(webhook).setScope(Webhook.Scope.Local); + Mockito.verify(webhook).setState(Webhook.State.Enabled); + } + + @Test(expected = InvalidParameterValueException.class) + public void updateWebhookThrowsExceptionForInvalidState() { + UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getState()).thenReturn("InvalidState"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + + webhookApiServiceImpl.updateWebhook(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void updateWebhookThrowsExceptionForInvalidScope() { + UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getScope()).thenReturn("InvalidScope"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.when(webhook.getAccountId()).thenReturn(1L); + Mockito.when(accountManager.getAccount(1L)).thenReturn(caller); + + webhookApiServiceImpl.updateWebhook(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void updateWebhookThrowsExceptionForNonHttpsWithSslVerification() { + UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + Mockito.when(webhook.getPayloadUrl()).thenReturn("http://abc.xyz"); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getPayloadUrl()).thenReturn("http://cloudstack.apache.org/"); + Mockito.when(cmd.isSslVerification()).thenReturn(true); + Mockito.when(webhook.getAccountId()).thenReturn(1L); + Mockito.when(accountManager.getAccount(1L)).thenReturn(caller); + Mockito.doNothing().when(webhookApiServiceImpl) + .validateWebhookOwnerPayloadUrl(caller, "http://cloudstack.apache.org/", webhook); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + + webhookApiServiceImpl.updateWebhook(cmd); + } + + @Test + public void updateWebhookDoesNotUpdateUnchangedFields() { + UpdateWebhookCmd cmd = Mockito.mock(UpdateWebhookCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(webhook.getPayloadUrl()).thenReturn("http://abc.xyz"); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getName()).thenReturn(null); + Mockito.when(cmd.getDescription()).thenReturn(null); + Mockito.when(cmd.getPayloadUrl()).thenReturn(null); + Mockito.when(cmd.getSecretKey()).thenReturn(null); + Mockito.when(cmd.isSslVerification()).thenReturn(null); + Mockito.when(cmd.getScope()).thenReturn(null); + Mockito.when(cmd.getState()).thenReturn(null); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.doReturn(Mockito.mock(WebhookResponse.class)).when(webhookApiServiceImpl) + .createWebhookResponse(1L); + + WebhookResponse response = webhookApiServiceImpl.updateWebhook(cmd); + + Assert.assertNotNull(response); + Mockito.verify(webhookDao, Mockito.never()).update(Mockito.anyLong(), Mockito.any()); + } + + @Test + public void listWebhookDeliveriesReturnsEmptyResponseForNoDeliveries() { + ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(null); + Mockito.when(cmd.getManagementServerId()).thenReturn(null); + Mockito.when(cmd.getKeyword()).thenReturn(null); + Mockito.when(cmd.getStartDate()).thenReturn(null); + Mockito.when(cmd.getEndDate()).thenReturn(null); + Mockito.when(cmd.getEventType()).thenReturn(null); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + Mockito.when(webhookDeliveryJoinDao.searchAndCountByListApiParameters(Mockito.any(), Mockito.anyList(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(List.of(), 0)); + + ListResponse response = webhookApiServiceImpl.listWebhookDeliveries(cmd); + + Assert.assertNotNull(response); + Assert.assertTrue(response.getResponses().isEmpty()); + Assert.assertEquals(0, (int) response.getCount()); + } + + @Test + public void listWebhookDeliveriesFiltersByWebhookId() { + ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class); + WebhookDeliveryJoinVO delivery = Mockito.mock(WebhookDeliveryJoinVO.class); + WebhookDeliveryResponse deliveryResponse = Mockito.mock(WebhookDeliveryResponse.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + Mockito.when(cmd.getManagementServerId()).thenReturn(null); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + Mockito.when(webhookDeliveryJoinDao.searchAndCountByListApiParameters(Mockito.any(), Mockito.anyList(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(List.of(delivery), 1)); + Mockito.doReturn(deliveryResponse).when(webhookApiServiceImpl).createWebhookDeliveryResponse(delivery); + Mockito.doReturn(null).when(webhookApiServiceImpl) + .basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null); + + ListResponse response = webhookApiServiceImpl.listWebhookDeliveries(cmd); + + Assert.assertNotNull(response); + Assert.assertEquals(1, response.getResponses().size()); + Assert.assertEquals(deliveryResponse, response.getResponses().get(0)); + Assert.assertEquals(1, (int) response.getCount()); + } + + @Test(expected = InvalidParameterValueException.class) + public void listWebhookDeliveriesThrowsExceptionForEndDateBeforeStartDate() { + ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class); + + Mockito.when(cmd.getStartDate()).thenReturn(new Date(System.currentTimeMillis())); + Mockito.when(cmd.getEndDate()).thenReturn(new Date(System.currentTimeMillis() - 1000)); + + webhookApiServiceImpl.listWebhookDeliveries(cmd); + } + + @Test + public void listWebhookDeliveriesFiltersByEventType() { + ListWebhookDeliveriesCmd cmd = Mockito.mock(ListWebhookDeliveriesCmd.class); + WebhookDeliveryJoinVO delivery = Mockito.mock(WebhookDeliveryJoinVO.class); + WebhookDeliveryResponse deliveryResponse = Mockito.mock(WebhookDeliveryResponse.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(null); + Mockito.when(cmd.getManagementServerId()).thenReturn(null); + Mockito.when(cmd.getEventType()).thenReturn("EventType"); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + Mockito.when(webhookDeliveryJoinDao.searchAndCountByListApiParameters(Mockito.any(), Mockito.anyList(), + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(List.of(delivery), 1)); + Mockito.doReturn(deliveryResponse).when(webhookApiServiceImpl).createWebhookDeliveryResponse(delivery); + + ListResponse response = webhookApiServiceImpl.listWebhookDeliveries(cmd); + + Assert.assertNotNull(response); + Assert.assertEquals(1, response.getResponses().size()); + Assert.assertEquals(deliveryResponse, response.getResponses().get(0)); + Assert.assertEquals(1, (int) response.getCount()); + } + + @Test + public void deleteWebhookDeliveryRemovesDeliveriesSuccessfully() { + DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + Mockito.when(cmd.getManagementServerId()).thenReturn(null); + Mockito.when(cmd.getStartDate()).thenReturn(null); + Mockito.when(cmd.getEndDate()).thenReturn(null); + Mockito.doReturn(null).when(webhookApiServiceImpl) + .basicWebhookDeliveryApiCheck(caller, null, 1L, null, null, null); + Mockito.when(webhookDeliveryDao.deleteByDeleteApiParams(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(5); + + int removed = webhookApiServiceImpl.deleteWebhookDelivery(cmd); + + Assert.assertEquals(5, removed); + Mockito.verify(webhookDeliveryDao).deleteByDeleteApiParams(null, 1L, null, null, null); + } + + @Test(expected = InvalidParameterValueException.class) + public void deleteWebhookDeliveryThrowsExceptionForInvalidDates() { + DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class); + Mockito.when(cmd.getStartDate()).thenReturn(new Date(System.currentTimeMillis())); + Mockito.when(cmd.getEndDate()).thenReturn(new Date(System.currentTimeMillis() - 1000)); + + webhookApiServiceImpl.deleteWebhookDelivery(cmd); + } + + @Test + public void deleteWebhookDeliveryHandlesNoDeliveriesToRemove() { + DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + Mockito.when(cmd.getManagementServerId()).thenReturn(null); + Mockito.when(cmd.getStartDate()).thenReturn(null); + Mockito.when(cmd.getEndDate()).thenReturn(null); + Mockito.when(webhookDao.findById(1L)).thenReturn(Mockito.mock(WebhookVO.class)); + Mockito.when(webhookDeliveryDao.deleteByDeleteApiParams(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(0); + + int removed = webhookApiServiceImpl.deleteWebhookDelivery(cmd); + + Assert.assertEquals(0, removed); + Mockito.verify(webhookDeliveryDao).deleteByDeleteApiParams(null, 1L, null, null, null); + } + + @Test(expected = PermissionDeniedException.class) + public void deleteWebhookDeliveryThrowsExceptionForUnauthorizedAccess() { + DeleteWebhookDeliveryCmd cmd = Mockito.mock(DeleteWebhookDeliveryCmd.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + Mockito.when(caller.getType()).thenReturn(Account.Type.NORMAL); + Mockito.when(webhookDao.findById(1L)).thenReturn(Mockito.mock(WebhookVO.class)); + + webhookApiServiceImpl.deleteWebhookDelivery(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void executeWebhookDeliveryThrowsExceptionWhenNoIdentifiersProvided() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(null); + Mockito.when(cmd.getPayloadUrl()).thenReturn(null); + + webhookApiServiceImpl.executeWebhookDelivery(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void executeWebhookDeliveryThrowsExceptionWhenBothDeliveryIdAndWebhookIdProvided() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getWebhookId()).thenReturn(2L); + + webhookApiServiceImpl.executeWebhookDelivery(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void executeWebhookDeliveryThrowsExceptionForInvalidDeliveryId() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + + webhookApiServiceImpl.executeWebhookDelivery(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void executeWebhookDeliveryThrowsExceptionForInvalidWebhookId() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + + webhookApiServiceImpl.executeWebhookDelivery(cmd); + } + + @Test + public void executeWebhookDeliveryExecutesSuccessfullyForValidDeliveryId() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + WebhookDeliveryVO delivery = Mockito.mock(WebhookDeliveryVO.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class); + WebhookDeliveryResponse response = Mockito.mock(WebhookDeliveryResponse.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getWebhookId()).thenReturn(null); + Mockito.when(webhookDeliveryDao.findById(1L)).thenReturn(delivery); + Mockito.when(delivery.getWebhookId()).thenReturn(2L); + Mockito.when(webhookDao.findById(2L)).thenReturn(webhook); + Mockito.when(webhookService.executeWebhookDelivery(delivery, webhook, null)).thenReturn(webhookDelivery); + Mockito.when(webhookDelivery.getId()).thenReturn(3L); + Mockito.when(webhookDeliveryJoinDao.findById(3L)).thenReturn(Mockito.mock(WebhookDeliveryJoinVO.class)); + Mockito.doReturn(response).when(webhookApiServiceImpl).createWebhookDeliveryResponse(Mockito.any()); + + WebhookDeliveryResponse result = webhookApiServiceImpl.executeWebhookDelivery(cmd); + + Assert.assertNotNull(result); + Assert.assertEquals(response, result); + } + + @Test + public void executeWebhookDeliveryExecutesSuccessfullyForValidWebhookId() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class); + WebhookDeliveryResponse response = Mockito.mock(WebhookDeliveryResponse.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.when(webhookService.executeWebhookDelivery(null, webhook, null)).thenReturn(webhookDelivery); + Mockito.when(webhookDelivery.getId()).thenReturn(WebhookDelivery.ID_DUMMY); + Mockito.doReturn(response).when(webhookApiServiceImpl).createTestWebhookDeliveryResponse(webhookDelivery, webhook); + + WebhookDeliveryResponse result = webhookApiServiceImpl.executeWebhookDelivery(cmd); + + Assert.assertNotNull(result); + Assert.assertEquals(response, result); + } + + @Test(expected = InvalidParameterValueException.class) + public void executeWebhookDeliveryThrowsExceptionForInvalidPayloadUrl() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + Mockito.when(cmd.getPayloadUrl()).thenReturn("invalid-url"); + + webhookApiServiceImpl.executeWebhookDelivery(cmd); + } + + @Test + public void executeWebhookDeliveryExecutesSuccessfullyForValidPayloadUrl() { + ExecuteWebhookDeliveryCmd cmd = Mockito.mock(ExecuteWebhookDeliveryCmd.class); + WebhookDelivery webhookDelivery = Mockito.mock(WebhookDelivery.class); + WebhookDeliveryResponse response = Mockito.mock(WebhookDeliveryResponse.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(null); + Mockito.when(cmd.getPayloadUrl()).thenReturn("https://example.com"); + Mockito.when(webhookService.executeWebhookDelivery(Mockito.eq(null), Mockito.any(Webhook.class), + Mockito.eq(null))).thenReturn(webhookDelivery); + Mockito.when(webhookDelivery.getId()).thenReturn(WebhookDelivery.ID_DUMMY); + Mockito.doReturn(response).when(webhookApiServiceImpl).createTestWebhookDeliveryResponse( + Mockito.eq(webhookDelivery), Mockito.any(Webhook.class)); + + WebhookDeliveryResponse result = webhookApiServiceImpl.executeWebhookDelivery(cmd); + + Assert.assertNotNull(result); + Assert.assertEquals(response, result); + } + + @Test + public void listWebhookFiltersReturnsEmptyResponseForNoFilters() { + ListWebhookFiltersCmd cmd = Mockito.mock(ListWebhookFiltersCmd.class); + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(null); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(new Pair<>(List.of(), 0)); + + ListResponse response = webhookApiServiceImpl.listWebhookFilters(cmd); + + Assert.assertNotNull(response); + Assert.assertTrue(response.getResponses().isEmpty()); + } + + @Test + public void listWebhookFiltersReturnsFiltersSuccessfully() { + ListWebhookFiltersCmd cmd = Mockito.mock(ListWebhookFiltersCmd.class); + WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class); + Mockito.when(filter.getWebhookId()).thenReturn(1L); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookFilterResponse filterResponse = Mockito.mock(WebhookFilterResponse.class); + + Mockito.when(cmd.getId()).thenReturn(null); + Mockito.when(cmd.getWebhookId()).thenReturn(1L); + Mockito.when(cmd.getStartIndex()).thenReturn(0L); + Mockito.when(cmd.getPageSizeVal()).thenReturn(10L); + Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(new Pair<>(List.of(filter), 1)); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.doReturn(filterResponse).when(webhookApiServiceImpl).createWebhookFilterResponse(filter, webhook); + + ListResponse response = webhookApiServiceImpl.listWebhookFilters(cmd); + + Assert.assertNotNull(response); + Assert.assertEquals(1, response.getResponses().size()); + Assert.assertEquals(filterResponse, response.getResponses().get(0)); + } + + @Test(expected = InvalidParameterValueException.class) + public void addWebhookFilterThrowsExceptionForInvalidWebhookId() { + AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class); + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(webhookDao.findById(1L)).thenReturn(null); + + webhookApiServiceImpl.addWebhookFilter(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void addWebhookFilterThrowsExceptionForInvalidMode() { + AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getMode()).thenReturn("InvalidMode"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + + webhookApiServiceImpl.addWebhookFilter(cmd); + } + + @Test + public void addWebhookFilterAddsFilterSuccessfully() { + AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class); + WebhookFilterResponse filterResponse = Mockito.mock(WebhookFilterResponse.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getType()).thenReturn("EventType"); + Mockito.when(cmd.getMode()).thenReturn("Include"); + Mockito.when(cmd.getMatchType()).thenReturn("Exact"); + Mockito.when(cmd.getValue()).thenReturn("value"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.when(webhookFilterDao.persist(Mockito.any(WebhookFilterVO.class))).thenReturn(filter); + Mockito.doReturn(filterResponse).when(webhookApiServiceImpl).createWebhookFilterResponse(filter, webhook); + + WebhookFilterResponse response = webhookApiServiceImpl.addWebhookFilter(cmd); + + Assert.assertNotNull(response); + Assert.assertEquals(filterResponse, response); + } + + @Test + public void addWebhookFilterAddsFilterSuccessfullyEvenWithExisting() { + AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class); + WebhookFilterVO newFilter = Mockito.mock(WebhookFilterVO.class); + WebhookFilterResponse filterResponse = Mockito.mock(WebhookFilterResponse.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(filter.getType()).thenReturn(WebhookFilter.Type.EventType); + Mockito.when(filter.getMode()).thenReturn(WebhookFilter.Mode.Exclude); + Mockito.when(filter.getMatchType()).thenReturn(WebhookFilter.MatchType.Prefix); + Mockito.when(filter.getValue()).thenReturn("value.old"); + Mockito.when(cmd.getType()).thenReturn("EventType"); + Mockito.when(cmd.getMode()).thenReturn("Include"); + Mockito.when(cmd.getMatchType()).thenReturn("Exact"); + Mockito.when(cmd.getValue()).thenReturn("value"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.when(webhookFilterDao.listByWebhook(1L)).thenReturn(List.of(filter)); + Mockito.when(webhookFilterDao.persist(Mockito.any(WebhookFilterVO.class))).thenReturn(newFilter); + Mockito.doReturn(filterResponse).when(webhookApiServiceImpl).createWebhookFilterResponse(newFilter, webhook); + + WebhookFilterResponse response = webhookApiServiceImpl.addWebhookFilter(cmd); + + Assert.assertNotNull(response); + Assert.assertEquals(filterResponse, response); + } + + @Test(expected = InvalidParameterValueException.class) + public void addWebhookFilterConflictsWithExisting() { + AddWebhookFilterCmd cmd = Mockito.mock(AddWebhookFilterCmd.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class); + + Mockito.when(webhook.getId()).thenReturn(1L); + Mockito.when(filter.getType()).thenReturn(WebhookFilter.Type.EventType); + Mockito.when(filter.getMode()).thenReturn(WebhookFilter.Mode.Exclude); + Mockito.when(filter.getMatchType()).thenReturn(WebhookFilter.MatchType.Prefix); + Mockito.when(filter.getValue()).thenReturn("value"); + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(cmd.getType()).thenReturn("EventType"); + Mockito.when(cmd.getMode()).thenReturn("Include"); + Mockito.when(cmd.getMatchType()).thenReturn("Exact"); + Mockito.when(cmd.getValue()).thenReturn("value.extra"); + Mockito.when(webhookDao.findById(1L)).thenReturn(webhook); + Mockito.when(webhookFilterDao.listByWebhook(1L)).thenReturn(List.of(filter)); + + webhookApiServiceImpl.addWebhookFilter(cmd); + } + + @Test + public void deleteWebhookFilterDeletesFilterSuccessfully() { + DeleteWebhookFilterCmd cmd = Mockito.mock(DeleteWebhookFilterCmd.class); + WebhookFilterVO filter = Mockito.mock(WebhookFilterVO.class); + WebhookVO webhook = Mockito.mock(WebhookVO.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(new Pair<>(List.of(filter), 1)); + Mockito.when(webhookDao.findById(Mockito.anyLong())).thenReturn(webhook); + Mockito.when(webhookFilterDao.delete(Mockito.anyLong(), Mockito.anyLong())).thenReturn(1); + + int result = webhookApiServiceImpl.deleteWebhookFilter(cmd); + + Assert.assertEquals(1, result); + } + + @Test + public void deleteWebhookFilterHandlesNoFiltersToDelete() { + DeleteWebhookFilterCmd cmd = Mockito.mock(DeleteWebhookFilterCmd.class); + + Mockito.when(cmd.getId()).thenReturn(1L); + Mockito.when(webhookFilterDao.searchBy(Mockito.any(), Mockito.any(), Mockito.anyLong(), Mockito.anyLong())) + .thenReturn(new Pair<>(List.of(), 0)); + + int result = webhookApiServiceImpl.deleteWebhookFilter(cmd); + + Assert.assertEquals(0, result); + } } diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java new file mode 100644 index 000000000000..90c0c323d0dc --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/WebhookServiceImplTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 context = Mockito.mock(WebhookServiceImpl.ManualDeliveryContext.class); + CompletableFuture future = Mockito.mock(CompletableFuture.class); + + Mockito.when(context.getFuture()).thenReturn(future); + AsyncCallbackDispatcher 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 webhooks = List.of(webhook1, webhook2); + Pair, 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 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 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 cache = Mockito.mock(LazyCache.class); + ReflectionTestUtils.setField(webhookServiceImpl, "webhookFiltersCache", cache); + + webhookServiceImpl.invalidateWebhookFiltersCache(123L); + + Mockito.verify(cache, Mockito.times(1)).invalidate(123L); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/AddWebhookFilterCmdTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/AddWebhookFilterCmdTest.java new file mode 100644 index 000000000000..4c1d7187979d --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/AddWebhookFilterCmdTest.java @@ -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()); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDeliveryCmdTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDeliveryCmdTest.java index 2a090eb7fb12..607dd5fc8fe4 100644 --- a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDeliveryCmdTest.java +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookDeliveryCmdTest.java @@ -19,6 +19,7 @@ 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 void testExecute() { 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()); + } } diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookFilterCmdTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookFilterCmdTest.java new file mode 100644 index 000000000000..ae5a49835ded --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/DeleteWebhookFilterCmdTest.java @@ -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()); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookFiltersCmdTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookFiltersCmdTest.java new file mode 100644 index 000000000000..7e936c285aed --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/api/command/user/ListWebhookFiltersCmdTest.java @@ -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 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()); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDaoImplTest.java new file mode 100644 index 000000000000..c2f0a8d3d3bd --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDaoImplTest.java @@ -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 mockSearchBuilder; + @Mock + private SearchCriteria 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 domainIds = List.of(2L, 3L); + + doReturn(List.of(mockWebhookVO)).when(webhookDao).listBy(any(SearchCriteria.class)); + + List 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 domainIds = Collections.emptyList(); + + doReturn(Collections.emptyList()).when(webhookDao).listBy(any(SearchCriteria.class)); + + List 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 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 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); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java new file mode 100644 index 000000000000..ff0f222cdcc6 --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryDaoImplTest.java @@ -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 mockSearchBuilder; + @Mock + private SearchCriteria 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 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)); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java new file mode 100644 index 000000000000..f7561536a3f9 --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookDeliveryJoinDaoImplTest.java @@ -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 mockSearchBuilder; + @Mock + private SearchCriteria 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, 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, 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, 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); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDaoImplTest.java new file mode 100644 index 000000000000..2f5efe2d9a6a --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookFilterDaoImplTest.java @@ -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 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, 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, 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 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 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); + } +} diff --git a/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookJoinDaoImplTest.java b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookJoinDaoImplTest.java new file mode 100644 index 000000000000..a77a2de8da60 --- /dev/null +++ b/plugins/event-bus/webhook/src/test/java/org/apache/cloudstack/mom/webhook/dao/WebhookJoinDaoImplTest.java @@ -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 mockSearchBuilder; + @Mock + private SearchCriteria 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 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 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 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); + } +} diff --git a/test/integration/smoke/test_webhook_lifecycle.py b/test/integration/smoke/test_webhook_lifecycle.py index 2d1f322be6b0..55947d71c809 100644 --- a/test/integration/smoke/test_webhook_lifecycle.py +++ b/test/integration/smoke/test_webhook_lifecycle.py @@ -283,7 +283,7 @@ def test_11_update_webhook(self): description=description, secretkey=secretkey, state=state - )['webhook'] + ) self.assertNotEqual( updated_webhook, None, diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 805ea1adae94..b9f657ab031d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -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...", @@ -632,6 +635,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", @@ -806,6 +810,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", @@ -1035,8 +1040,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", @@ -1075,6 +1082,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", @@ -1253,6 +1261,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", @@ -1518,6 +1527,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)", @@ -2433,6 +2443,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", @@ -3282,6 +3293,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", @@ -3817,6 +3829,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", @@ -3830,6 +3843,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", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 47aa3d2ddef1..6445bdf61cf1 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -924,6 +924,16 @@ @pressEnter="saveValue(record)" > +
+
+
+ + + + + + + {{ $t('label.include') }} + {{ $t('label.exclude') }} + + + + + + + + + {{ $t('label.exact') }} + {{ $t('label.prefix') }} + {{ $t('label.suffix') }} + {{ $t('label.contains') }} + + + + + + + + + + + + + + + + + + {{ $t('label.reset') }} + {{ $t('label.add') }} + + + + +
+ + + + + {{ (selectedRowKeys && selectedRowKeys.length > 0) ? $t('label.action.delete.webhook.filters') : $t('label.action.clear.webhook.filters') }} + + + + + +
+ + + + + diff --git a/ui/src/config/section/tools.js b/ui/src/config/section/tools.js index a07228ca87b4..5b7f4b9af325 100644 --- a/ui/src/config/section/tools.js +++ b/ui/src/config/section/tools.js @@ -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'))) diff --git a/utils/src/main/java/com/cloud/utils/EnumUtils.java b/utils/src/main/java/com/cloud/utils/EnumUtils.java index 02b6a25b8955..c5cf01679ec1 100644 --- a/utils/src/main/java/com/cloud/utils/EnumUtils.java +++ b/utils/src/main/java/com/cloud/utils/EnumUtils.java @@ -55,4 +55,12 @@ public static > T fromString(Class clz, String value) { } return null; } + + public static > T getEnumIgnoreCase(final Class enumClass, final String enumName) { + return org.apache.commons.lang3.EnumUtils.getEnumIgnoreCase(enumClass, enumName); + } + + public static > T getEnumIgnoreCase(final Class enumClass, final String enumName, T defaultValue) { + return org.apache.commons.lang3.EnumUtils.getEnumIgnoreCase(enumClass, enumName, defaultValue); + } }