This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch 3.40.x in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 9d1df867376233a99c2b66610b1c3cc82b90bb2c 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 abf156a37b..a6e526ddd1 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] ---- @@ -278,3 +278,27 @@ Camel's upgrade guide additionally asks `camel-avro-rpc` users to trust the `org 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(); + } }
