From ec913bbcfddb82f34c55a1bb14d4ce0f3048671d Mon Sep 17 00:00:00 2001 From: Torsten Mielke Date: Mon, 31 Aug 2026 20:00:15 +0200 Subject: [PATCH] CAMEL-24495: camel-salesforce-maven-plugin - support JWT and Client Credentials authentication The Maven plugin previously only supported Username-Password authentication, hardcoding the grant type in the codegen layer. With Salesforce retiring the USERNAME_PASSWORD grant type in Winter '27 (February 2027), this adds support for JWT and Client Credentials flows. Changes: - Add authenticationType parameter to explicitly select the auth type - Refactor codegen login config to use no-arg constructor + setters, enabling auto-detection from provided credentials - Make userName optional (not needed for Client Credentials) - Add Mojo-level validation for ambiguous credential combinations - Add manual integration tests for all three authentication types - Refactor AbstractSalesforceMojoTest into a utility class holding only static setup helpers; move login tests into CamelSalesforceLoginManualIT to separate test infrastructure from test logic - Update README.md with per-auth-type requirements and examples Note: without the ambiguity validation, omitting the password while providing clientSecret and userName would silently auto-detect Client Credentials instead of failing for Username-Password. The Mojo now rejects this combination early with an actionable error message. (cherry picked from commit b4164ad87cc9e8e837dad30fe4b2425365940012) The upgrade-guide entry from main's camel-4x-upgrade-guide-4_23.adoc is dropped here: that file doesn't exist on this branch, and per this project's convention upgrade-guide entries for a backported change belong in the matching camel-4x-upgrade-guide-4_XX.adoc file on main (a follow-up there covering the 4.22.x line), not duplicated onto the maintenance branch itself. Co-authored-by: Claude Opus 4.6 --- .../codegen/AbstractSalesforceExecution.java | 28 ++- .../camel-salesforce-maven-plugin/README.md | 182 +++++++++++------- .../camel/maven/AbstractSalesforceMojo.java | 32 +-- .../maven/AbstractSalesforceMojoTest.java | 103 +++------- .../maven/CamelSalesforceLoginManualIT.java | 162 ++++++++++++++++ .../maven/CamelSalesforceMojoManualIT.java | 4 +- .../maven/GeneratePubSubMojoManualIT.java | 4 +- .../maven/SalesforceMojoValidationTest.java | 153 +++++++++++++++ .../camel/maven/SchemaMojoManualIT.java | 4 +- 9 files changed, 507 insertions(+), 165 deletions(-) create mode 100644 components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceLoginManualIT.java create mode 100644 components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SalesforceMojoValidationTest.java diff --git a/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java b/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java index 043e09e5a35f0..4be104abe72e4 100644 --- a/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java +++ b/components/camel-salesforce/camel-salesforce-codegen/src/main/java/org/apache/camel/component/salesforce/codegen/AbstractSalesforceExecution.java @@ -25,6 +25,7 @@ import java.util.concurrent.ExecutorService; import org.apache.camel.CamelContext; +import org.apache.camel.component.salesforce.AuthenticationType; import org.apache.camel.component.salesforce.SalesforceHttpClient; import org.apache.camel.component.salesforce.SalesforceLoginConfig; import org.apache.camel.component.salesforce.api.SalesforceException; @@ -157,6 +158,11 @@ public abstract class AbstractSalesforceExecution { */ String userName; + /** + * Salesforce authentication type. + */ + AuthenticationType authenticationType; + /** * Salesforce API version. */ @@ -321,14 +327,16 @@ private SalesforceHttpClient createHttpClient() throws Exception { } private SalesforceLoginConfig getSalesforceLoginSession() { - if (keyStoreParameters != null) { - SalesforceLoginConfig salesforceLoginConfig - = new SalesforceLoginConfig(loginUrl, clientId, userName, keyStoreParameters, false); - salesforceLoginConfig.setJwtAudience(jwtAudience); - - return salesforceLoginConfig; - } - return new SalesforceLoginConfig(loginUrl, clientId, clientSecret, userName, password, false); + SalesforceLoginConfig config = new SalesforceLoginConfig(); + config.setLoginUrl(loginUrl); + config.setClientId(clientId); + config.setClientSecret(clientSecret); + config.setUserName(userName); + config.setPassword(password); + config.setKeystore(keyStoreParameters); + config.setJwtAudience(jwtAudience); + config.setType(authenticationType); + return config; } private void disconnectFromSalesforce(final RestClient restClient) { @@ -416,6 +424,10 @@ public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters) { this.keyStoreParameters = keyStoreParameters; } + public void setAuthenticationType(AuthenticationType authenticationType) { + this.authenticationType = authenticationType; + } + public void setUserName(String userName) { this.userName = userName; } diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/README.md b/components/camel-salesforce/camel-salesforce-maven-plugin/README.md index d06a2c1ea8a57..131cd7f431f2c 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/README.md +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/README.md @@ -4,7 +4,7 @@ This plugin generates DTOs for use with the [Camel Salesforce Component](https:/ ## Usage ## -This plugin provides three maven goals: +This plugin provides three Maven goals: * The `generate` goal generates DTOs for use with the REST API. * The `generatePubSub` goal generates Apache Avro `SpecificRecord` subclasses for use with the PubSub API. @@ -12,29 +12,30 @@ This plugin provides three maven goals: The plugin configuration has the following properties. -* clientId - Salesforce client Id for Remote API access -* clientSecret - Salesforce client secret for Remote API access -* userName - Salesforce account username -* password - Salesforce account password (including secret token) -* jwtAudience - Salesforce JWT audience (defaults to "https://login.salesforce.com") -* keystoreResource - Path to keystore file for JWT authentication -* keystorePassword - Password for keystore file -* keystoreType - Type of keystore file (defaults to "JKS") -* loginUrl - Salesforce loginUrl (defaults to "https://login.salesforce.com") -* version - Salesforce Rest API version, defaults to 25.0 -* outputDirectory - Directory where to place generated DTOs, defaults to ${project.build.directory}/generated-sources/camel-salesforce -* includes - List of SObject types to include -* topics - List of topics to include, .e.g., `/event/BatchApexErrorEvent`. This property only applies to the `generatePubSub` goal. -* excludes - List of SObject types to exclude -* includePattern - Java RegEx for SObject types to include -* excludePattern - Java RegEx for SObject types to exclude -* packageName - Java package name for generated DTOs, defaults to org.apache.camel.salesforce.dto. -* customTypes - override default types in generated DTOs -* useStringsForPicklists - Use strings instead of enumerations for picklists. Default is false. -* childRelationshipNameSuffix - Suffix for child relationship property name. Necessary if an SObject +* `clientId` - Salesforce client Id for Remote API access +* `clientSecret` - Salesforce client secret for Remote API access +* `userName` - Salesforce account username (required for Username-Password and JWT flows) +* `password` - Salesforce account password (including secret token) +* `authenticationType` - Salesforce authentication type: USERNAME_PASSWORD, JWT, or CLIENT_CREDENTIALS. If not specified, auto-detected from provided credentials. +* `jwtAudience` - Salesforce JWT audience (defaults to "https://login.salesforce.com") +* `keystoreResource` - Path to keystore file for JWT authentication +* `keystorePassword` - Password for keystore file +* `keystoreType` - Type of keystore file (defaults to "JKS") +* `loginUrl` - Salesforce loginUrl (defaults to "https://login.salesforce.com") +* `version` - Salesforce Rest API version, defaults to 25.0 +* `outputDirectory` - Directory where to place generated DTOs, defaults to ${project.build.directory}/generated-sources/camel-salesforce +* `includes` - List of SObject types to include +* `topics` - List of topics to include, e.g., `/event/BatchApexErrorEvent`. This property only applies to the `generatePubSub` goal. +* `excludes` - List of SObject types to exclude +* `includePattern` - Java RegEx for SObject types to include +* `excludePattern` - Java RegEx for SObject types to exclude +* `packageName` - Java package name for generated DTOs, defaults to org.apache.camel.salesforce.dto. +* `customTypes` - override default types in generated DTOs +* `useStringsForPicklists` - Use strings instead of enumerations for picklists. Default is false. +* `childRelationshipNameSuffix` - Suffix for child relationship property name. Necessary if an SObject has a lookup field with the same name as its Child Relationship Name. If setting to something other than default, "List" is a sensible value. -* enumerationOverrideProperties - Override picklist enum value generation via a java.util.Properties instance. +* `enumerationOverrideProperties` - Override picklist enum value generation via a java.util.Properties instance. Property name format: `SObject.FieldName.PicklistValue`. Property value is the desired enum value. E.g.: ``` @@ -47,57 +48,63 @@ Property name format: `SObject.FieldName.PicklistValue`. Property value is the d Additional properties to provide proxy information, if behind a firewall. -* httpProxyHost -* httpProxyPort -* httpProxyUsername -* httpProxyPassword -* httpProxyRealm -* httpProxyAuthUri -* httpProxyUseDigestAuth -* httpProxyIncludedAddresses -* httpProxyExcludedAddresses +* `httpProxyHost` +* `httpProxyPort` +* `httpProxyUsername` +* `httpProxyPassword` +* `httpProxyRealm` +* `httpProxyAuthUri` +* `httpProxyUseDigestAuth` +* `httpProxyIncludedAddresses` +* `httpProxyExcludedAddresses` -There are two authentication methods supported by the plugin: Username-Password and JWT. -The plugin will use the Username-Password method if the `clientSecret` is specified and will use the JWT method if the `keystoreResource` is specified. +Three authentication methods are supported by the plugin: Username-Password, JWT, and Client Credentials. +The plugin auto-detects the authentication method from the provided credentials, or you can set `authenticationType` explicitly. -Sample pom.xml using Username-Password authentication: +* **Username-Password** requires: `clientId`, `clientSecret`, `userName`, and `password`.
+ Auto-detected when `password` is specified. +* **JWT** requires: `clientId`, `userName`, `loginUrl` (My Domain URL), `keystoreResource`, and `keystorePassword`.
+ `keystoreType` defaults to JKS, `jwtAudience` defaults to `https://login.salesforce.com`.
+ Auto-detected when `keystoreResource` is specified. +* **Client Credentials** requires: `clientId`, `clientSecret`, and `loginUrl` (My Domain URL).
+ Auto-detected when only `clientId` and `clientSecret` are specified (no `password`, no `userName`, no `keystoreResource`). + +___ +
+ +### Username-Password Authentication Type ### + +Sample pom.xml using **Username-Password** authentication: ``` - - 5MVG9uudbyLbNPZOFutIHJpIb2nchnCiNE_NqeYcewMCPPT8_6VV_LQF_CJ813456GxzhxZdxlGwbYI_yzHmz + 5MVG9uudbyLbNPZOFut...GwbYI_yzHmz 5630289243049151316 foo@bar.com foopasswordCbe5V27JxD0JXYFGJIdIEWB7p - - https://myDomain.my.salesforce.com - + https://myDomain.my.salesforce.com foo.bar.com 8090 - - ... - ... ... - - + org.apache.camel.maven camel-salesforce-maven-plugin - 2.17.1 + ${camel.version} ${camelSalesforce.clientId} ${camelSalesforce.clientSecret} ${camelSalesforce.userName} ${camelSalesforce.password} - ${camelSalesforce.loginUrl} + ${camelSalesforce.loginUrl} Account Contacts @@ -106,7 +113,6 @@ Sample pom.xml using Username-Password authentication: ${camelSalesforce.httpProxyPort} - @@ -117,49 +123,46 @@ The plugin should be configured for the rest of the properties, and can be execu mvn camel-salesforce:generate -DcamelSalesforce.clientId= -DcamelSalesforce.clientSecret= -DcamelSalesforce.userName= -DcamelSalesforce.password= -Sample pom.xml using JWT authentication: +___ +
+ +### JWT Authentication Type ### + +Sample pom.xml using **JWT** authentication: ``` - - 5MVG9uudbyLbNPZOFutIHJpIb2nchnCiNE_NqeYcewMCPPT8_6VV_LQF_CJ813456GxzhxZdxlGwbYI_yzHmz + 5MVG9uudbyLbNPZOFut...GwbYI_yzHmz foo@bar.com src/main/resources/salesforce.jks foopasswordCbe5V27JxD0JXYFGJIdIEWB7p JKS - https://login.salesforce.com - - https://myDomain.my.salesforce.com - + https://myDomain.my.salesforce.com foo.bar.com 8090 - - ... - ... ... - - + org.apache.camel.maven camel-salesforce-maven-plugin - 2.17.1 + ${camel.version} ${camelSalesforce.clientId} ${camelSalesforce.userName} ${camelSalesforce.keystore.resource} ${camelSalesforce.keystore.password} - ${camelSalesforce.keystore.type} - ${camelSalesforce.jwtAudience} - ${camelSalesforce.loginUrl} + ${camelSalesforce.keystore.type} + ${camelSalesforce.jwtAudience} + ${camelSalesforce.loginUrl} Account Contacts @@ -168,7 +171,6 @@ Sample pom.xml using JWT authentication: ${camelSalesforce.httpProxyPort} - @@ -177,9 +179,57 @@ Sample pom.xml using JWT authentication: For obvious security reasons it is recommended that the clientId, userName, keystoreResource, keystorePassword, keystoreType and jwtAudience fields be not set in the pom.xml. The plugin should be configured for the rest of the properties, and can be executed using the following command: - mvn camel-salesforce:generate -DcamelSalesforce.clientId= -DcamelSalesforce.userName= -DcamelSalesforce.keystore.resource= -DcamelSalesforce.keystore.password= -DcamelSalesforce.keystore.type= -DcamelSalesforce.jwtAudience= + mvn camel-salesforce:generate -DcamelSalesforce.clientId= -DcamelSalesforce.userName= -DcamelSalesforce.keystore.resource= -DcamelSalesforce.keystore.password= -DcamelSalesforce.keystore.type= -DcamelSalesforce.jwtAudience= -DcamelSalesforce.loginUrl= + +___ +
+ +### Client Credentials Authentication Type ### + +Sample pom.xml using **Client Credentials** authentication +``` + + + + + 5MVG9uudbyLbNPZOFut...GwbYI_yzHmz + 5630289243049151316 + https://myDomain.my.salesforce.com + + ... + + ... + + ... + + + org.apache.camel.maven + camel-salesforce-maven-plugin + ${camel.version} + + ${camelSalesforce.clientId} + ${camelSalesforce.clientSecret} + ${camelSalesforce.loginUrl} + + Account + Contacts + + + + + + + +``` +For obvious security reasons it is recommended that the clientId and clientSecret fields be not set in the pom.xml. +The plugin should be configured for the rest of the properties, and can be executed using the following command: + + mvn camel-salesforce:generate -DcamelSalesforce.clientId= -DcamelSalesforce.clientSecret= -DcamelSalesforce.loginUrl= +___ + The generated DTOs use Jackson. All Salesforce field types are supported. Date and time fields are mapped to java.time.ZonedDateTime, and picklist fields are mapped to generated Java Enumerations. Relationship fields, e.g. `Contact.Account`, will be strongly typed if the referenced SObject type is listed in `includes`. Otherwise, the type of the reference object will be `AbstractDescribedSObjectBase`. Some useful but non-obvious SObjects to include are `RecordType`, `User`, `Group`, and `Name`. @@ -205,4 +255,4 @@ You can customize types, i.e. use java.time.LocalDateTime instead of the default -```` +``` diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java index 575ff03ca865d..78a65bc147df4 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/main/java/org/apache/camel/maven/AbstractSalesforceMojo.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; +import org.apache.camel.component.salesforce.AuthenticationType; import org.apache.camel.component.salesforce.SalesforceEndpointConfig; import org.apache.camel.component.salesforce.SalesforceLoginConfig; import org.apache.camel.component.salesforce.codegen.AbstractSalesforceExecution; @@ -142,11 +143,18 @@ public abstract class AbstractSalesforceMojo extends AbstractMojo { final SSLContextParameters sslContextParameters = new SSLContextParameters(); /** - * Salesforce username. + * Salesforce username. Required for USERNAME_PASSWORD and JWT authentication types. */ - @Parameter(property = "camelSalesforce.userName", required = true) + @Parameter(property = "camelSalesforce.userName") String userName; + /** + * Salesforce authentication type. If not specified, auto-detected from provided credentials. Supported values: + * USERNAME_PASSWORD, JWT, CLIENT_CREDENTIALS. + */ + @Parameter(property = "camelSalesforce.authenticationType") + AuthenticationType authenticationType; + /** * Salesforce JWT Audience. */ @@ -213,6 +221,7 @@ protected void setup() { execution.setLoginUrl(loginUrl); execution.setUserName(userName); execution.setPassword(password); + execution.setAuthenticationType(authenticationType); execution.setVersion(version); execution.setSslContextParameters(sslContextParameters); execution.setJwtAudience(jwtAudience); @@ -225,15 +234,7 @@ private void validateAuthenticationParameters() throws MojoExecutionException { "Either property: clientSecret or property: keystoreResource must be provided."); } else if (clientSecret != null && keystoreResource != null) { throw new MojoExecutionException( - "Property: clientSecret or property: keystoreResource must be provided."); - } - - if (clientSecret != null) { - if (password == null) { - throw new MojoExecutionException( - // NOTE: a text error message to clarify the problem - "Property 'password' must be provided when property 'clientSecret' was provided."); // NOSONAR - } + "Only one of clientSecret or keystoreResource may be provided, not both."); } if (keystoreResource != null) { @@ -243,6 +244,15 @@ private void validateAuthenticationParameters() throws MojoExecutionException { "Property 'keystorePassword' must be provided when property 'keystoreResource' was provided."); // NOSONAR } } + + if (authenticationType == null && clientSecret != null && userName != null && password == null + && keystoreResource == null) { + throw new MojoExecutionException( + "Ambiguous authentication configuration: 'userName' and 'clientSecret' are set but 'password' is missing. " + + "For Username-Password authentication, provide the 'password' property. " + + "For Client Credentials authentication, remove the 'userName' property " + + "or set 'authenticationType' to CLIENT_CREDENTIALS explicitly."); + } } private KeyStoreParameters generateKeyStoreParameters() { diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java index f0f177af315a1..98f2bdffb99e5 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/AbstractSalesforceMojoTest.java @@ -20,88 +20,18 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Properties; +import org.apache.camel.component.salesforce.AuthenticationType; import org.apache.camel.component.salesforce.SalesforceEndpointConfig; -import org.apache.camel.component.salesforce.codegen.AbstractSalesforceExecution; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public abstract class AbstractSalesforceMojoTest { - private static final Map> NO_HEADERS = Collections.emptyMap(); + static final String TEST_LOGIN_PROPERTIES = "../test-salesforce-login.properties"; - private static final String TEST_LOGIN_PROPERTIES = "../test-salesforce-login.properties"; - - @Test - public void shouldLoginAndProvideRestClient() throws IOException, MojoExecutionException, MojoFailureException { - final AbstractSalesforceMojo mojo = new AbstractSalesforceMojo() { - final Logger logger = LoggerFactory.getLogger(AbstractSalesforceExecution.class.getName()); - - @Override - protected AbstractSalesforceExecution getSalesforceExecution() { - return new AbstractSalesforceExecution() { - @Override - protected void executeWithClient() { - assertThat(getRestClient()).isNotNull(); - - getRestClient().getGlobalObjects(NO_HEADERS, (response, headers, exception) -> { - assertThat(exception).isNull(); - }); - } - - @Override - protected Logger getLog() { - return logger; - } - }; - } - }; - - setup(mojo); - - mojo.execute(); - } - - @Test - public void shouldLoginWithJwtAndProvideRestClient() throws IOException, MojoExecutionException, MojoFailureException { - final AbstractSalesforceMojo mojo = new AbstractSalesforceMojo() { - final Logger logger = LoggerFactory.getLogger(AbstractSalesforceExecution.class.getName()); - - @Override - protected AbstractSalesforceExecution getSalesforceExecution() { - return new AbstractSalesforceExecution() { - @Override - protected void executeWithClient() { - assertThat(getRestClient()).isNotNull(); - - getRestClient().getGlobalObjects(NO_HEADERS, (response, headers, exception) -> { - assertThat(exception).isNull(); - }); - } - - @Override - protected Logger getLog() { - return logger; - } - }; - } - }; - - setupJwt(mojo); - - mojo.execute(); - } - - static void setup(final AbstractSalesforceMojo mojo) throws IOException { + static void setupUsernamePassword(final AbstractSalesforceMojo mojo) throws IOException { // load test-salesforce-login properties try (final InputStream stream = new FileInputStream(TEST_LOGIN_PROPERTIES)) { final Properties properties = new Properties(); @@ -110,6 +40,9 @@ static void setup(final AbstractSalesforceMojo mojo) throws IOException { mojo.clientSecret = properties.getProperty("salesforce.client.secret"); mojo.userName = properties.getProperty("salesforce.username"); mojo.password = properties.getProperty("salesforce.password"); + assumeTrue(mojo.password != null && !mojo.password.isEmpty(), + "Property 'salesforce.password' must be set in " + TEST_LOGIN_PROPERTIES + + " for USERNAME_PASSWORD authentication test"); mojo.loginUrl = properties.getProperty("salesforce.login.url"); mojo.version = SalesforceEndpointConfig.DEFAULT_VERSION; } catch (final FileNotFoundException e) { @@ -147,4 +80,26 @@ static void setupJwt(final AbstractSalesforceMojo mojo) throws IOException { throw exception; } } + + static void setupClientCredentials(final AbstractSalesforceMojo mojo) throws IOException { + // load test-salesforce-login properties + try (final InputStream stream = new FileInputStream(TEST_LOGIN_PROPERTIES)) { + final Properties properties = new Properties(); + properties.load(stream); + mojo.clientId = properties.getProperty("salesforce.client.id"); + mojo.clientSecret = properties.getProperty("salesforce.client.secret"); + mojo.authenticationType = AuthenticationType.CLIENT_CREDENTIALS; + mojo.loginUrl = properties.getProperty("salesforce.login.url"); + mojo.version = SalesforceEndpointConfig.DEFAULT_VERSION; + } catch (final FileNotFoundException e) { + final FileNotFoundException exception + = new FileNotFoundException( + "Create a properties file named " + TEST_LOGIN_PROPERTIES + + " with clientId, clientSecret" + + " for a Salesforce connected app configured for Client Credentials flow."); + exception.initCause(e); + + throw exception; + } + } } diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceLoginManualIT.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceLoginManualIT.java new file mode 100644 index 0000000000000..d324b5f63dd4d --- /dev/null +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceLoginManualIT.java @@ -0,0 +1,162 @@ +/* + * 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.maven; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.camel.component.salesforce.codegen.AbstractSalesforceExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.camel.maven.AbstractSalesforceMojoTest.setupClientCredentials; +import static org.apache.camel.maven.AbstractSalesforceMojoTest.setupJwt; +import static org.apache.camel.maven.AbstractSalesforceMojoTest.setupUsernamePassword; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test that verifies Salesforce login with all supported authentication types. The {@code ManualIT} suffix + * prevents automatic execution by Maven Surefire and Failsafe — run explicitly with: + * + *
+ * mvn test -Dtest=CamelSalesforceLoginManualIT
+ * 
+ * + * Requires a properties file at {@code ../test-salesforce-login.properties} with: + * + *
+ * # Required for USERNAME_PASSWORD test
+ * salesforce.client.id=...
+ * salesforce.client.secret=...
+ * salesforce.username=...
+ * salesforce.password=...
+ * salesforce.login.url=https://your-domain.my.salesforce.com
+ *
+ * # Required for CLIENT_CREDENTIALS test (uses client.id, client.secret, login.url from above)
+ *
+ * # Required for JWT test
+ * salesforce.keystore.resource=...
+ * salesforce.keystore.password=...
+ * salesforce.keystore.type=JKS
+ * 
+ */ +public class CamelSalesforceLoginManualIT { + + private static final Map> NO_HEADERS = Collections.emptyMap(); + + private static final Logger logger = LoggerFactory.getLogger(CamelSalesforceLoginManualIT.class.getName()); + + @Test + public void shouldLoginWithUsernamePasswordAndProvideRestClient() + throws IOException, MojoExecutionException, MojoFailureException { + logger.info("Testing shouldLoginWithUsernamePasswordAndProvideRestClient()"); + final AbstractSalesforceMojo mojo = new AbstractSalesforceMojo() { + final Logger logger = LoggerFactory.getLogger(AbstractSalesforceExecution.class.getName()); + + @Override + protected AbstractSalesforceExecution getSalesforceExecution() { + return new AbstractSalesforceExecution() { + @Override + protected void executeWithClient() { + assertThat(getRestClient()).isNotNull(); + + getRestClient().getGlobalObjects(NO_HEADERS, (response, headers, exception) -> { + assertThat(exception).isNull(); + }); + } + + @Override + protected Logger getLog() { + return logger; + } + }; + } + }; + + setupUsernamePassword(mojo); + + mojo.execute(); + } + + @Test + public void shouldLoginWithJwtAndProvideRestClient() throws IOException, MojoExecutionException, MojoFailureException { + logger.info("Testing shouldLoginWithJwtAndProvideRestClient()"); + final AbstractSalesforceMojo mojo = new AbstractSalesforceMojo() { + final Logger logger = LoggerFactory.getLogger(AbstractSalesforceExecution.class.getName()); + + @Override + protected AbstractSalesforceExecution getSalesforceExecution() { + return new AbstractSalesforceExecution() { + @Override + protected void executeWithClient() { + assertThat(getRestClient()).isNotNull(); + + getRestClient().getGlobalObjects(NO_HEADERS, (response, headers, exception) -> { + assertThat(exception).isNull(); + }); + } + + @Override + protected Logger getLog() { + return logger; + } + }; + } + }; + + setupJwt(mojo); + + mojo.execute(); + } + + @Test + public void shouldLoginWithClientCredentialsAndProvideRestClient() + throws IOException, MojoExecutionException, MojoFailureException { + logger.info("Testing shouldLoginWithClientCredentialsAndProvideRestClient()"); + final AbstractSalesforceMojo mojo = new AbstractSalesforceMojo() { + final Logger logger = LoggerFactory.getLogger(AbstractSalesforceExecution.class.getName()); + + @Override + protected AbstractSalesforceExecution getSalesforceExecution() { + return new AbstractSalesforceExecution() { + @Override + protected void executeWithClient() { + assertThat(getRestClient()).isNotNull(); + + getRestClient().getGlobalObjects(NO_HEADERS, (response, headers, exception) -> { + assertThat(exception).isNull(); + }); + } + + @Override + protected Logger getLog() { + return logger; + } + }; + } + }; + + setupClientCredentials(mojo); + + mojo.execute(); + } +} diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceMojoManualIT.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceMojoManualIT.java index 888a52f358af2..86e697820a9bc 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceMojoManualIT.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/CamelSalesforceMojoManualIT.java @@ -34,7 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import static org.apache.camel.maven.AbstractSalesforceMojoTest.setup; +import static org.apache.camel.maven.AbstractSalesforceMojoTest.setupUsernamePassword; import static org.assertj.core.api.Assertions.assertThat; public class CamelSalesforceMojoManualIT { @@ -71,7 +71,7 @@ GenerateMojo createMojo() throws IOException { final GenerateMojo mojo = new GenerateMojo(); // set login properties - setup(mojo); + setupUsernamePassword(mojo); // set defaults mojo.version = SalesforceEndpointConfig.DEFAULT_VERSION; diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/GeneratePubSubMojoManualIT.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/GeneratePubSubMojoManualIT.java index 19ae4db8c6b19..f82a36137111e 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/GeneratePubSubMojoManualIT.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/GeneratePubSubMojoManualIT.java @@ -35,7 +35,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import static org.apache.camel.maven.AbstractSalesforceMojoTest.setup; +import static org.apache.camel.maven.AbstractSalesforceMojoTest.setupUsernamePassword; import static org.assertj.core.api.Assertions.assertThat; public class GeneratePubSubMojoManualIT { @@ -74,7 +74,7 @@ GeneratePubSubMojo createMojo() throws IOException { final GeneratePubSubMojo mojo = new GeneratePubSubMojo(); // set login properties - setup(mojo); + setupUsernamePassword(mojo); // set additional properties specific to this Mojo try (final InputStream stream = new FileInputStream(TEST_LOGIN_PROPERTIES)) { diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SalesforceMojoValidationTest.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SalesforceMojoValidationTest.java new file mode 100644 index 0000000000000..c812365d5a6d1 --- /dev/null +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SalesforceMojoValidationTest.java @@ -0,0 +1,153 @@ +/* + * 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.maven; + +import org.apache.camel.component.salesforce.AuthenticationType; +import org.apache.camel.component.salesforce.codegen.AbstractSalesforceExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for authentication parameter validation in {@link AbstractSalesforceMojo}. These tests verify that + * {@code validateAuthenticationParameters()} rejects invalid credential combinations and accepts valid ones for all + * supported authentication types (USERNAME_PASSWORD, JWT, CLIENT_CREDENTIALS). + */ +public class SalesforceMojoValidationTest { + + private static final String VALIDATION_PASSED = "validation passed"; + + private AbstractSalesforceMojo createMojo() { + return new AbstractSalesforceMojo() { + @Override + protected AbstractSalesforceExecution getSalesforceExecution() { + throw new RuntimeException(VALIDATION_PASSED); + } + }; + } + + // --- Validation rejection tests --- + + // Validation must fail when no authentication credential (clientSecret or keystoreResource) is provided + @Test + void shouldRejectWhenNeitherClientSecretNorKeystoreProvided() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining("Either property: clientSecret or property: keystoreResource must be provided"); + } + + // clientSecret and keystoreResource are mutually exclusive — providing both must be rejected + @Test + void shouldRejectWhenBothClientSecretAndKeystoreProvided() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.clientSecret = "test-secret"; + mojo.keystoreResource = "/some/keystore.jks"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining("Only one of clientSecret or keystoreResource may be provided, not both"); + } + + // JWT authentication requires a keystore password to unlock the keystore + @Test + void shouldRejectKeystoreWithoutPassword() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.keystoreResource = "/some/keystore.jks"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining("keystorePassword' must be provided"); + } + + // When clientSecret and userName are set but password is missing, the configuration is ambiguous: + // it could be USERNAME_PASSWORD (missing password) or CLIENT_CREDENTIALS (stray userName). + // Validation must reject this unless authenticationType is set explicitly. + @Test + void shouldRejectAmbiguousCredentialsWithoutAuthenticationType() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.clientSecret = "test-secret"; + mojo.userName = "user@example.com"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining("Ambiguous authentication configuration"); + } + + // --- Validation acceptance tests (one per auth method) --- + + // USERNAME_PASSWORD: clientSecret + userName + password is a valid, unambiguous combination + @Test + void shouldAcceptUsernamePasswordCredentials() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.clientSecret = "test-secret"; + mojo.userName = "user@example.com"; + mojo.password = "test-password"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining(VALIDATION_PASSED); + } + + // JWT: keystoreResource + keystorePassword + userName is valid — no ambiguity since clientSecret is absent + @Test + void shouldAcceptJwtCredentials() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.keystoreResource = "/some/keystore.jks"; + mojo.keystorePassword = "test-password"; + mojo.userName = "user@example.com"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining(VALIDATION_PASSED); + } + + // CLIENT_CREDENTIALS (auto-detected): clientSecret without userName is unambiguously Client Credentials + @Test + void shouldAcceptClientCredentialsWithoutUserName() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.clientSecret = "test-secret"; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining(VALIDATION_PASSED); + } + + // CLIENT_CREDENTIALS (explicit): clientSecret + userName would normally be ambiguous, but setting + // authenticationType explicitly resolves it — validation must accept this + @Test + void shouldAcceptExplicitClientCredentialsWithUserName() { + AbstractSalesforceMojo mojo = createMojo(); + mojo.clientId = "test-client-id"; + mojo.clientSecret = "test-secret"; + mojo.userName = "user@example.com"; + mojo.authenticationType = AuthenticationType.CLIENT_CREDENTIALS; + + assertThatThrownBy(mojo::execute) + .isInstanceOf(MojoExecutionException.class) + .hasMessageContaining(VALIDATION_PASSED); + } +} diff --git a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SchemaMojoManualIT.java b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SchemaMojoManualIT.java index dc8b3fecba9b6..6bbcd7e406255 100644 --- a/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SchemaMojoManualIT.java +++ b/components/camel-salesforce/camel-salesforce-maven-plugin/src/test/java/org/apache/camel/maven/SchemaMojoManualIT.java @@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import static org.apache.camel.maven.AbstractSalesforceMojoTest.setup; +import static org.apache.camel.maven.AbstractSalesforceMojoTest.setupUsernamePassword; import static org.junit.jupiter.api.Assertions.assertTrue; public class SchemaMojoManualIT { @@ -37,7 +37,7 @@ public class SchemaMojoManualIT { @Test public void testExecuteJsonSchema() throws Exception { final SchemaMojo mojo = new SchemaMojo(); - setup(mojo); + setupUsernamePassword(mojo); mojo.includes = new String[] { "Account" }; mojo.outputDirectory = temp.toFile();