atiaomar1978-hub commented on code in PR #26010: URL: https://github.com/apache/camel/pull/26010#discussion_r3910978231
########## components/camel-alibaba/camel-alibaba-eventbridge/src/main/java/org/apache/camel/component/alibaba/eventbridge/EventSourceCache.java: ########## @@ -0,0 +1,362 @@ +/* + * 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.camel.component.alibaba.eventbridge; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.aliyun.eventbridge.EventBridgeClient; +import com.aliyun.eventbridge.models.EventBusEntry; +import com.aliyun.eventbridge.models.EventRuleDTO; +import com.aliyun.eventbridge.models.ListEventBusesRequest; +import com.aliyun.eventbridge.models.ListEventBusesResponse; +import com.aliyun.eventbridge.models.ListRulesRequest; +import com.aliyun.eventbridge.models.ListRulesResponse; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventBus; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventSource; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TTL-based in-memory cache for verified Alibaba Cloud EventBridge bus, event source, and source-scoped event type + * definitions. + * <p> + * Implements a two-phase validated cache workflow: + * <ol> + * <li>Fetch and validate configured event sources and event types against Alibaba Cloud API ({@code listEventBuses} and + * {@code listRules}).</li> + * <li><b>Only after validation passes</b>, populate the cache with the verified {@link BusMetadata} for fast runtime + * comparison.</li> + * </ol> + */ +final class EventSourceCache { + + private static final Logger LOG = LoggerFactory.getLogger(EventSourceCache.class); + private static final Gson GSON = new Gson(); + private static final int PAGE_LIMIT = 100; + + /** + * Java 16 record holding cached bus metadata including mapped event sources and their permitted event types. + */ + public record BusMetadata(boolean exists, Map<String, Set<String>> sourceToTypesMap) { + public BusMetadata { + if (sourceToTypesMap == null) { + sourceToTypesMap = Collections.emptyMap(); + } else { + Map<String, Set<String>> unmodifiable = new HashMap<>(); + for (Map.Entry<String, Set<String>> entry : sourceToTypesMap.entrySet()) { + unmodifiable.put(entry.getKey(), Collections.unmodifiableSet(new HashSet<>(entry.getValue()))); + } + sourceToTypesMap = Collections.unmodifiableMap(unmodifiable); + } + } Review Comment: When rule metadata is unavailable, isKnownSource/isKnownType return true. Document this explicitly in the component guide, or tighten behaviour when validateEventSource/validateEventType is enabled. ########## components/camel-alibaba/camel-alibaba-eventbridge/src/main/java/org/apache/camel/component/alibaba/eventbridge/AlibabaEventBridgeUtils.java: ########## @@ -62,117 +74,346 @@ public static EventBridgeClient createClient(AlibabaEventBridgeEndpoint endpoint } public static ClientConfigurations createClientConfigurations(AlibabaEventBridgeEndpoint endpoint, Exchange exchange) { - ClientConfigurations configuration = new ClientConfigurations(); - configuration.setOperation( - OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.OPERATION, endpoint.getOperation())); - configuration.setEventBusName( - OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.EVENT_BUS_NAME, - endpoint.getEventBusName())); - configuration.setEventSource( + String defaultBusName = OpenApiClientSupport.resolveString( + exchange, AlibabaEventBridgeProperties.EVENT_BUS_NAME, endpoint.getEventBusName()); + + return new ClientConfigurations( + OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.OPERATION, endpoint.getOperation()), + defaultBusName, OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.EVENT_SOURCE, - endpoint.getEventSource())); - configuration.setEventType( - OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.EVENT_TYPE, endpoint.getEventType())); - configuration.setEventSubject( + endpoint.getEventSource()), + OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.EVENT_TYPE, endpoint.getEventType()), OpenApiClientSupport.resolveString(exchange, AlibabaEventBridgeProperties.EVENT_SUBJECT, - endpoint.getEventSubject())); - return configuration; + endpoint.getEventSubject()), + OpenApiClientSupport.resolveBoolean(exchange, AlibabaEventBridgeProperties.VALIDATE_EVENT_SOURCE, + endpoint.isValidateEventSource()), + OpenApiClientSupport.resolveBoolean(exchange, AlibabaEventBridgeProperties.VALIDATE_EVENT_TYPE, + endpoint.isValidateEventType()), + OpenApiClientSupport.resolveBoolean(exchange, AlibabaEventBridgeProperties.VALIDATE_EVENT_SPEC, + endpoint.isValidateEventSpec()), + resolveAllowedEventBuses(exchange, AlibabaEventBridgeProperties.ALLOWED_EVENT_SOURCES, + endpoint.getAllowedEventSources(), defaultBusName), + OpenApiClientSupport.resolveLong(exchange, AlibabaEventBridgeProperties.EVENT_SOURCE_CACHE_TTL, + endpoint.getEventSourceCacheTtl())); } public static List<CloudEvent> resolveCloudEvents(Exchange exchange, ClientConfigurations configuration) { + return resolveCloudEvents(exchange, configuration, null, null); + } + + public static List<CloudEvent> resolveCloudEvents( + Exchange exchange, ClientConfigurations configuration, + EventSourceCache eventSourceCache, EventBridgeClient client) { Object body = exchange.getMessage().getBody(); List<CloudEvent> events = new ArrayList<>(); + MapCloudEventValidator mapValidator = new MapCloudEventValidator(eventSourceCache); if (body instanceof List<?> listBody) { for (Object item : listBody) { - events.add(toCloudEvent(item, configuration)); + events.add(toCloudEvent(item, configuration, mapValidator, client)); } return events; } - events.add(toCloudEvent(body, configuration)); + events.add(toCloudEvent(body, configuration, mapValidator, client)); return events; } - private static CloudEvent toCloudEvent(Object body, ClientConfigurations configuration) { + private static CloudEvent toCloudEvent( + Object body, ClientConfigurations configuration, + MapCloudEventValidator mapValidator, EventBridgeClient client) { if (body instanceof CloudEvent cloudEvent) { + mapValidator.validateCloudEvent(cloudEvent, configuration, client); return cloudEvent; } if (body instanceof Map<?, ?> mapBody) { - String eventBusName - = stringValue(mapBody.get(AlibabaEventBridgeConstants.EVENT_BUS_NAME), configuration.getEventBusName()); - String source = stringValue(mapBody.get(AlibabaEventBridgeConstants.EVENT_SOURCE), configuration.getEventSource()); - String type = stringValue(mapBody.get(AlibabaEventBridgeConstants.EVENT_TYPE), configuration.getEventType()); - String subject - = stringValue(mapBody.get(AlibabaEventBridgeConstants.EVENT_SUBJECT), configuration.getEventSubject()); - String data = jsonDataValue(mapBody.get(AlibabaEventBridgeConstants.EVENT_DATA)); - - if (ObjectHelper.isEmpty(source) || ObjectHelper.isEmpty(type) || ObjectHelper.isEmpty(eventBusName)) { - throw new IllegalArgumentException("Event source, type and event bus name are required"); - } - - EventBuilder builder = EventBuilder.builder() - .withSource(URI.create(source)) - .withType(type) - .withAliyunEventBus(eventBusName); - - if (ObjectHelper.isNotEmpty(subject)) { - builder.withSubject(subject); - } - if (data != null) { - builder.withJsonStringData(data); - } - return builder.build(); + return mapValidator.validateAndBuild(mapBody, configuration, client); } if (body instanceof String stringBody) { - if (ObjectHelper.isEmpty(configuration.getEventSource()) - || ObjectHelper.isEmpty(configuration.getEventType()) - || ObjectHelper.isEmpty(configuration.getEventBusName())) { + if (ObjectHelper.isEmpty(configuration.eventSource()) + || ObjectHelper.isEmpty(configuration.eventType()) + || ObjectHelper.isEmpty(configuration.eventBusName())) { throw new IllegalArgumentException("Event source, type and event bus name are required when body is a string"); } + mapValidator.validateBusSourceAndType( + configuration.eventBusName(), configuration.eventSource(), configuration.eventType(), + configuration, client); + EventBuilder builder = EventBuilder.builder() Review Comment: Security note: ALLOWED_EVENT_SOURCES can be overridden via header/property. If routes accept untrusted ingress, this can replace the endpoint whitelist per message. Add untrusted-ingress guidance to strip CamelAlibabaEventBridge* headers before the producer when URI options are policy. ########## components/camel-alibaba/camel-alibaba-eventbridge/src/main/java/org/apache/camel/component/alibaba/eventbridge/AlibabaEventBridgeEndpoint.java: ########## @@ -74,12 +74,38 @@ public class AlibabaEventBridgeEndpoint extends DefaultEndpoint { @UriParam(description = "Default event subject", displayName = "Event Subject") Review Comment: Missing upgrade guide: validateEventSpec defaults to true for map payloads. Routes with non-1.0 specversion or blank id will start failing. Add a camel-alibaba-eventbridge section to the 4.23 upgrade guide. ########## components/camel-alibaba/camel-alibaba-eventbridge/src/main/docs/alibaba-eventbridge-component.adoc: ########## @@ -44,56 +44,243 @@ include::partial$component-endpoint-headers.adoc[] == Usage -=== Message headers evaluated by the EventBridge producer +=== Message Headers and Properties -[width="100%",cols="10%,10%,80%",options="header",] +The component defines constants in package `org.apache.camel.component.alibaba.eventbridge.constants`: + +* `AlibabaEventBridgeHeaders` defines all message header constants evaluated or set by the EventBridge producer. +* `AlibabaEventBridgeProperties` extends `AlibabaEventBridgeHeaders` and provides constants for Exchange properties (such as `OPERATION`), while inheriting all header constants. + +==== Message headers evaluated by the EventBridge producer + +[width="100%",cols="25%,25%,10%,40%",options="header",] |======================================================================= -|Header |Type |Description +|Constant (`AlibabaEventBridgeHeaders`) |Header String |Type |Description + +|`EVENT_BUS_NAME` |`CamelAlibabaEventBridgeEventBusName` |`String` | Event bus name to publish events to (overrides the endpoint option). + +|`EVENT_SOURCE` |`CamelAlibabaEventBridgeEventSource` |`String` | Event source URI (overrides the endpoint option). -|`CamelAlibabaEventBridgeOperation` |`String` | Name of operation to invoke +|`EVENT_TYPE` |`CamelAlibabaEventBridgeEventType` |`String` | Event type (overrides the endpoint option). -|`CamelAlibabaEventBridgeEventBusName` |`String` | Event bus name to publish events to (overrides the endpoint option) +|`EVENT_SUBJECT` |`CamelAlibabaEventBridgeEventSubject` |`String` | Event subject (overrides the endpoint option). -|`CamelAlibabaEventBridgeEventSource` |`String` | Event source URI (overrides the endpoint option) +|`VALIDATE_EVENT_SOURCE` |`CamelAlibabaEventBridgeValidateEventSource` |`boolean` | Whether to validate the event bus and event source against Alibaba Cloud. -|`CamelAlibabaEventBridgeEventType` |`String` | Event type (overrides the endpoint option) +|`VALIDATE_EVENT_TYPE` |`CamelAlibabaEventBridgeValidateEventType` |`boolean` | Whether to validate the event type against Alibaba Cloud rule filter patterns for that source. -|`CamelAlibabaEventBridgeEventSubject` |`String` | Event subject (overrides the endpoint option) +|`VALIDATE_EVENT_SPEC` |`CamelAlibabaEventBridgeValidateEventSpec` |`boolean` | Whether to enforce CloudEvents 1.0 specification compliance checks (defaults to `true`). + +|`ALLOWED_EVENT_SOURCES` |`CamelAlibabaEventBridgeAllowedEventSources` |`Object` | Allowed event sources and source-scoped event types per bus (DSL string, JSON string, Map, or List). + +|`EVENT_SOURCE_CACHE_TTL` |`CamelAlibabaEventBridgeEventSourceCacheTtl` |`long` | Cache time-to-live in milliseconds for verified Alibaba Cloud event bus and type metadata (defaults to `300000` / 5 minutes). |======================================================================= -If any of the above headers are set, they will override their corresponding query parameter value. +If any of the above headers are set, they will override their corresponding exchange property or query parameter value. -=== Message headers set by the EventBridge producer +==== Message headers set by the EventBridge producer -[width="100%",cols="10%,10%,80%",options="header",] +[width="100%",cols="25%,25%,10%,40%",options="header",] +|======================================================================= +|Constant (`AlibabaEventBridgeHeaders`) |Header String |Type |Description + +|`REQUEST_ID` |`CamelAlibabaEventBridgeRequestId` |`String` | Alibaba Cloud request ID returned by EventBridge. + +|======================================================================= Review Comment: Docs CI failure: website build reports AsciiDoc list item index warnings at lines 140-145 (nested ordered list under item 1). Use list continuations (+) or restructure the numbered sub-list so the site build passes. ########## components/camel-alibaba/camel-alibaba-eventbridge/src/main/java/org/apache/camel/component/alibaba/eventbridge/EventSourceCache.java: ########## @@ -0,0 +1,362 @@ +/* + * 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.camel.component.alibaba.eventbridge; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.aliyun.eventbridge.EventBridgeClient; +import com.aliyun.eventbridge.models.EventBusEntry; +import com.aliyun.eventbridge.models.EventRuleDTO; +import com.aliyun.eventbridge.models.ListEventBusesRequest; +import com.aliyun.eventbridge.models.ListEventBusesResponse; +import com.aliyun.eventbridge.models.ListRulesRequest; +import com.aliyun.eventbridge.models.ListRulesResponse; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventBus; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventSource; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TTL-based in-memory cache for verified Alibaba Cloud EventBridge bus, event source, and source-scoped event type + * definitions. + * <p> + * Implements a two-phase validated cache workflow: + * <ol> + * <li>Fetch and validate configured event sources and event types against Alibaba Cloud API ({@code listEventBuses} and + * {@code listRules}).</li> + * <li><b>Only after validation passes</b>, populate the cache with the verified {@link BusMetadata} for fast runtime + * comparison.</li> + * </ol> + */ +final class EventSourceCache { + + private static final Logger LOG = LoggerFactory.getLogger(EventSourceCache.class); + private static final Gson GSON = new Gson(); + private static final int PAGE_LIMIT = 100; + + /** + * Java 16 record holding cached bus metadata including mapped event sources and their permitted event types. + */ + public record BusMetadata(boolean exists, Map<String, Set<String>> sourceToTypesMap) { + public BusMetadata { + if (sourceToTypesMap == null) { + sourceToTypesMap = Collections.emptyMap(); + } else { + Map<String, Set<String>> unmodifiable = new HashMap<>(); + for (Map.Entry<String, Set<String>> entry : sourceToTypesMap.entrySet()) { + unmodifiable.put(entry.getKey(), Collections.unmodifiableSet(new HashSet<>(entry.getValue()))); + } + sourceToTypesMap = Collections.unmodifiableMap(unmodifiable); + } + } + + public boolean isKnownSource(String source) { + if (source == null || sourceToTypesMap.isEmpty()) { + return true; + } + return sourceToTypesMap.containsKey(source.trim()) || sourceToTypesMap.containsKey("*"); + } + + public boolean isKnownType(String source, String eventType) { + if (eventType == null || sourceToTypesMap.isEmpty()) { + return true; + } + Set<String> types = sourceToTypesMap.get(source != null ? source.trim() : null); + if (types == null || types.isEmpty()) { + types = sourceToTypesMap.get("*"); + } + if (types == null || types.isEmpty()) { + return true; + } + return types.contains(eventType.trim()); + } + } + + /** + * Java 16 record representing a TTL-aware cache entry. + */ + public record CacheEntry<T>(T value, long expiryTime) { + public boolean isExpired(long now) { + return now >= expiryTime; + } + } + + private final Map<String, CacheEntry<BusMetadata>> cache = new ConcurrentHashMap<>(); + private final long ttlMillis; + + EventSourceCache(long ttlMillis) { + this.ttlMillis = ttlMillis; + } + + /** + * Validates the given {@code eventBusName} and any configured {@link AllowedEventBus} definitions against Alibaba + * Cloud. Upon successful validation, the verified metadata is stored in the cache. + * + * @param eventBusName the target event bus name + * @param allowedBus the configured whitelist rules for this bus, or {@code null} + * @param validateSource whether to validate event source existence against Alibaba Cloud + * @param validateType whether to validate event types against Alibaba Cloud rule filter patterns + * @param client the EventBridge client instance; if {@code null}, validation is bypassed + * @return the verified {@link BusMetadata} + */ + BusMetadata validateAndUpdateCache( + String eventBusName, AllowedEventBus allowedBus, + boolean validateSource, boolean validateType, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName)) { + return new BusMetadata(true, Collections.emptyMap()); + } + + long now = System.currentTimeMillis(); + CacheEntry<BusMetadata> entry = cache.get(eventBusName); + if (entry != null && !entry.isExpired(now)) { + return entry.value(); + } + + boolean busExists = fetchEventBusExists(eventBusName, client); + if (!busExists) { + throw new IllegalArgumentException( + String.format("Event bus '%s' does not exist in Alibaba Cloud EventBridge", eventBusName)); + } + + Map<String, Set<String>> cloudSourceToTypes = fetchCloudSourceToTypes(eventBusName, client); + + if (validateSource && allowedBus != null && !allowedBus.allowedSources().isEmpty() && !cloudSourceToTypes.isEmpty()) { + for (String source : allowedBus.allowedSources().keySet()) { + if (!cloudSourceToTypes.containsKey(source) && !cloudSourceToTypes.containsKey("*")) { + throw new IllegalArgumentException( + String.format( + "Event source '%s' is not registered in Alibaba Cloud rules for event bus '%s'. Known sources: %s", + source, eventBusName, cloudSourceToTypes.keySet())); + } + } + } + + if (validateType && allowedBus != null && !allowedBus.allowedSources().isEmpty() && !cloudSourceToTypes.isEmpty()) { + for (Map.Entry<String, AllowedEventSource> sourceEntry : allowedBus.allowedSources().entrySet()) { + String source = sourceEntry.getKey(); + Set<String> allowedTypes = sourceEntry.getValue().allowedEventTypes(); + if (allowedTypes != null && !allowedTypes.isEmpty()) { + Set<String> cloudTypes = cloudSourceToTypes.get(source); + if (cloudTypes == null || cloudTypes.isEmpty()) { + cloudTypes = cloudSourceToTypes.get("*"); + } + if (cloudTypes != null && !cloudTypes.isEmpty()) { + for (String type : allowedTypes) { + if (!cloudTypes.contains(type)) { + throw new IllegalArgumentException( + String.format( + "Event type '%s' is not registered in Alibaba Cloud rules for source '%s' on bus '%s'. Allowed in Cloud: %s", + type, source, eventBusName, cloudTypes)); + } + } + } + } + } + } + + BusMetadata metadata = new BusMetadata(true, cloudSourceToTypes); + cache.put(eventBusName, new CacheEntry<>(metadata, now + ttlMillis)); + return metadata; + } + + /** + * Checks if the event bus is known to exist. + */ + boolean isKnownEventBus(String eventBusName, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName)) { + return true; + } + long now = System.currentTimeMillis(); + CacheEntry<BusMetadata> entry = cache.get(eventBusName); + if (entry != null && !entry.isExpired(now)) { + return entry.value().exists(); + } + return fetchEventBusExists(eventBusName, client); + } + + /** + * Checks if the event source is registered for the given bus. + */ + boolean isKnownEventSource(String eventBusName, String eventSource, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName) || ObjectHelper.isEmpty(eventSource)) { + return true; + } + BusMetadata metadata = validateAndUpdateCache(eventBusName, null, false, false, client); + return metadata.isKnownSource(eventSource); + } + + /** + * Checks if the event type is valid for the given event source on the bus. + */ + boolean isKnownEventType(String eventBusName, String eventSource, String eventType, EventBridgeClient client) { + if (client == null || ObjectHelper.isEmpty(eventBusName) || ObjectHelper.isEmpty(eventType)) { + return true; + } + BusMetadata metadata = validateAndUpdateCache(eventBusName, null, false, false, client); + return metadata.isKnownType(eventSource, eventType); + } + + /** + * Fetches whether the event bus exists via {@code listEventBuses}. + */ + boolean fetchEventBusExists(String eventBusName, EventBridgeClient client) { + try { Review Comment: Fail-open validation (medium): On listEventBuses API failure this returns true (see also line 230), so validateEventSource can be bypassed during cloud/API errors. Same pattern in fetchCloudSourceToTypes with empty metadata. ########## components/camel-alibaba/camel-alibaba-eventbridge/src/test/java/org/apache/camel/component/alibaba/eventbridge/MapCloudEventValidationTest.java: ########## @@ -0,0 +1,332 @@ +/* + * 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.camel.component.alibaba.eventbridge; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.aliyun.eventbridge.EventBridgeClient; +import com.aliyun.eventbridge.models.CloudEvent; +import com.aliyun.eventbridge.models.EventBusEntry; +import com.aliyun.eventbridge.models.EventRuleDTO; +import com.aliyun.eventbridge.models.ListEventBusesRequest; +import com.aliyun.eventbridge.models.ListEventBusesResponse; +import com.aliyun.eventbridge.models.ListRulesRequest; +import com.aliyun.eventbridge.models.ListRulesResponse; +import org.apache.camel.Exchange; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventBus; +import org.apache.camel.component.alibaba.eventbridge.models.AllowedEventSource; +import org.apache.camel.component.alibaba.eventbridge.models.ClientConfigurations; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MapCloudEventValidationTest extends CamelTestSupport { + + private EventBridgeClient eventBridgeClient; + private EventSourceCache eventSourceCache; + private MapCloudEventValidator validator; + + @BeforeEach + void initTest() { + eventBridgeClient = mock(EventBridgeClient.class); + eventSourceCache = new EventSourceCache(300000L); + validator = new MapCloudEventValidator(eventSourceCache); + } + + @Test + void testValidateAndBuildSuccessWithFullMap() { + ClientConfigurations config + = new ClientConfigurations(null, "default-bus", null, null, null, false, false, true, Map.of(), 300000L); + + Map<String, Object> map = new HashMap<>(); + map.put("eventBusName", "my-bus"); + map.put("source", "acs:oss:cn-hangzhou:12345:my-bucket"); + map.put("type", "oss:ObjectCreated:PutObject"); + map.put("id", "event-id-123"); + map.put("specversion", "1.0"); + map.put("subject", "my-object.jpg"); + map.put("time", "2026-08-23T10:15:30Z"); + map.put("datacontenttype", "application/json"); + map.put("dataschema", "http://example.com/schema.json"); + map.put("data", Map.of("fileSize", 1024, "bucket", "my-bucket")); + + CloudEvent event = validator.validateAndBuild(map, config, eventBridgeClient); + + assertThat(event).isNotNull(); + assertThat(event.getSource().toString()).isEqualTo("acs:oss:cn-hangzhou:12345:my-bucket"); + assertThat(event.getType()).isEqualTo("oss:ObjectCreated:PutObject"); + assertThat(event.getId()).isEqualTo("event-id-123"); + assertThat(event.getSubject()).isEqualTo("my-object.jpg"); + assertThat(event.getSpecversion()).isEqualTo("1.0"); + assertThat(event.getDatacontenttype()).isEqualTo("application/json"); + assertThat(event.getDataschema().toString()).isEqualTo("http://example.com/schema.json"); + assertThat(new String(event.getData(), StandardCharsets.UTF_8)).contains("\"fileSize\":1024"); + } + + @Test + void testValidateAndBuildWithFallbackConfig() { + ClientConfigurations config + = new ClientConfigurations(null, "default-bus", "my.custom.app", "order.created", "order-999"); + + Map<String, Object> map = new HashMap<>(); + map.put("data", "{\"orderId\":\"999\"}"); + + CloudEvent event = validator.validateAndBuild(map, config, eventBridgeClient); + + assertThat(event).isNotNull(); + assertThat(event.getSource().toString()).isEqualTo("my.custom.app"); + assertThat(event.getType()).isEqualTo("order.created"); + assertThat(event.getSubject()).isEqualTo("order-999"); + assertThat(new String(event.getData(), StandardCharsets.UTF_8)).isEqualTo("{\"orderId\":\"999\"}"); + } + + @Test + void testValidateFailsWhenBusNameMissing() { + ClientConfigurations config = new ClientConfigurations(); + Map<String, Object> map = Map.of("source", "my.source", "type", "my.type"); + + assertThatThrownBy(() -> validator.validateAndBuild(map, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Event bus name is required"); + } + + @Test + void testValidateFailsWhenSourceMissing() { + ClientConfigurations config = new ClientConfigurations(null, "test-bus", null, null, null); + Map<String, Object> map = Map.of("type", "my.type"); + + assertThatThrownBy(() -> validator.validateAndBuild(map, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Event 'source' cannot be empty"); + } + + @Test + void testValidateFailsWhenTypeMissing() { + ClientConfigurations config = new ClientConfigurations(null, "test-bus", "my.source", null, null); + Map<String, Object> map = Map.of("source", "my.source"); + + assertThatThrownBy(() -> validator.validateAndBuild(map, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Event 'type' cannot be empty"); + } + + @Test + void testValidateFailsWhenInvalidSpecversion() { + ClientConfigurations config = new ClientConfigurations(null, "test-bus", null, null, null, false, true); + + Map<String, Object> map = Map.of( + "source", "my.source", + "type", "my.type", + "specversion", "0.3"); + + assertThatThrownBy(() -> validator.validateAndBuild(map, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid CloudEvent specversion: '0.3'"); + } + + @Test + void testSingleBusDslWithColonsInSourceAndTypes() { + String dsl + = "acs:oss:cn-hangzhou:12345:my-bucket -> oss:ObjectCreated:PutObject, oss:ObjectCreated:PostObject ; app.orders -> order:created:v1"; + Map<String, AllowedEventBus> buses = AlibabaEventBridgeUtils.parseAllowedBusesFromString(dsl, "order-bus"); + + assertThat(buses).containsKey("order-bus"); + AllowedEventBus bus = buses.get("order-bus"); + assertThat(bus.allowedSources()).containsKeys("acs:oss:cn-hangzhou:12345:my-bucket", "app.orders"); + + AllowedEventSource ossSource = bus.allowedSources().get("acs:oss:cn-hangzhou:12345:my-bucket"); + assertThat(ossSource.allowedEventTypes()).containsExactlyInAnyOrder( + "oss:ObjectCreated:PutObject", "oss:ObjectCreated:PostObject"); + + ClientConfigurations config = new ClientConfigurations( + null, "order-bus", null, null, null, false, false, true, buses, 300000L); + + Map<String, Object> validEvent = Map.of( + "source", "acs:oss:cn-hangzhou:12345:my-bucket", + "type", "oss:ObjectCreated:PutObject"); + + CloudEvent event = validator.validateAndBuild(validEvent, config, eventBridgeClient); + assertThat(event).isNotNull(); + + Map<String, Object> invalidTypeEvent = Map.of( + "source", "acs:oss:cn-hangzhou:12345:my-bucket", + "type", "oss:ObjectDeleted:DeleteObject"); + + assertThatThrownBy(() -> validator.validateAndBuild(invalidTypeEvent, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Event type 'oss:ObjectDeleted:DeleteObject' is not allowed for event source"); + } + + @Test + void testMultiBusDslWithColonsInSourceAndTypes() { + String dsl + = "orders-bus[ acs:oss:cn-hangzhou:12345:orders -> oss:ObjectCreated:PutObject ; app.orders -> order:created:v1 ]" + + " | payments-bus[ app.payments -> payment:authorized:v1, payment:captured:v1 ]"; + + Map<String, AllowedEventBus> buses = AlibabaEventBridgeUtils.parseAllowedBusesFromString(dsl, null); + assertThat(buses).containsKeys("orders-bus", "payments-bus"); + + ClientConfigurations config = new ClientConfigurations( + null, "orders-bus", null, null, null, false, false, true, buses, 300000L); + + Map<String, Object> event1 = Map.of( + "eventBusName", "orders-bus", + "source", "app.orders", + "type", "order:created:v1"); + assertThat(validator.validateAndBuild(event1, config, eventBridgeClient)).isNotNull(); + + Map<String, Object> event2 = Map.of( + "eventBusName", "payments-bus", + "source", "app.payments", + "type", "payment:captured:v1"); + assertThat(validator.validateAndBuild(event2, config, eventBridgeClient)).isNotNull(); + + Map<String, Object> invalidEvent1 = Map.of( + "eventBusName", "orders-bus", + "source", "app.payments", + "type", "payment:captured:v1"); + assertThatThrownBy(() -> validator.validateAndBuild(invalidEvent1, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Event source 'app.payments' is not in the allowed sources list for bus 'orders-bus'"); + + Map<String, Object> invalidEvent2 = Map.of( + "eventBusName", "unknown-bus", + "source", "app.orders", + "type", "order:created:v1"); + assertThatThrownBy(() -> validator.validateAndBuild(invalidEvent2, config, eventBridgeClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Event bus 'unknown-bus' is not in the allowed event buses list"); + } + + @Test + void testJsonConfigurationParsing() { + String json = """ + { + "orders-bus": { + "acs:oss:cn-hangzhou:12345:orders": ["oss:ObjectCreated:PutObject", "oss:ObjectCreated:PostObject"], + "app.orders": ["order:created:v1"] + }, + "payments-bus": { + "app.payments": ["payment:authorized:v1"] + } + } + """; + + Map<String, AllowedEventBus> buses = AlibabaEventBridgeUtils.parseAllowedBusesFromString(json, null); + assertThat(buses).containsKeys("orders-bus", "payments-bus"); + + AllowedEventBus ordersBus = buses.get("orders-bus"); + assertThat(ordersBus.allowedSources().get("acs:oss:cn-hangzhou:12345:orders").allowedEventTypes()) + .contains("oss:ObjectCreated:PutObject", "oss:ObjectCreated:PostObject"); + } + + @Test + void testValidatedCacheWorkflowSuccess() { Review Comment: Test coverage gaps: add validateCloudEvent for CloudEvent passthrough; API failure behaviour when validateEventSource=true; rejection when map body overrides bus/source outside whitelist; validateEventSpec=false disables spec checks. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
