This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git


The following commit(s) were added to refs/heads/main by this push:
     new 6b96370ad0 Document and test Avro trusted classes for the Salesforce 
Pub / Sub API
6b96370ad0 is described below

commit 6b96370ad07dc1e4b4a30854fc5b23501f6a9cf5
Author: James Netherton <[email protected]>
AuthorDate: Mon Oct 12 19:57:32 2026 +0100

    Document and test Avro trusted classes for the Salesforce Pub / Sub API
    
    * Correct the AvroGenerated claim in the 3.40.0 migration guide
    
    The guide said that Avro added @AvroGenerated in 1.9.0. The annotation and 
its
    use in generated records date back to at least Avro 1.7.4, so specific 
records
    generated before 1.9.0 are trusted automatically like any other.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    * Document and test Avro trusted classes for the Salesforce Pub / Sub API
    
    Avro 1.12.2 only maps events to classes that are trusted. A subscription 
with
    pubSubDeserializeType=POJO therefore fails with a SecurityException unless 
the
    pubSubPojoClass is trusted via quarkus.avro.trusted-classes or
    quarkus.avro.trusted-packages. The class is an endpoint option, so the
    extension cannot determine it at build time.
    
    Document what needs to be trusted for each pubSubDeserializeType in the
    Salesforce extension docs and in the 3.40.0 migration guide.
    
    The Pub / Sub API tests only ran with Salesforce credentials, so the failure
    went unnoticed. Test subscribing with each pubSubDeserializeType against a 
mock
    Pub / Sub API gRPC server when no credentials are provided.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../modules/ROOT/pages/migration-guide/3.40.0.adoc |  26 +++-
 .../pages/reference/extensions/salesforce.adoc     |  52 ++++++++
 .../salesforce/runtime/src/main/doc/usage.adoc     |  51 ++++++++
 integration-tests/salesforce/README.adoc           |   2 +
 .../component/salesforce/SalesforceResource.java   |   8 ++
 .../component/salesforce/SalesforceRoutes.java     |  60 ++++-----
 .../src/main/resources/application.properties      |   2 +
 .../component/salesforce/MockPubSubService.java    | 134 +++++++++++++++++++++
 .../component/salesforce/SalesforcePubSubIT.java   |  26 ++++
 .../component/salesforce/SalesforcePubSubTest.java |  88 ++++++++++++++
 .../salesforce/SalesforceTestResource.java         |  40 +++++-
 11 files changed, 458 insertions(+), 31 deletions(-)

diff --git a/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc 
b/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
index 49d5fc0a8c..33daeb4aaf 100644
--- a/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
+++ b/docs/modules/ROOT/pages/migration-guide/3.40.0.adoc
@@ -265,7 +265,7 @@ java.lang.SecurityException: Forbidden 
com.acme.orders.avro.Order! This class is
 
 Classes annotated with `@org.apache.avro.specific.AvroGenerated` are trusted 
automatically, which covers everything produced by the `generate-code` goal of 
`quarkus-maven-plugin` from `src/main/avro`. Nothing needs to be done for those.
 
-Specific records that predate that annotation, which Avro added in 1.9.0, or 
that were written by hand, have to be trusted explicitly. Prefer the Quarkus 
configuration properties, which are resolved at build time and so apply in both 
JVM and native mode:
+Specific records without that annotation, such as those written by hand, have 
to be trusted explicitly. Prefer the Quarkus configuration properties, which 
are resolved at build time and so apply in both JVM and native mode:
 
 [source,properties]
 ----
@@ -276,3 +276,27 @@ quarkus.avro.trusted-classes=com.acme.LegacyOrder
 Avro's own `org.apache.avro.SERIALIZABLE_PACKAGES` and 
`org.apache.avro.SERIALIZABLE_CLASSES` system properties still work as an 
addendum, since Quarkus adds its trusted classes on top of them rather than 
replacing them. They are read once, when Avro initializes, so they have to be 
set on the command line. Note that `*` in 
`org.apache.avro.SERIALIZABLE_PACKAGES` disables the class loading protection 
entirely.
 
 Whichever route is used, list only the packages holding your own Avro model 
classes.
+
+== Salesforce extension changes
+
+=== Pub / Sub API event classes must be trusted by Avro
+
+The Salesforce Pub / Sub API encodes events with Avro, so the Avro change 
described above also applies to routes that use `pubSubSubscribe` or 
`pubSubPublish`.
+
+The most likely case to be affected is a subscription with 
`pubSubDeserializeType=POJO`. The class configured on `pubSubPojoClass` is a 
plain class, so it is never trusted automatically and each event now fails with:
+
+[source,text]
+----
+java.lang.SecurityException: Forbidden org.foo.TestEvent! This class is not 
trusted to be included in Avro schemas.
+----
+
+Trust the class, or its package, in `application.properties`.
+
+[source,properties]
+----
+quarkus.avro.trusted-classes=org.foo.TestEvent
+----
+
+Subscriptions with the default `AVRO` type, or with `SPECIFIC_RECORD`, map 
events to the Avro `SpecificRecord` classes in the `com.sforce.eventbus` 
package. Those are trusted automatically if they are annotated with 
`@org.apache.avro.specific.AvroGenerated`, and only need to be trusted 
explicitly if they lack that annotation, for example because they were written 
by hand. The `GENERIC_RECORD` and `JSON` types are unaffected.
+
+Refer to the 
xref:reference/extensions/salesforce.adoc#extensions-salesforce-usage-trusting-classes-used-with-the-pub-sub-api[Salesforce
 extension documentation] for details.
diff --git a/docs/modules/ROOT/pages/reference/extensions/salesforce.adoc 
b/docs/modules/ROOT/pages/reference/extensions/salesforce.adoc
index b386fe2393..9951815e60 100644
--- a/docs/modules/ROOT/pages/reference/extensions/salesforce.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/salesforce.adoc
@@ -84,6 +84,58 @@ To generate Salesforce DTOs for your project, use the 
`salesforce-maven-plugin`.
 </plugin>
 ----
 
+[id="extensions-salesforce-usage-trusting-classes-used-with-the-pub-sub-api"]
+=== Trusting classes used with the Pub / Sub API
+
+The Pub / Sub API exchanges events encoded with Avro. Since Avro 1.12.2, 
classes that events are mapped to must be explicitly trusted, otherwise the 
event is rejected with:
+
+[source,text]
+----
+java.lang.SecurityException: Forbidden org.foo.TestEvent! This class is not 
trusted to be included in Avro schemas.
+----
+
+Whether anything needs to be trusted depends on the `pubSubDeserializeType` of 
the subscribing endpoint.
+
+[cols="1,3"]
+|===
+|`pubSubDeserializeType` |Classes to trust
+
+|`AVRO` (default), `SPECIFIC_RECORD`
+|The Avro `SpecificRecord` classes in the `com.sforce.eventbus` package that 
events are mapped to. Classes annotated with 
`@org.apache.avro.specific.AvroGenerated` are trusted automatically, which 
covers classes generated by the Avro compiler. Only classes without that 
annotation, such as those written by hand, need to be trusted explicitly.
+
+|`POJO`
+|The class configured on the `pubSubPojoClass` option. This is always 
required, as it is a plain class that Avro maps to reflectively.
+
+|`GENERIC_RECORD`, `JSON`
+|None.
+|===
+
+For example, given the following route.
+
+[source,java]
+----
+from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=POJO&pubSubPojoClass=org.foo.TestEvent")
+    .log("Received Salesforce POJO topic message: ${body}");
+----
+
+Class `org.foo.TestEvent` would need to be trusted in `application.properties`.
+
+[source,properties]
+----
+quarkus.avro.trusted-classes=org.foo.TestEvent
+----
+
+Alternatively, trust every class in a package and its sub-packages. List only 
the packages holding your own event classes.
+
+[source,properties]
+----
+quarkus.avro.trusted-packages=org.foo
+----
+
+Publishing with `pubSubPublish` does not require the message body class to be 
trusted. However, if an Avro `SpecificRecord` class exists for the topic in the 
`com.sforce.eventbus` package, then publishing a POJO body requires that class 
to be trusted in the same way as for subscribing.
+
+Refer to the 
xref:reference/extensions/avro.adoc#extensions-avro-configuration-trusted-classes[Avro
 extension documentation] for more information.
+
 
[id="extensions-salesforce-usage-native-mode-support-for-pub-sub-api-with-pojo-pubsubdeserializetype"]
 === Native mode support for Pub / Sub API with POJO `pubSubDeserializeType`
 
diff --git a/extensions/salesforce/runtime/src/main/doc/usage.adoc 
b/extensions/salesforce/runtime/src/main/doc/usage.adoc
index b6b9becd5b..ca4b0e9589 100644
--- a/extensions/salesforce/runtime/src/main/doc/usage.adoc
+++ b/extensions/salesforce/runtime/src/main/doc/usage.adoc
@@ -34,6 +34,57 @@ To generate Salesforce DTOs for your project, use the 
`salesforce-maven-plugin`.
 </plugin>
 ----
 
+=== Trusting classes used with the Pub / Sub API
+
+The Pub / Sub API exchanges events encoded with Avro. Since Avro 1.12.2, 
classes that events are mapped to must be explicitly trusted, otherwise the 
event is rejected with:
+
+[source,text]
+----
+java.lang.SecurityException: Forbidden org.foo.TestEvent! This class is not 
trusted to be included in Avro schemas.
+----
+
+Whether anything needs to be trusted depends on the `pubSubDeserializeType` of 
the subscribing endpoint.
+
+[cols="1,3"]
+|===
+|`pubSubDeserializeType` |Classes to trust
+
+|`AVRO` (default), `SPECIFIC_RECORD`
+|The Avro `SpecificRecord` classes in the `com.sforce.eventbus` package that 
events are mapped to. Classes annotated with 
`@org.apache.avro.specific.AvroGenerated` are trusted automatically, which 
covers classes generated by the Avro compiler. Only classes without that 
annotation, such as those written by hand, need to be trusted explicitly.
+
+|`POJO`
+|The class configured on the `pubSubPojoClass` option. This is always 
required, as it is a plain class that Avro maps to reflectively.
+
+|`GENERIC_RECORD`, `JSON`
+|None.
+|===
+
+For example, given the following route.
+
+[source,java]
+----
+from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=POJO&pubSubPojoClass=org.foo.TestEvent")
+    .log("Received Salesforce POJO topic message: ${body}");
+----
+
+Class `org.foo.TestEvent` would need to be trusted in `application.properties`.
+
+[source,properties]
+----
+quarkus.avro.trusted-classes=org.foo.TestEvent
+----
+
+Alternatively, trust every class in a package and its sub-packages. List only 
the packages holding your own event classes.
+
+[source,properties]
+----
+quarkus.avro.trusted-packages=org.foo
+----
+
+Publishing with `pubSubPublish` does not require the message body class to be 
trusted. However, if an Avro `SpecificRecord` class exists for the topic in the 
`com.sforce.eventbus` package, then publishing a POJO body requires that class 
to be trusted in the same way as for subscribing.
+
+Refer to the 
xref:reference/extensions/avro.adoc#extensions-avro-configuration-trusted-classes[Avro
 extension documentation] for more information.
+
 === Native mode support for Pub / Sub API with POJO `pubSubDeserializeType`
 
 When using the Camel Salesforce Pub / Sub API and `pubSubDeserializeType` is 
configured as `POJO`, you must register any classes configured on the 
`pubSubPojoClass` option for reflection.
diff --git a/integration-tests/salesforce/README.adoc 
b/integration-tests/salesforce/README.adoc
index df31dfe5fc..362adffc17 100644
--- a/integration-tests/salesforce/README.adoc
+++ b/integration-tests/salesforce/README.adoc
@@ -4,6 +4,8 @@ By default, the Salesforce integration tests use WireMock to 
stub the API intera
 
 IMPORTANT: Note that when using Wiremock, the Change Data Capture Events test, 
and the Platform event test are disabled.
 
+The Pub / Sub API uses gRPC, which WireMock cannot stub. A mock Pub / Sub API 
server is started alongside WireMock instead, and `SalesforcePubSubTest` tests 
subscribing to events with each `pubSubDeserializeType`. Publishing events is 
not covered, since the Pub / Sub API client can only publish over TLS. When 
Salesforce credentials are provided, the mock server is not started, 
`SalesforcePubSubTest` is disabled, and the Pub / Sub API is tested against 
Salesforce by `SalesforceIntegrationTest`.
+
 To run `camel-quarkus-salesforce` integration tests using Salesforce API 
interactions, you must first create a Salesforce developer account 
https://developer.salesforce.com/.
 
 Next create a new 'Connected App' by visiting Apps  -> App Manager page. In 
the form, enable OAuth Settings, and set those fields.
diff --git 
a/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceResource.java
 
b/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceResource.java
index 9b35ca899e..f2624e56c6 100644
--- 
a/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceResource.java
+++ 
b/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceResource.java
@@ -46,6 +46,7 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.ConsumerTemplate;
 import org.apache.camel.Exchange;
 import org.apache.camel.FluentProducerTemplate;
+import org.apache.camel.component.salesforce.PubSubApiConsumer;
 import org.apache.camel.component.salesforce.SalesforceEndpointConfig;
 import org.apache.camel.component.salesforce.api.dto.Attributes;
 import org.apache.camel.component.salesforce.api.dto.CreateSObjectResult;
@@ -66,6 +67,7 @@ import 
org.apache.camel.quarkus.component.salesforce.generated.QueryRecordsAccou
 import 
org.apache.camel.quarkus.component.salesforce.model.GlobalObjectsAndHeaders;
 import org.apache.camel.quarkus.component.salesforce.model.TestEventPojo;
 import org.apache.camel.spi.RouteController;
+import org.eclipse.microprofile.config.ConfigProvider;
 
 @Path("/salesforce")
 public class SalesforceResource {
@@ -172,6 +174,12 @@ public class SalesforceResource {
             throws Exception {
         RouteController controller = context.getRouteController();
         if (action.equals("start")) {
+            // Set by the test resource when the mock Pub / Sub API server is 
used, as it does not support TLS
+            if (context.getRoute(routeId).getConsumer() instanceof 
PubSubApiConsumer consumer
+                    && 
ConfigProvider.getConfig().getOptionalValue("salesforce.pubsub.use-plain-text", 
Boolean.class)
+                            .orElse(false)) {
+                consumer.setUsePlainTextConnection(true);
+            }
             controller.startRoute(routeId);
         } else if (action.equals("stop")) {
             controller.stopRoute(routeId);
diff --git 
a/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceRoutes.java
 
b/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceRoutes.java
index 60ea50c2b7..cd31488339 100644
--- 
a/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceRoutes.java
+++ 
b/integration-tests/salesforce/src/main/java/org/apache/camel/quarkus/component/salesforce/SalesforceRoutes.java
@@ -102,35 +102,37 @@ public class SalesforceRoutes extends RouteBuilder {
             from("direct:pubSubPublish")
                     .log("Sent Salesforce topic message: ${body}")
                     .to("salesforce:pubSubPublish:/event/TestEvent__e");
-
-            // AVRO deserialize type (also analogous to SPECIFIC_RECORD)
-            from("salesforce:pubSubSubscribe:/event/TestEvent__e")
-                    .routeId("topicSubscribeAvro")
-                    .autoStartup(false)
-                    .log("Received Salesforce AVRO topic message: ${body}")
-                    .to("seda:pubSubSubscribeAvro");
-
-            // GENERIC_RECORD deserialize type
-            
from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=GENERIC_RECORD")
-                    .routeId("topicSubscribeGenericRecord")
-                    .autoStartup(false)
-                    .log("Received Salesforce GENERIC_RECORD topic message: 
${body}")
-                    .to("seda:pubSubSubscribeGenericRecord");
-
-            // JSON deserialize type
-            
from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=JSON")
-                    .routeId("topicSubscribeJson")
-                    .autoStartup(false)
-                    .log("Received Salesforce JSON topic message: ${body}")
-                    .to("seda:pubSubSubscribeJson");
-
-            // POJO deserialize type
-            
from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=POJO&pubSubPojoClass="
-                    + TestEventPojo.class.getName())
-                    .routeId("topicSubscribePojo")
-                    .autoStartup(false)
-                    .log("Received Salesforce POJO topic message: ${body}")
-                    .to("seda:pubSubSubscribePojo");
         }
+
+        // The Pub / Sub API subscribers work with real credentials, or a mock 
Pub / Sub API server
+
+        // AVRO deserialize type (also analogous to SPECIFIC_RECORD)
+        from("salesforce:pubSubSubscribe:/event/TestEvent__e")
+                .routeId("topicSubscribeAvro")
+                .autoStartup(false)
+                .log("Received Salesforce AVRO topic message: ${body}")
+                .to("seda:pubSubSubscribeAvro");
+
+        // GENERIC_RECORD deserialize type
+        
from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=GENERIC_RECORD")
+                .routeId("topicSubscribeGenericRecord")
+                .autoStartup(false)
+                .log("Received Salesforce GENERIC_RECORD topic message: 
${body}")
+                .to("seda:pubSubSubscribeGenericRecord");
+
+        // JSON deserialize type
+        
from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=JSON")
+                .routeId("topicSubscribeJson")
+                .autoStartup(false)
+                .log("Received Salesforce JSON topic message: ${body}")
+                .to("seda:pubSubSubscribeJson");
+
+        // POJO deserialize type
+        
from("salesforce:pubSubSubscribe:/event/TestEvent__e?pubSubDeserializeType=POJO&pubSubPojoClass="
+                + TestEventPojo.class.getName())
+                .routeId("topicSubscribePojo")
+                .autoStartup(false)
+                .log("Received Salesforce POJO topic message: ${body}")
+                .to("seda:pubSubSubscribePojo");
     }
 }
diff --git 
a/integration-tests/salesforce/src/main/resources/application.properties 
b/integration-tests/salesforce/src/main/resources/application.properties
index f85470aa24..b01a803795 100644
--- a/integration-tests/salesforce/src/main/resources/application.properties
+++ b/integration-tests/salesforce/src/main/resources/application.properties
@@ -20,3 +20,5 @@
 #
 camel.component.salesforce.loginUrl = https://login.salesforce.com
 
+# The class used with pubSubDeserializeType=POJO must be trusted by Avro
+quarkus.avro.trusted-classes = 
org.apache.camel.quarkus.component.salesforce.model.TestEventPojo
diff --git 
a/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/MockPubSubService.java
 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/MockPubSubService.java
new file mode 100644
index 0000000000..d6e0885b09
--- /dev/null
+++ 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/MockPubSubService.java
@@ -0,0 +1,134 @@
+/*
+ * 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.quarkus.component.salesforce;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import com.google.protobuf.ByteString;
+import com.salesforce.eventbus.protobuf.ConsumerEvent;
+import com.salesforce.eventbus.protobuf.FetchRequest;
+import com.salesforce.eventbus.protobuf.FetchResponse;
+import com.salesforce.eventbus.protobuf.ProducerEvent;
+import com.salesforce.eventbus.protobuf.PubSubGrpc;
+import com.salesforce.eventbus.protobuf.SchemaInfo;
+import com.salesforce.eventbus.protobuf.SchemaRequest;
+import com.salesforce.eventbus.protobuf.TopicInfo;
+import com.salesforce.eventbus.protobuf.TopicRequest;
+import io.grpc.stub.StreamObserver;
+import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericDatumWriter;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.GenericRecordBuilder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.EncoderFactory;
+
+/**
+ * A minimal stand-in for the Salesforce Pub / Sub API, which delivers a 
single TestEvent__e event to each subscriber.
+ */
+public class MockPubSubService extends PubSubGrpc.PubSubImplBase {
+    public static final String CREATED_BY = "0055g00000AbCdEfGH";
+    public static final long CREATED_DATE = 1760000000000L;
+    public static final String TEST_FIELD_VALUE = "Camel Quarkus Pub / Sub 
test";
+
+    private static final String SCHEMA_ID = "test-event-schema";
+    // Matches the schema of the generated com.sforce.eventbus.TestEvent__e
+    private static final String SCHEMA_JSON = """
+            {
+              "type": "record",
+              "name": "TestEvent__e",
+              "namespace": "com.sforce.eventbus",
+              "fields": [
+                {"name": "CreatedDate", "type": "long", "doc": 
"CreatedDate:DateTime"},
+                {"name": "CreatedById", "type": "string", "doc": 
"CreatedBy:EntityId"},
+                {"name": "Test_Field__c", "type": ["null", "string"], "doc": 
"Data:Text", "default": null}
+              ]
+            }
+            """;
+
+    @Override
+    public void getTopic(TopicRequest request, StreamObserver<TopicInfo> 
responseObserver) {
+        responseObserver.onNext(TopicInfo.newBuilder()
+                .setTopicName(request.getTopicName())
+                .setCanPublish(true)
+                .setCanSubscribe(true)
+                .setSchemaId(SCHEMA_ID)
+                .build());
+        responseObserver.onCompleted();
+    }
+
+    @Override
+    public void getSchema(SchemaRequest request, StreamObserver<SchemaInfo> 
responseObserver) {
+        responseObserver.onNext(SchemaInfo.newBuilder()
+                .setSchemaId(SCHEMA_ID)
+                .setSchemaJson(SCHEMA_JSON)
+                .build());
+        responseObserver.onCompleted();
+    }
+
+    @Override
+    public StreamObserver<FetchRequest> 
subscribe(StreamObserver<FetchResponse> responseObserver) {
+        return new StreamObserver<>() {
+            @Override
+            public void onNext(FetchRequest request) {
+                ByteString replayId = ByteString.copyFromUtf8("1");
+                ConsumerEvent event = ConsumerEvent.newBuilder()
+                        .setReplayId(replayId)
+                        .setEvent(ProducerEvent.newBuilder()
+                                .setId("test-event")
+                                .setSchemaId(SCHEMA_ID)
+                                
.setPayload(ByteString.copyFrom(createPayload())))
+                        .build();
+
+                // Report the remaining requested events as pending, so that 
the client does not ask for more
+                responseObserver.onNext(FetchResponse.newBuilder()
+                        .addEvents(event)
+                        .setLatestReplayId(replayId)
+                        .setPendingNumRequested(request.getNumRequested())
+                        .build());
+            }
+
+            @Override
+            public void onError(Throwable t) {
+                // Noop
+            }
+
+            @Override
+            public void onCompleted() {
+                responseObserver.onCompleted();
+            }
+        };
+    }
+
+    private static byte[] createPayload() {
+        Schema schema = new Schema.Parser().parse(SCHEMA_JSON);
+        GenericRecord record = new GenericRecordBuilder(schema)
+                .set("CreatedDate", CREATED_DATE)
+                .set("CreatedById", CREATED_BY)
+                .set("Test_Field__c", TEST_FIELD_VALUE)
+                .build();
+
+        try {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            BinaryEncoder encoder = 
EncoderFactory.get().directBinaryEncoder(out, null);
+            new GenericDatumWriter<GenericRecord>(schema).write(record, 
encoder);
+            return out.toByteArray();
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git 
a/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforcePubSubIT.java
 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforcePubSubIT.java
new file mode 100644
index 0000000000..deeb5aee55
--- /dev/null
+++ 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforcePubSubIT.java
@@ -0,0 +1,26 @@
+/*
+ * 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.quarkus.component.salesforce;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import org.junit.jupiter.api.condition.EnabledIf;
+
+@EnabledIf("org.apache.camel.quarkus.component.salesforce.SalesforceTestResource#isMockBackend")
+@QuarkusIntegrationTest
+class SalesforcePubSubIT extends SalesforcePubSubTest {
+
+}
diff --git 
a/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforcePubSubTest.java
 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforcePubSubTest.java
new file mode 100644
index 0000000000..e824b7b499
--- /dev/null
+++ 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforcePubSubTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.quarkus.component.salesforce;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.response.ValidatableResponse;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
+
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests the Pub / Sub API subscriber against {@link MockPubSubService}. When 
Salesforce credentials are provided,
+ * the Pub / Sub API is tested against Salesforce in {@link 
SalesforceIntegrationTest} instead.
+ */
+@EnabledIf("org.apache.camel.quarkus.component.salesforce.SalesforceTestResource#isMockBackend")
+@QuarkusTest
+@QuarkusTestResource(SalesforceTestResource.class)
+class SalesforcePubSubTest {
+
+    @Test
+    void testPubSubAvro() {
+        subscribe("topicSubscribeAvro", "/salesforce/subscribe/event/avro")
+                .body(
+                        "createdBy", is(MockPubSubService.CREATED_BY),
+                        "createdDate", is(MockPubSubService.CREATED_DATE),
+                        "testFieldValue", 
is(MockPubSubService.TEST_FIELD_VALUE));
+    }
+
+    @Test
+    void testPubSubGenericRecord() {
+        subscribe("topicSubscribeGenericRecord", 
"/salesforce/subscribe/event/generic/record")
+                .body(
+                        "createdBy", is(MockPubSubService.CREATED_BY),
+                        "createdDate", 
is(String.valueOf(MockPubSubService.CREATED_DATE)),
+                        "testFieldValue", 
is(MockPubSubService.TEST_FIELD_VALUE));
+    }
+
+    @Test
+    void testPubSubJson() {
+        subscribe("topicSubscribeJson", "/salesforce/subscribe/event/json")
+                .body(
+                        "CreatedById", is(MockPubSubService.CREATED_BY),
+                        "CreatedDate", is(MockPubSubService.CREATED_DATE),
+                        "Test_Field__c", 
is(MockPubSubService.TEST_FIELD_VALUE));
+    }
+
+    @Test
+    void testPubSubPojo() {
+        subscribe("topicSubscribePojo", "/salesforce/subscribe/event/pojo")
+                .body(
+                        "createdBy", is(MockPubSubService.CREATED_BY),
+                        "createdDate", is(MockPubSubService.CREATED_DATE),
+                        "testFieldValue", 
is(MockPubSubService.TEST_FIELD_VALUE));
+    }
+
+    private static ValidatableResponse subscribe(String routeId, String path) {
+        try {
+            RestAssured.post("/salesforce/route/" + routeId + "/start")
+                    .then()
+                    .statusCode(200);
+
+            return RestAssured.get(path)
+                    .then()
+                    .statusCode(200);
+        } finally {
+            RestAssured.post("/salesforce/route/" + routeId + "/stop")
+                    .then()
+                    .statusCode(200);
+        }
+    }
+}
diff --git 
a/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforceTestResource.java
 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforceTestResource.java
index afa81b420e..4e40613c82 100644
--- 
a/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforceTestResource.java
+++ 
b/integration-tests/salesforce/src/test/java/org/apache/camel/quarkus/component/salesforce/SalesforceTestResource.java
@@ -16,8 +16,12 @@
  */
 package org.apache.camel.quarkus.component.salesforce;
 
+import java.io.IOException;
 import java.util.Map;
+import java.util.stream.Stream;
 
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
 import 
org.apache.camel.quarkus.test.wiremock.WireMockTestResourceLifecycleManager;
 
 public class SalesforceTestResource extends 
WireMockTestResourceLifecycleManager {
@@ -27,6 +31,16 @@ public class SalesforceTestResource extends 
WireMockTestResourceLifecycleManager
     private static final String SALESFORCE_USERNAME = "SALESFORCE_USERNAME";
     private static final String SALESFORCE_PASSWORD = "SALESFORCE_PASSWORD";
 
+    private Server pubSubServer;
+
+    /**
+     * Whether the tests run against mocked Salesforce APIs, which is the case 
unless Salesforce credentials are provided
+     */
+    public static boolean isMockBackend() {
+        return Stream.of(SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET, 
SALESFORCE_USERNAME, SALESFORCE_PASSWORD)
+                .anyMatch(envVar -> System.getenv(envVar) == null);
+    }
+
     @Override
     protected String getRecordTargetBaseUrl() {
         return SALESFORCE_BASE_URL;
@@ -34,7 +48,7 @@ public class SalesforceTestResource extends 
WireMockTestResourceLifecycleManager
 
     @Override
     protected boolean isMockingEnabled() {
-        return !envVarsPresent(SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET, 
SALESFORCE_USERNAME, SALESFORCE_PASSWORD);
+        return isMockBackend();
     }
 
     @Override
@@ -46,6 +60,30 @@ public class SalesforceTestResource extends 
WireMockTestResourceLifecycleManager
             options.put("camel.component.salesforce.password", "password");
             options.put("camel.component.salesforce.clientId", "clientId");
         }
+
+        // The Pub / Sub API uses gRPC, which WireMock cannot stub
+        if (isMockingEnabled()) {
+            try {
+                pubSubServer = ServerBuilder.forPort(0)
+                        .addService(new MockPubSubService())
+                        .build()
+                        .start();
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to start the mock Pub / Sub 
API server", e);
+            }
+            options.put("camel.component.salesforce.pubSubHost", "localhost");
+            options.put("camel.component.salesforce.pubSubPort", 
String.valueOf(pubSubServer.getPort()));
+            // The mock Pub / Sub API server does not support TLS
+            options.put("salesforce.pubsub.use-plain-text", "true");
+        }
         return options;
     }
+
+    @Override
+    public void stop() {
+        if (pubSubServer != null) {
+            pubSubServer.shutdownNow();
+        }
+        super.stop();
+    }
 }

Reply via email to