diff --git a/pom.xml b/pom.xml
index 4caca2ef..47b30405 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
UTF-8
+
https://testendpoint
admin@org.com
standard@org.com
@@ -177,6 +178,19 @@
5.18.0
test
+
+
+ org.seleniumhq.selenium
+ selenium-java
+ 4.15.0
+ test
+
+
+ org.seleniumhq.selenium
+ selenium-chrome-driver
+ 4.15.0
+ test
+
@@ -607,6 +621,11 @@
verify
!${skip-unit-tests}
+
+ ${test.endpoint}
+ ${test.user.default}
+ ${test.password}
+
**/action/**,
**/dao/**,
diff --git a/src/main/java/com/salesforce/dataloader/ui/URLUtil.java b/src/main/java/com/salesforce/dataloader/ui/URLUtil.java
index 4f4debfb..7bdc8a51 100644
--- a/src/main/java/com/salesforce/dataloader/ui/URLUtil.java
+++ b/src/main/java/com/salesforce/dataloader/ui/URLUtil.java
@@ -34,7 +34,36 @@
public class URLUtil {
private static Logger logger = DLLogManager.getLogger(URLUtil.class);
+ private static volatile UrlOpener testHook = null;
+
+ /**
+ * Set a test hook for URL opening (used by tests to inject Selenium).
+ * @param opener The UrlOpener implementation to use for tests
+ */
+ public static void setTestHook(UrlOpener opener) {
+ testHook = opener;
+ }
+
+ /**
+ * Clear the test hook to restore normal URL opening behavior.
+ */
+ public static void clearTestHook() {
+ testHook = null;
+ }
+
public static void openURL(String url) {
+ // Check for test hook first
+ if (testHook != null) {
+ try {
+ logger.debug("Using test hook for URL opening: " + url);
+ testHook.open(url);
+ return;
+ } catch (Exception e) {
+ logger.warn("Test hook failed, falling back to default behavior: " + e.getMessage());
+ }
+ }
+
+ // Default production behavior
if (Desktop.isDesktopSupported()) {
Desktop desktop = Desktop.getDesktop();
try {
diff --git a/src/main/java/com/salesforce/dataloader/ui/UrlOpener.java b/src/main/java/com/salesforce/dataloader/ui/UrlOpener.java
new file mode 100644
index 00000000..93df14a6
--- /dev/null
+++ b/src/main/java/com/salesforce/dataloader/ui/UrlOpener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2015, salesforce.com, inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.salesforce.dataloader.ui;
+
+/**
+ * Interface for opening URLs, allowing test seams for browser automation.
+ * Production uses Desktop.browse(), tests can inject Selenium WebDriver.
+ */
+public interface UrlOpener {
+ /**
+ * Opens the specified URL.
+ * @param url The URL to open
+ * @throws Exception if the URL cannot be opened
+ */
+ void open(String url) throws Exception;
+}
\ No newline at end of file
diff --git a/src/test/java/com/salesforce/dataloader/oauth/OAuthTestSeamSeleniumTest.java b/src/test/java/com/salesforce/dataloader/oauth/OAuthTestSeamSeleniumTest.java
new file mode 100644
index 00000000..de82a4f7
--- /dev/null
+++ b/src/test/java/com/salesforce/dataloader/oauth/OAuthTestSeamSeleniumTest.java
@@ -0,0 +1,457 @@
+/*
+ * Copyright (c) 2015, salesforce.com, inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.salesforce.dataloader.oauth;
+
+import com.salesforce.dataloader.ConfigTestBase;
+import com.salesforce.dataloader.config.AppConfig;
+import com.salesforce.dataloader.ui.URLUtil;
+import com.salesforce.dataloader.ui.SeleniumUrlOpener;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.TimeoutException;
+
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+import org.junit.Assert;
+
+/**
+ * Clean OAuth test that uses the test seam pattern to inject Selenium into handleOAuthLogin().
+ * Tests the complete OAuth flow: PKCE timeout -> Device Flow -> Login -> Allow -> Continue
+ */
+public class OAuthTestSeamSeleniumTest extends ConfigTestBase {
+
+ private WebDriver driver;
+ private WebDriverWait wait;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setupController();
+
+ System.out.println("๐ง Setting up OAuth test with Selenium...");
+
+ // Setup Selenium
+ ChromeOptions options = new ChromeOptions();
+ options.addArguments("--disable-web-security");
+ options.addArguments("--disable-features=VizDisplayCompositor");
+ options.addArguments("--no-sandbox");
+
+ driver = new ChromeDriver(options);
+ wait = new WebDriverWait(driver, Duration.ofSeconds(30));
+
+ // Inject Selenium as the URL opener using test seam
+ SeleniumUrlOpener seleniumOpener = new SeleniumUrlOpener(driver, false);
+ URLUtil.setTestHook(seleniumOpener);
+ System.out.println("โ
Test seam configured - OAuth will use Selenium browser");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ try {
+ URLUtil.clearTestHook();
+
+ // Clear OAuth tokens
+ AppConfig config = getController().getAppConfig();
+ if (config != null) {
+ config.setValue(AppConfig.PROP_OAUTH_ACCESSTOKEN, "");
+ config.setValue(AppConfig.PROP_OAUTH_REFRESHTOKEN, "");
+ config.setValue(AppConfig.PROP_OAUTH_INSTANCE_URL, "");
+ }
+
+ if (getController() != null) {
+ getController().logout();
+ }
+
+ if (driver != null) {
+ Thread.sleep(2000);
+ driver.quit();
+ }
+
+ } catch (Exception e) {
+ System.err.println("โ ๏ธ Warning during teardown: " + e.getMessage());
+ }
+ }
+
+ @Test
+ public void testHandleOAuthLoginWithPKCEFlow() throws Exception {
+ System.out.println("๐งช Testing handleOAuthLogin() with automated Selenium flow");
+
+ // Start handleOAuthLogin() in background
+ CompletableFuture handleOAuthFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ System.out.println("๐ Starting handleOAuthLogin()...");
+
+ OAuthFlowHandler oauthHandler = new OAuthFlowHandler(
+ getController().getAppConfig(),
+ (status) -> System.out.println("OAuth: " + status),
+ null, // null controller to avoid SWT issues (we are not testing dataloader ui layer; only oauth flow in browser)
+ null // null runnable to avoid SWT issues (we are not testing dataloader ui layer; only oauth flow in browser)
+ );
+
+ boolean result = oauthHandler.handleOAuthLogin();
+ System.out.println("๐ฏ handleOAuthLogin() result: " + result);
+ return result;
+
+ } catch (Exception e) {
+ System.err.println("โ handleOAuthLogin() failed: " + e.getMessage());
+ return false;
+ }
+ });
+
+ System.out.println("โณ Waiting for OAuth flow to navigate browser...");
+ Thread.sleep(3000);
+
+ // Handle the OAuth flow with Selenium
+ try {
+ String currentUrl = driver.getCurrentUrl();
+ System.out.println("๐ Current URL: " + currentUrl);
+
+ if (currentUrl.contains("salesforce") || currentUrl.contains("orgfarm")) {
+ System.out.println("โ
Selenium navigated to OAuth URL");
+
+ // Handle login if needed
+ String pageSource = driver.getPageSource();
+ if (pageSource.contains("name=\"username\"") || pageSource.contains("name=\"pw\"")) {
+ System.out.println("๐ Performing login...");
+ performAutomatedLogin();
+ Thread.sleep(2000);
+
+ // Assert we're no longer on login page
+ String postLoginUrl = driver.getCurrentUrl();
+ assertFalse("Should not be on login page after login",
+ postLoginUrl.contains("/login"));
+ }
+
+ // Handle authorization if needed
+ pageSource = driver.getPageSource();
+ if (pageSource.contains("Allow") || pageSource.contains("Authorize")) {
+ System.out.println("๐ฑ๏ธ Clicking authorization...");
+ handleAuthorizationPage();
+ Thread.sleep(3000);
+
+ // Assert we moved past authorization page
+ String postAuthUrl = driver.getCurrentUrl();
+ assertTrue("Should be redirected after authorization",
+ !postAuthUrl.equals(currentUrl));
+ }
+
+ currentUrl = driver.getCurrentUrl();
+ if (currentUrl.contains("localhost:")) {
+ System.out.println("๐ OAuth callback completed");
+
+ // Assert we're on localhost callback
+ assertTrue("Should be on localhost callback URL",
+ currentUrl.contains("localhost:"));
+
+ // Use WebDriverWait to handle page loading and success verification
+ try {
+ WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
+ wait.until(ExpectedConditions.or(
+ ExpectedConditions.titleContains("Success"),
+ ExpectedConditions.urlContains("success"),
+ ExpectedConditions.presenceOfElementLocated(By.xpath("//*[contains(text(), 'Authorization Successful')]")),
+ ExpectedConditions.presenceOfElementLocated(By.xpath("//*[contains(text(), 'authorization successful')]")),
+ ExpectedConditions.presenceOfElementLocated(By.xpath("//*[contains(text(), 'SUCCESS')]"))
+ ));
+ System.out.println("โ
Authorization success page verified");
+ } catch (org.openqa.selenium.TimeoutException e) {
+ System.out.println("โ ๏ธ Success page elements not found within timeout, checking page source...");
+ try {
+ String pageContent = driver.getPageSource();
+ if (pageContent.contains("Authorization Successful!") ||
+ pageContent.contains("authorization successful") ||
+ pageContent.contains("SUCCESS")) {
+ System.out.println("โ
Authorization success verified in page source");
+ } else {
+ System.out.println("โ ๏ธ Authorization success message not found, but OAuth tokens obtained successfully");
+ }
+ } catch (Exception pageSourceException) {
+ System.out.println("โ ๏ธ Could not verify success page, but OAuth flow completed successfully");
+ }
+ }
+ }
+
+ } else {
+ Assert.fail("OAuth URL not detected: " + currentUrl);
+ }
+
+ } catch (Exception e) {
+ Assert.fail("Selenium error during OAuth flow: " + e.getMessage());
+ }
+
+ // Wait for handleOAuthLogin() to complete
+ try {
+ boolean result = handleOAuthFuture.get(30, TimeUnit.SECONDS);
+
+ if (result) {
+ System.out.println("๐ SUCCESS: OAuth login completed");
+
+ // Verify tokens were set
+ AppConfig config = getController().getAppConfig();
+ String accessToken = config.getString(AppConfig.PROP_OAUTH_ACCESSTOKEN);
+ String instanceUrl = config.getString(AppConfig.PROP_OAUTH_INSTANCE_URL);
+
+ System.out.println("๐ Tokens: " +
+ (accessToken != null && !accessToken.isEmpty() ? "โ
Access " : "โ Access ") +
+ (instanceUrl != null && !instanceUrl.isEmpty() ? "โ
Instance" : "โ Instance"));
+
+ assertTrue("handleOAuthLogin() should return true", result);
+ assertNotNull("Access token should be set", accessToken);
+ assertFalse("Access token should not be empty", accessToken.trim().isEmpty());
+
+ } else {
+ Assert.fail("handleOAuthLogin() returned false - OAuth flow failed");
+ }
+
+ } catch (Exception e) {
+ Assert.fail("handleOAuthLogin() timed out or failed: " + e.getMessage());
+ }
+
+ System.out.println("โ
Test completed");
+ }
+
+ /**
+ * Clean OAuth flow test: PKCE timeout -> Device Flow -> Login -> Allow -> Continue
+ * Refactored based on actual execution path analysis.
+ */
+ @Test
+ public void testHandleOAuthLoginWithDeviceFlow() throws Exception {
+ System.out.println("๐งช Starting clean OAuth flow test...");
+ CompletableFuture oauthFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ OAuthFlowHandler oauthHandler = new OAuthFlowHandler(
+ getController().getAppConfig(),
+ (status) -> System.out.println("OAuth: " + status),
+ null, // null controller to avoid SWT issues (we are not testing dataloader ui layer; only oauth flow in browser)
+ null // null runnable to avoid SWT issues (we are not testing dataloader ui layer; only oauth flow in browser)
+ );
+ return oauthHandler.handleOAuthLogin();
+ } catch (Exception e) {
+ System.err.println("OAuth flow error: " + e.getMessage());
+ return false;
+ }
+ });
+
+ // Step 1: Wait for OAuth flow to determine and navigate to Device Flow
+ System.out.println("โณ Step 1: Waiting for OAuth pre-flight checks and Device Flow navigation...");
+ Thread.sleep(3000);
+
+ // Verify we're on Device Flow page
+ String currentUrl = driver.getCurrentUrl();
+ assertTrue("Should be on Device Flow page", currentUrl.contains("setup/connect"));
+ System.out.println("โ
Step 1 verified: On Device Flow page");
+
+ // Verify the page contains expected elements
+ String pageSource = driver.getPageSource();
+ assertTrue("Device Flow page should contain Connect button",
+ pageSource.contains("Connect") || pageSource.contains("Submit"));
+
+ // Step 2: Click Connect button (code is pre-filled)
+ System.out.println("๐ฑ Step 2: Clicking Connect button...");
+ WebElement connectButton = driver.findElement(By.xpath("//input[@type='submit' and (@value='Connect' or @value='Submit')]"));
+ assertNotNull("Connect button should be found", connectButton);
+ assertTrue("Connect button should be enabled", connectButton.isEnabled());
+ connectButton.click();
+ Thread.sleep(2000);
+
+ // Verify we were redirected from Device Flow page
+ String postConnectUrl = driver.getCurrentUrl();
+ assertNotEquals("URL should change after clicking Connect", currentUrl, postConnectUrl);
+ System.out.println("โ
Step 2 verified: Redirected after Connect click");
+
+ // Step 3: Perform login (redirected to login page)
+ System.out.println("๐ Step 3: Performing login...");
+
+ // Verify we're on login page
+ String loginPageSource = driver.getPageSource();
+ assertTrue("Should be on login page",
+ loginPageSource.contains("name=\"username\"") && loginPageSource.contains("name=\"pw\""));
+
+ performAutomatedLogin();
+
+ // Verify login was successful (no longer on login page)
+ String postLoginUrl = driver.getCurrentUrl();
+ assertFalse("Should not be on login page after successful login",
+ postLoginUrl.contains("/login"));
+ System.out.println("โ
Step 3 verified: Login successful");
+
+ // Step 4: Click Allow button on authorization page
+ System.out.println("โ
Step 4: Clicking Allow button...");
+ Thread.sleep(2000); // Wait for authorization page to load
+
+ // Verify we're on authorization page
+ String authPageSource = driver.getPageSource();
+ assertTrue("Should be on authorization page with Allow button",
+ authPageSource.contains("Allow"));
+
+ WebElement allowButton = driver.findElement(By.xpath("//input[normalize-space(@value)='Allow']"));
+ assertNotNull("Allow button should be found", allowButton);
+ assertTrue("Allow button should be enabled", allowButton.isEnabled());
+ allowButton.click();
+ Thread.sleep(3000);
+
+ // Verify we reached success page
+ String successUrl = driver.getCurrentUrl();
+ assertTrue("Should reach success page after Allow", successUrl.contains("user_approved=1"));
+ System.out.println("โ
Step 4 verified: Authorization successful");
+
+ // Step 5: Click Continue button to complete flow
+ System.out.println("โก๏ธ Step 5: Clicking Continue button...");
+
+ // Verify Continue button exists
+ String successPageSource = driver.getPageSource();
+ assertTrue("Success page should contain Continue button",
+ successPageSource.contains("Continue"));
+
+ WebElement continueButton = driver.findElement(By.xpath("//input[@value='Continue']"));
+ assertNotNull("Continue button should be found", continueButton);
+ assertTrue("Continue button should be enabled", continueButton.isEnabled());
+ continueButton.click();
+
+ System.out.println("๐ Clean OAuth flow test PASSED!");
+ URLUtil.clearTestHook();
+ }
+
+ /**
+ * Test to verify that DataLoader fails when PKCE is disabled and only device flow is enabled.
+ * This represents the scenario before 7 AM on 9/2 when Connected Apps only support device flow.
+ * DL version 64.0.1 only supports PKCE, so this should fail gracefully.
+ */
+ @Test
+ public void testHandleOAuthLoginWithPKCEDisabled() throws Exception {
+ System.out.println("๐งช Testing DataLoader behavior when PKCE is disabled (device flow only)...");
+
+ // Start OAuth flow in background
+ CompletableFuture oauthFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ OAuthFlowHandler oauthHandler = new OAuthFlowHandler(
+ getController().getAppConfig(),
+ (status) -> System.out.println("OAuth Status: " + status),
+ null, // null controller to avoid SWT issues
+ null // null runnable to avoid SWT issues
+ );
+ return oauthHandler.handleOAuthLogin();
+ } catch (Exception e) {
+ return false;
+ }
+ });
+
+ // Wait for OAuth flow to complete (timeout is 60 seconds)
+ System.out.println("โณ Waiting for OAuth flow to complete...");
+ Boolean oauthResult;
+ try {
+ oauthResult = oauthFuture.get(65, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ oauthResult = false;
+ }
+
+ // Analyze the OAuth attempt
+ String currentUrl = driver.getCurrentUrl();
+ System.out.println("๐ OAuth result: " + oauthResult + ", URL: " + currentUrl);
+
+ // Check what type of OAuth flow was attempted
+ boolean pkceAttempted = currentUrl.contains("oauth2/authorize") &&
+ (currentUrl.contains("code_challenge") || currentUrl.contains("error="));
+ boolean deviceFlowAttempted = currentUrl.contains("setup/connect");
+
+ // Main assertions
+ assertTrue("PKCE authorization should have been attempted", pkceAttempted);
+ assertFalse("Device flow should NOT have been attempted", deviceFlowAttempted);
+ assertFalse("OAuth should fail when PKCE is disabled", oauthResult);
+
+ System.out.println("โ
TEST PASSED: DataLoader attempted PKCE (not device flow) and failed as expected");
+ }
+
+ /**
+ * Handle authorization page by clicking Allow/Authorize button
+ */
+ private void handleAuthorizationPage() throws Exception {
+ try {
+ WebElement allowButton = wait.until(ExpectedConditions.elementToBeClickable(
+ By.xpath("//*[contains(text(), 'Allow') or contains(text(), 'Authorize')]")));
+ allowButton.click();
+ } catch (Exception e) {
+ System.out.println("โ ๏ธ Authorization button not found: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Perform automated login using credentials from pom.xml system properties
+ */
+ private void performAutomatedLogin() throws Exception {
+ String username = System.getProperty("test.user.default");
+ String password = System.getProperty("test.password");
+
+ // Assert credentials are available
+ assertNotNull("Username should be provided via system property", username);
+ assertNotNull("Password should be provided via system property", password);
+ assertFalse("Username should not be empty", username.trim().isEmpty());
+ assertFalse("Password should not be empty", password.trim().isEmpty());
+
+ System.out.println("๐ Using credentials from pom.xml: " + username);
+
+ try {
+ WebElement usernameField = wait.until(ExpectedConditions.presenceOfElementLocated(By.name("username")));
+ assertNotNull("Username field should be found", usernameField);
+ assertTrue("Username field should be enabled", usernameField.isEnabled());
+ usernameField.clear();
+ usernameField.sendKeys(username);
+
+ WebElement passwordField = driver.findElement(By.name("pw"));
+ assertNotNull("Password field should be found", passwordField);
+ assertTrue("Password field should be enabled", passwordField.isEnabled());
+ passwordField.clear();
+ passwordField.sendKeys(password);
+
+ WebElement loginButton = driver.findElement(By.name("Login"));
+ assertNotNull("Login button should be found", loginButton);
+ assertTrue("Login button should be enabled", loginButton.isEnabled());
+ loginButton.click();
+
+ // Wait for login to complete (no longer on login page)
+ wait.until(ExpectedConditions.not(ExpectedConditions.urlContains("/login")));
+
+ // Verify we're no longer on login page
+ String currentUrl = driver.getCurrentUrl();
+ assertFalse("Should not be on login page after successful login",
+ currentUrl.contains("/login"));
+
+ } catch (Exception e) {
+ Assert.fail("Login failed: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/salesforce/dataloader/ui/SeleniumUrlOpener.java b/src/test/java/com/salesforce/dataloader/ui/SeleniumUrlOpener.java
new file mode 100644
index 00000000..9a770bdf
--- /dev/null
+++ b/src/test/java/com/salesforce/dataloader/ui/SeleniumUrlOpener.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2015, salesforce.com, inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided
+ * that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the
+ * following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
+ * promote products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.salesforce.dataloader.ui;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+
+/**
+ * Test implementation of UrlOpener that uses Selenium WebDriver.
+ * This allows tests to control browser navigation instead of opening system browser.
+ */
+public final class SeleniumUrlOpener implements UrlOpener {
+ private final WebDriver driver;
+ private final boolean openInNewTab;
+
+ public SeleniumUrlOpener(WebDriver driver) {
+ this(driver, false);
+ }
+
+ public SeleniumUrlOpener(WebDriver driver, boolean openInNewTab) {
+ this.driver = driver;
+ this.openInNewTab = openInNewTab;
+ }
+
+ @Override
+ public void open(String url) throws Exception {
+ System.out.println("๐ฏ SeleniumUrlOpener: Navigating to " + url);
+
+ if (openInNewTab) {
+ // Open in new tab and switch to it
+ ((JavascriptExecutor) driver).executeScript("window.open(arguments[0], '_blank');", url);
+
+ // Switch to the new tab
+ String originalWindow = driver.getWindowHandle();
+ for (String windowHandle : driver.getWindowHandles()) {
+ if (!windowHandle.equals(originalWindow)) {
+ driver.switchTo().window(windowHandle);
+ break;
+ }
+ }
+ System.out.println("๐ฏ SeleniumUrlOpener: Opened in new tab and switched");
+ } else {
+ // Open in current tab
+ driver.get(url);
+ System.out.println("๐ฏ SeleniumUrlOpener: Opened in current tab");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties
index d894aaa1..898f1616 100644
--- a/src/test/resources/test.properties
+++ b/src/test/resources/test.properties
@@ -24,6 +24,19 @@ main.src.dir=${project.build.sourceDirectory}
target.dir=${project.build.directory}
process.encryptionKeyFile=${test.encryptionFile}
+
+## OAuth client IDs for testing
+## For OAuth rollout testing, uncomment and modify manually or via script and input consumer key for connected app -
+#sfdc.oauth.Production.bulk.clientid=
+#sfdc.oauth.Production.partner.clientid=
+
+## Bulk API configuration - controls which Connected App is used
+## DataLoaderPartnerUI (default): sfdc.useBulkApi=false, sfdc.useBulkV2Api=false
+## DataLoaderBulkUI: sfdc.useBulkApi=true OR sfdc.useBulkV2Api=true
+## Uncomment and modify via script for testing -
+#sfdc.useBulkApi=
+#sfdc.useBulkV2Api=
+
## TODO: properties below here don't really belong and should be removed
## defaults that don't necessarily belong here
test.account.extid=Oracle_Id__c