From 6566c31dccbddf81b446bfb76fecdf958e322d6d Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 22 Jan 2026 15:56:44 -0800 Subject: [PATCH 1/3] Advanced authentication testing 1) test_config.json renamed as ui_test_config.json and TestConfig.swift renamed as UITestConfig.swift 2) ui_test_config.json has a loginHosts section, each loginHost has a name, url and a list of users - two login hosts are expected currently one for regular authentication and one for advanced authentication 3) UITestConfig.swift changed accordingly as well as tests and tests helpers 4) Existing tests all use regular auth org - new tests AdvancedAuthBeaconLoginTests use the advanced auth org 5) Had to tweak BaseAuthFlowTest.swift and LoginPageObject.swift to handle both regular authentication advanced authentication NB: I did not yet install any of the existing CAs or ECAs in the new org I created with advanced authentication turned on - for that reason, the only tests using advanced authentication are the ones using beacon apps (beacon apps auto-install in all orgs) --- .gitignore | 1 + .../AuthFlowTester.xcodeproj/project.pbxproj | 2 +- .../PageObjects/LoginPageObject.swift | 26 +++- .../Tests/AdvancedAuthBeaconLoginTests.swift | 43 ++++++ .../Tests/BaseAuthFlowTesterTest.swift | 72 ++++++--- .../Tests/BeaconLoginTests.swift | 26 +++- .../Tests/ECALoginTests.swift | 2 +- .../Tests/LegacyLoginTests.swift | 2 +- .../Tests/MigrationTests.swift | 21 ++- .../Tests/MultiUserLoginTests.swift | 26 +++- ...figUtils.swift => UITestConfigUtils.swift} | 138 ++++++++++-------- ...json.sample => ui_test_config.json.sample} | 71 +++++++-- 12 files changed, 320 insertions(+), 110 deletions(-) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift rename native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/{TestConfigUtils.swift => UITestConfigUtils.swift} (69%) rename native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/{test_config.json.sample => ui_test_config.json.sample} (60%) diff --git a/.gitignore b/.gitignore index 61c7d289bb..f8e697a3f9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ shared/test/test_credentials.json /libs/MobileSync/build/ /libs/SalesforceSDKCommon/build/ /native/SampleApps/RestAPIExplorer/build/ +/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/ui_test_config.json node_modules/ .idea/ package-lock.json diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj index 64e09cae45..02f7192349 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -83,7 +83,7 @@ 4F5946272ED670C7003C5BDE /* Exceptions for "AuthFlowTesterUITests" folder in "AuthFlowTesterUITests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - test_config.json.sample, + ui_test_config.json.sample, ); target = 4F8E4AF02ED13CE800DA7B7A /* AuthFlowTesterUITests */; }; diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift index baae10ff57..df66938f30 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift @@ -43,6 +43,13 @@ class LoginPageObject { return loginNavigationBar().waitForExistence(timeout: timeout) } + func switchToLSCIfShowingAdvancedAuthentication() -> Void { + if (isShowingAdvancedAuth()) { + tap(advancedAuthCloseButton()) + tap(hostRow(host: "Production")) + } + } + func configureLoginHost(host: String) -> Void { tap(settingsButton()) tap(changeServerButton()) @@ -60,7 +67,9 @@ class LoginPageObject { func performLogin(username: String, password: String) { setTextField(usernameField(), value: username) + tap(passwordFieldLabel()) // click on label to hide keyboard setTextField(passwordField(), value: password) + tap(usernameFieldLabel()) // click on label to hide keyboard tap(loginButton()) tapIfPresent(allowButton()) } @@ -163,11 +172,19 @@ class LoginPageObject { private func hostRow(host: String) -> XCUIElement { return app.staticTexts[host].firstMatch } + + private func usernameFieldLabel() -> XCUIElement { + return app.staticTexts["Username"] + } private func usernameField() -> XCUIElement { return app.descendants(matching: .textField).element } + private func passwordFieldLabel() -> XCUIElement { + return app.staticTexts["Password"] + } + private func passwordField() -> XCUIElement { return app.descendants(matching: .secureTextField).element } @@ -239,6 +256,10 @@ class LoginPageObject { return importConfigAlert().buttons["Import"] } + private func advancedAuthCloseButton() -> XCUIElement { + return app.otherElements["TopBrowserBar"].buttons["Close"] + } + // MARK: - Actions private func tap(_ element: XCUIElement) { @@ -271,7 +292,6 @@ class LoginPageObject { } textField.typeText(value) - tapIfPresent(toolbarDoneButton()) } private func setSwitchField(_ switchField: XCUIElement, value: Bool) { @@ -293,5 +313,9 @@ class LoginPageObject { return row.waitForExistence(timeout: timeout) } + private func isShowingAdvancedAuth() -> Bool { + return advancedAuthCloseButton().waitForExistence(timeout: timeout) + } + } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift new file mode 100644 index 0000000000..d7f0a59d5f --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift @@ -0,0 +1,43 @@ +/* + AdvancedAuthBeaconLoginTests.swift + AuthFlowTesterUITests + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software 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 of salesforce.com, inc. + + 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. + */ + +import XCTest + +/// Tests for login flows using Beacon app configurations with advanced authentication. +/// This class runs the same tests as BeaconLoginTests but uses the advanced_auth login host. +/// +/// NB: Tests use the first user from ui_test_config.json (advanced_auth host) +/// +class AdvancedAuthBeaconLoginTests: BeaconLoginTests { + + // MARK: - Login Host Configuration + + /// Override to use advanced authentication login host. + override func loginHostConfig() -> KnownLoginHostConfig { + return .advancedAuth + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift index 2e7fa83df7..1da6ba2075 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift @@ -36,18 +36,12 @@ class BaseAuthFlowTesterTest: XCTestCase { private var mainPage: AuthFlowTesterMainPageObject! // Test configuration - private let testConfig = TestConfigUtils.shared - private let host: String = TestConfigUtils.shared.loginHostNoProtocol ?? "" + private let testConfig = UITestConfigUtils.shared private var loginHostConfiguredAlready = false override func setUp() { super.setUp() continueAfterFailure = false - - guard host != "" else { - XCTFail("No login host configured") - fatalError("No login host configured") - } } override func tearDown() { @@ -78,6 +72,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// Must be called after `launch()`. /// /// - Parameters: + /// - loginHost: The login host configuration to use. /// - user: The user to log in with. /// - staticAppConfigName: The static app configuration name. /// - staticScopeSelection: The scope selection for static configuration. Defaults to `.empty`. @@ -86,6 +81,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// - useWebServerFlow: Whether to use web server OAuth flow. Defaults to `true`. /// - useHybridFlow: Whether to use hybrid authentication flow. Defaults to `true`. func login( + loginHost: KnownLoginHostConfig, user: KnownUserConfig, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection = .empty, @@ -94,13 +90,12 @@ class BaseAuthFlowTesterTest: XCTestCase { useWebServerFlow: Bool = true, useHybridFlow: Bool = true, ) { - // To speed up things a bit - only configuring login host once (it never changes) - if (!loginHostConfiguredAlready) { - loginPage.configureLoginHost(host: host) - loginHostConfiguredAlready = true - } + // The login settings button is shown only for regular authentication + // If the configured login host uses advanced authentication + // we need to switch a login host that does not use it (login.salesforce.com) + loginPage.switchToLSCIfShowingAdvancedAuthentication() - let userConfig = getUser(user) + let userConfig = getUser(loginHost: loginHost, user: user) let staticAppConfig = getAppConfig(named: staticAppConfigName) let dynamicAppConfig = dynamicAppConfigName == nil ? nil : getAppConfig(named: dynamicAppConfigName!) let staticScopes = testConfig.getScopesToRequest(for: staticAppConfig, staticScopeSelection) @@ -115,6 +110,12 @@ class BaseAuthFlowTesterTest: XCTestCase { useHybridFlow: useHybridFlow, ) + // NBL Configuring login host last + // When the configured login host requires advanced authentication + // the login settings button is no longer available on the screen + let hostConfig = try! testConfig.getLoginHost(loginHost) + loginPage.configureLoginHost(host: hostConfig.urlNoProtocol) + loginPage.performLogin(username: userConfig.username, password: userConfig.password) } @@ -133,6 +134,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// Use this method when multiple users are logged in and you want to switch between them. /// /// - Parameters: + /// - loginHost: The login host configuration to use. /// - user: The user to switch to. /// - staticAppConfigName: The static app configuration name. /// - staticScopeSelection: The scope selection for static configuration. Defaults to `.empty`. @@ -141,6 +143,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// - useWebServerFlow: Whether web server OAuth flow was used. Defaults to `true`. /// - useHybridFlow: Whether hybrid authentication flow was used. Defaults to `true`. func switchToUserAndValidate( + loginHost: KnownLoginHostConfig, user: KnownUserConfig, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection = .empty, @@ -150,10 +153,11 @@ class BaseAuthFlowTesterTest: XCTestCase { useHybridFlow: Bool = true ) { // Switch user - mainPage.switchToUser(username: getUser(user).username) + mainPage.switchToUser(username: getUser(loginHost: loginHost, user: user).username) // Validate validate( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -170,6 +174,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// Use this for the initial login flow in tests. /// /// - Parameters: + /// - loginHost: The login host configuration to use. /// - user: The user to log in with. /// - staticAppConfigName: The static app configuration name. /// - staticScopeSelection: The scope selection for static configuration. Defaults to `.empty`. @@ -178,6 +183,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// - useWebServerFlow: Whether to use web server OAuth flow. Defaults to `true`. /// - useHybridFlow: Whether to use hybrid authentication flow. Defaults to `true`. func launchAndLogin( + loginHost: KnownLoginHostConfig, user: KnownUserConfig, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection = .empty, @@ -191,6 +197,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Login login( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -207,6 +214,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// Use this for the initial login flow in tests. /// /// - Parameters: + /// - loginHost: The login host configuration to use. Defaults to `.regularAuth`. /// - user: The user to log in with. Defaults to `.first`. /// - staticAppConfigName: The static app configuration name. /// - staticScopeSelection: The scope selection for static configuration. Defaults to `.empty`. @@ -215,6 +223,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// - useWebServerFlow: Whether to use web server OAuth flow. Defaults to `true`. /// - useHybridFlow: Whether to use hybrid authentication flow. Defaults to `true`. func launchLoginAndValidate( + loginHost: KnownLoginHostConfig = .regularAuth, user: KnownUserConfig = .first, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection = .empty, @@ -232,6 +241,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Login login( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -243,6 +253,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Validate validate( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -259,6 +270,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// Taps the "Add User" button before performing login. /// /// - Parameters: + /// - loginHost: The login host configuration to use. /// - user: The user to log in with. /// - staticAppConfigName: The static app configuration name. /// - staticScopeSelection: The scope selection for static configuration. Defaults to `.empty`. @@ -267,6 +279,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// - useWebServerFlow: Whether to use web server OAuth flow. Defaults to `true`. /// - useHybridFlow: Whether to use hybrid authentication flow. Defaults to `true`. func loginOtherUserAndValidate( + loginHost: KnownLoginHostConfig, user: KnownUserConfig, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection = .empty, @@ -284,6 +297,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Login login( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -295,6 +309,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Validate validate( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -311,12 +326,14 @@ class BaseAuthFlowTesterTest: XCTestCase { /// with the expected credentials. Use this to test session persistence. /// /// - Parameters: + /// - loginHost: The login host configuration to use. Defaults to `.regularAuth`. /// - user: The user that should still be logged in after restart. Defaults to `.first`. /// - userAppConfigName: The app configuration the user was logged in with. /// - userScopeSelection: The scope selection the user was logged in with. Defaults to `.empty`. /// - useWebServerFlow: Whether web server OAuth flow was used. Defaults to `true`. /// - useHybridFlow: Whether hybrid authentication flow was used. Defaults to `true`. func restartAndValidate( + loginHost: KnownLoginHostConfig = .regularAuth, user: KnownUserConfig = .first, userAppConfigName: KnownAppConfig, userScopeSelection: ScopeSelection = .empty, @@ -330,6 +347,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Validate user // Not checking static app config since it will depend on the bootconfig of the target app validateUser( + loginHost: loginHost, user: user, userAppConfigName: userAppConfigName, userScopeSelection: userScopeSelection, @@ -344,6 +362,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// then validates that the credentials are updated correctly and the refresh token has changed. /// /// - Parameters: + /// - loginHost: The login host configuration to use. /// - staticAppConfigName: The static app configuration name. /// - staticScopeSelection: The scope selection for static configuration. Defaults to `.empty`. /// - migrationAppConfigName: The app configuration to migrate to. @@ -351,6 +370,7 @@ class BaseAuthFlowTesterTest: XCTestCase { /// - useWebServerFlow: Whether to use web server OAuth flow. Defaults to `true`. /// - useHybridFlow: Whether to use hybrid authentication flow. Defaults to `true`. func migrateAndValidate( + loginHost: KnownLoginHostConfig, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection = .empty, migrationAppConfigName: KnownAppConfig, @@ -362,7 +382,7 @@ class BaseAuthFlowTesterTest: XCTestCase { let originalUserCredentials = mainPage.getUserCredentials() // Get current user - let user = getKnownUserConfig(byUsername: originalUserCredentials.username) + let user = getKnownUserConfig(loginHost: loginHost, byUsername: originalUserCredentials.username) // Migrate refresh token @@ -373,6 +393,7 @@ class BaseAuthFlowTesterTest: XCTestCase { // Validate after migration let migratedUserCredentials = validate( + loginHost: loginHost, user: user, staticAppConfigName: staticAppConfigName, staticScopeSelection: staticScopeSelection, @@ -394,6 +415,7 @@ class BaseAuthFlowTesterTest: XCTestCase { @discardableResult private func validateUser( + loginHost: KnownLoginHostConfig, user: KnownUserConfig, userAppConfigName: KnownAppConfig, userScopeSelection: ScopeSelection, @@ -401,7 +423,7 @@ class BaseAuthFlowTesterTest: XCTestCase { useHybridFlow: Bool, ) -> UserCredentialsData { - let userConfig = getUser(user) + let userConfig = getUser(loginHost: loginHost, user: user) let userAppConfig = getAppConfig(named: userAppConfigName) let expectedGrantedScopes = testConfig.getExpectedScopesGranted(for: userAppConfig, userScopeSelection) let issuesJwt = userAppConfig.issuesJwt @@ -439,6 +461,7 @@ class BaseAuthFlowTesterTest: XCTestCase { @discardableResult private func validate( + loginHost: KnownLoginHostConfig, user: KnownUserConfig, staticAppConfigName: KnownAppConfig, staticScopeSelection: ScopeSelection, @@ -454,6 +477,7 @@ class BaseAuthFlowTesterTest: XCTestCase { assertMainPageLoaded() let userCredentials = validateUser( + loginHost: loginHost, user: user, userAppConfigName: userAppConfigName, userScopeSelection: userScopeSelection, @@ -569,21 +593,21 @@ class BaseAuthFlowTesterTest: XCTestCase { } } - private func getUser(_ user: KnownUserConfig) -> UserConfig { + private func getUser(loginHost: KnownLoginHostConfig, user: KnownUserConfig) -> UserConfig { do { - return try testConfig.getUser(user) + return try testConfig.getUser(loginHost, user) } catch { - XCTFail("Failed to get user \(user): \(error)") - fatalError("Failed to get user \(user): \(error)") + XCTFail("Failed to get user \(user) from login host \(loginHost): \(error)") + fatalError("Failed to get user \(user) from login host \(loginHost): \(error)") } } - private func getKnownUserConfig(byUsername username: String) -> KnownUserConfig { + private func getKnownUserConfig(loginHost: KnownLoginHostConfig, byUsername username: String) -> KnownUserConfig { do { - return try testConfig.getKnownUserConfig(byUsername: username) + return try testConfig.getKnownUserConfig(loginHost, byUsername: username) } catch { - XCTFail("Failed to get user \(username): \(error)") - fatalError("Failed to get user \(username): \(error)") + XCTFail("Failed to get user \(username) from login host \(loginHost): \(error)") + fatalError("Failed to get user \(username) from login host \(loginHost): \(error)") } } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BeaconLoginTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BeaconLoginTests.swift index 19e0742635..c98d2023f9 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BeaconLoginTests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BeaconLoginTests.swift @@ -30,42 +30,50 @@ import XCTest /// Tests for login flows using Beacon app configurations. /// Beacon apps are lightweight authentication apps for specific use cases. /// -/// NB: Tests use the first user from test_config.json +/// NB: Tests use the first user from ui_test_config.json /// class BeaconLoginTests: BaseAuthFlowTesterTest { + // MARK: - Login Host Configuration + + /// Returns the login host configuration to use for tests. + /// Subclasses can override this to use a different login host. + func loginHostConfig() -> KnownLoginHostConfig { + return .regularAuth + } + // MARK: - Beacon Opaque Tests /// Login with Beacon advanced opaque using default scopes and web server flow. func testBeaconAdvancedOpaque_DefaultScopes() throws { - launchLoginAndValidate(staticAppConfigName: .beaconAdvancedOpaque) + launchLoginAndValidate(loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedOpaque) } /// Login with Beacon advanced opaque using subset of scopes and web server flow. func testBeaconAdvancedOpaque_SubsetScopes() throws { - launchLoginAndValidate(staticAppConfigName: .beaconAdvancedOpaque, staticScopeSelection: .subset) + launchLoginAndValidate(loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedOpaque, staticScopeSelection: .subset) } /// Login with Beacon advanced opaque using all scopes and web server flow. func testBeaconAdvancedOpaque_AllScopes() throws { - launchLoginAndValidate(staticAppConfigName: .beaconAdvancedOpaque, staticScopeSelection: .all) + launchLoginAndValidate(loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedOpaque, staticScopeSelection: .all) } // MARK: - Beacon JWT Tests /// Login with Beacon advanced JWT using default scopes and web server flow. func testBeaconAdvancedJwt_DefaultScopes() throws { - launchLoginAndValidate(staticAppConfigName: .beaconAdvancedJwt) + launchLoginAndValidate(loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedJwt) } /// Login with Beacon advanced JWT using subset of scopes and web server flow. func testBeaconAdvancedJwt_SubsetScopes() throws { - launchLoginAndValidate(staticAppConfigName: .beaconAdvancedJwt, staticScopeSelection: .subset) + launchLoginAndValidate(loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedJwt, staticScopeSelection: .subset) } /// Login with Beacon advanced JWT using all scopes and web server flow. func testBeaconAdvancedJwt_AllScopes() throws { - launchLoginAndValidate(staticAppConfigName: .beaconAdvancedJwt, staticScopeSelection: .all) + launchLoginAndValidate(loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedJwt, staticScopeSelection: .all) } // MARK: - Using dynamic config @@ -74,10 +82,12 @@ class BeaconLoginTests: BaseAuthFlowTesterTest { /// Restart the application and validate it still works afterwards func testBeaconAdvancedJwt_DefaultScopes_DynamicConfiguration_WithRestart() throws { launchLoginAndValidate( + loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedOpaque, dynamicAppConfigName: .beaconAdvancedJwt ) restartAndValidate( + loginHost: loginHostConfig(), userAppConfigName: .beaconAdvancedJwt ) } @@ -86,11 +96,13 @@ class BeaconLoginTests: BaseAuthFlowTesterTest { /// Restart the application and validate it still works afterwards func testBeaconAdvancedJwt_SubsetScopes_DynamicConfiguration_WithRestart() throws { launchLoginAndValidate( + loginHost: loginHostConfig(), staticAppConfigName: .beaconAdvancedOpaque, dynamicAppConfigName: .beaconAdvancedJwt, dynamicScopeSelection: .subset ) restartAndValidate( + loginHost: loginHostConfig(), userAppConfigName: .beaconAdvancedJwt, userScopeSelection: .subset ) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/ECALoginTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/ECALoginTests.swift index 11376936bc..03140bd9ea 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/ECALoginTests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/ECALoginTests.swift @@ -30,7 +30,7 @@ import XCTest /// Tests for login flows using External Client App (ECA) configurations. /// ECA apps are first-party Salesforce apps that use enhanced authentication flows. /// -/// NB: Tests use the first user from test_config.json +/// NB: Tests use the first user from ui_test_config.json /// class ECALoginTests: BaseAuthFlowTesterTest { diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/LegacyLoginTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/LegacyLoginTests.swift index bd0011206b..5745248968 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/LegacyLoginTests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/LegacyLoginTests.swift @@ -32,7 +32,7 @@ import XCTest /// - User agent flow tests /// - Non-hybrid flow tests /// -/// NB: Tests use the first user from test_config.json +/// NB: Tests use the first user from ui_test_config.json /// class LegacyLoginTests: BaseAuthFlowTesterTest { diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MigrationTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MigrationTests.swift index 578ac05df4..103dd5dd99 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MigrationTests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MigrationTests.swift @@ -31,7 +31,7 @@ import XCTest /// These tests verify that users can seamlessly transition between app types /// (CA, ECA, Beacon) and token formats (opaque, JWT) without re-authentication. /// -/// NB: Tests use the second user from test_config.json +/// NB: Tests use the second user from ui_test_config.json /// class MigrationTests: BaseAuthFlowTesterTest { @@ -40,11 +40,13 @@ class MigrationTests: BaseAuthFlowTesterTest { /// Migrate within same CA (scope upgrade). func testMigrateCA_AddMoreScopes() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .caAdvancedJwt, staticScopeSelection: .subset ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .caAdvancedJwt, staticScopeSelection: .subset, migrationAppConfigName: .caAdvancedJwt, @@ -55,11 +57,13 @@ class MigrationTests: BaseAuthFlowTesterTest { /// Migrate within same ECA (scope upgrade). func testMigrateECA_AddMoreScopes() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .ecaAdvancedJwt, staticScopeSelection: .subset ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .ecaAdvancedJwt, staticScopeSelection: .subset, migrationAppConfigName: .ecaAdvancedJwt, @@ -70,11 +74,13 @@ class MigrationTests: BaseAuthFlowTesterTest { /// Migrate within same Beacon (scope upgrade). func testMigrateBeacon_AddMoreScopes() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .beaconAdvancedJwt, staticScopeSelection: .subset ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .beaconAdvancedJwt, staticScopeSelection: .subset, migrationAppConfigName: .beaconAdvancedJwt, @@ -87,10 +93,12 @@ class MigrationTests: BaseAuthFlowTesterTest { // Migrate from CA to Beacon func testMigrateCAToBeacon() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .caAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .caAdvancedOpaque, migrationAppConfigName: .beaconAdvancedOpaque ) @@ -99,10 +107,12 @@ class MigrationTests: BaseAuthFlowTesterTest { // Migrate from Beacon to CA func testMigrateBeaconToCA() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .beaconAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .beaconAdvancedOpaque, migrationAppConfigName: .caAdvancedOpaque ) @@ -113,14 +123,17 @@ class MigrationTests: BaseAuthFlowTesterTest { /// Migrate from CA to ECA and back to CA func testMigrateCAToECA() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .caAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .caAdvancedOpaque, migrationAppConfigName: .ecaAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .caAdvancedOpaque, // should not have changed migrationAppConfigName: .caAdvancedOpaque ) @@ -129,14 +142,17 @@ class MigrationTests: BaseAuthFlowTesterTest { // Migrate from CA to Beacon and back to CA func testMigrateCAToBeaconAndBack() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .caAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .caAdvancedOpaque, migrationAppConfigName: .beaconAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .caAdvancedOpaque, // should not have changed migrationAppConfigName: .caAdvancedOpaque ) @@ -145,14 +161,17 @@ class MigrationTests: BaseAuthFlowTesterTest { /// Migrate from Beacon opaque to Beacon JWT and back to Beacon opaque func testMigrateBeaconOpaqueToJWTAndBack() throws { launchAndLogin( + loginHost: .regularAuth, user:.second, staticAppConfigName: .beaconAdvancedOpaque ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .beaconAdvancedOpaque, migrationAppConfigName: .beaconAdvancedJwt ) migrateAndValidate( + loginHost: .regularAuth, staticAppConfigName: .beaconAdvancedOpaque, // should not have changed migrationAppConfigName: .beaconAdvancedOpaque ) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MultiUserLoginTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MultiUserLoginTests.swift index 137bfc8f18..f46e604cf5 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MultiUserLoginTests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/MultiUserLoginTests.swift @@ -33,7 +33,7 @@ import XCTest /// - Same or different app types (opaque vs JWT) /// - Same or different scopes /// -/// NB: Tests use the fourth and fifth user from test_config.json +/// NB: Tests use the fourth and fifth user from ui_test_config.json /// class MultiUserLoginTests: BaseAuthFlowTesterTest { @@ -43,24 +43,28 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { func testBothStatic_SameApp_SameScopes() throws { // Initial user launchAndLogin( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque ) // Other user loginOtherUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque ) // Switch back to initial user switchToUserAndValidate( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque, userAppConfigName: .ecaAdvancedOpaque) // Switch back to other user switchToUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque, userAppConfigName: .ecaAdvancedOpaque) @@ -73,24 +77,28 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { func testBothStatic_DifferentApps() throws { // Initial user launchAndLogin( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque ) // Other user loginOtherUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedJwt ) // Switch back to initial user switchToUserAndValidate( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedJwt, // static config overwritten userAppConfigName: .ecaAdvancedOpaque) // Switch back to other user switchToUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedJwt, userAppConfigName: .ecaAdvancedJwt) @@ -103,6 +111,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { func testBothStatic_SameApp_DifferentScopes() throws { // Initial user launchAndLogin( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque, staticScopeSelection: .subset @@ -110,12 +119,14 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Other user loginOtherUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque ) // Switch back to initial user switchToUserAndValidate( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque, staticScopeSelection: .empty, @@ -125,6 +136,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Switch back to other user switchToUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque, staticScopeSelection: .empty, @@ -142,12 +154,14 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { func testFirstStatic_SecondDynamic_DifferentApps() throws { // Initial user launchAndLogin( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque ) // Other user loginOtherUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque, dynamicAppConfigName: .ecaAdvancedJwt @@ -155,6 +169,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Switch back to initial user switchToUserAndValidate( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque, userAppConfigName: .ecaAdvancedOpaque @@ -162,6 +177,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Switch back to other user switchToUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque, userAppConfigName: .ecaAdvancedJwt, @@ -175,6 +191,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { func testFirstDynamic_SecondStatic_DifferentApps() throws { // Initial user launchAndLogin( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaBasicOpaque, dynamicAppConfigName: .ecaAdvancedJwt @@ -182,12 +199,14 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Other user loginOtherUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque ) // Switch back to initial user switchToUserAndValidate( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaAdvancedOpaque, userAppConfigName: .ecaAdvancedJwt @@ -195,6 +214,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Switch back to other user switchToUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaAdvancedOpaque, userAppConfigName: .ecaAdvancedOpaque, @@ -210,6 +230,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { func testBothDynamic_DifferentApps() throws { // Initial user launchAndLogin( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaBasicOpaque, dynamicAppConfigName: .ecaAdvancedOpaque @@ -217,6 +238,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Other user loginOtherUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaBasicOpaque, dynamicAppConfigName: .ecaAdvancedJwt @@ -224,6 +246,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Switch back to initial user switchToUserAndValidate( + loginHost: .regularAuth, user: .fourth, staticAppConfigName: .ecaBasicOpaque, userAppConfigName: .ecaAdvancedOpaque @@ -231,6 +254,7 @@ class MultiUserLoginTests: BaseAuthFlowTesterTest { // Switch back to other user switchToUserAndValidate( + loginHost: .regularAuth, user: .fifth, staticAppConfigName: .ecaBasicOpaque, userAppConfigName: .ecaAdvancedJwt, diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/TestConfigUtils.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/UITestConfigUtils.swift similarity index 69% rename from native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/TestConfigUtils.swift rename to native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/UITestConfigUtils.swift index 1d0329987d..3fe233f9da 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/TestConfigUtils.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/UITestConfigUtils.swift @@ -1,5 +1,5 @@ /* - TestConfigUtils.swift + UITestConfigUtils.swift AuthFlowTesterUITests Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. @@ -38,34 +38,37 @@ enum TestConfigError: Error, CustomStringConvertible { case userNotFound(String) case appNotFound(String) case appNotConfigured(String) + case loginHostNotFound(String) var description: String { switch self { case .noPrimaryUser: - return "No primary user found in test_config.json" + return "No primary user found in ui_test_config.json" case .noSecondaryUser: - return "No secondary user found in test_config.json" + return "No secondary user found in ui_test_config.json" case .noThirdUser: - return "No third user found in test_config.json" + return "No third user found in ui_test_config.json" case .noFourthUser: - return "No fourth user found in test_config.json" + return "No fourth user found in ui_test_config.json" case .noFifthUser: - return "No fifth user found in test_config.json" + return "No fifth user found in ui_test_config.json" case .userNotFound(let username): - return "User '\(username)' not found in test_config.json" + return "User '\(username)' not found in ui_test_config.json" case .appNotFound(let appName): - return "App '\(appName)' not found in test_config.json" + return "App '\(appName)' not found in ui_test_config.json" case .appNotConfigured(let appName): - return "App '\(appName)' has empty consumerKey in test_config.json" + return "App '\(appName)' has empty consumerKey in ui_test_config.json" + case .loginHostNotFound(let hostName): + return "Login host '\(hostName)' not found in ui_test_config.json" } } } -// MAKR: - ScopeSelection +// MARK: - ScopeSelection enum ScopeSelection { case empty // will not send scopes param - should be granted all the scopes defined on the server - case all // will send all the scopes defined in test_config.json - case subset // will send a subset of the scopes defined in test_config.json + case all // will send all the scopes defined in ui_test_config.json + case subset // will send a subset of the scopes defined in ui_test_config.json } // MARK: - Configured Users @@ -78,6 +81,13 @@ enum KnownUserConfig { case fifth } +// MARK: - Login Host Names + +enum KnownLoginHostConfig: String { + case regularAuth = "regular_auth" + case advancedAuth = "advanced_auth" +} + // MARK: - App Names enum KnownAppConfig: String { @@ -97,6 +107,20 @@ enum KnownAppConfig: String { // MARK: - Configuration Models +/// Represents a login host configuration for testing +struct LoginHostConfig: Codable { + let name: String + let url: String + let users: [UserConfig] + + /// Returns URL without protocol (https:// or http://) + var urlNoProtocol: String { + return url + .replacingOccurrences(of: "https://", with: "") + .replacingOccurrences(of: "http://", with: "") + } +} + /// Represents an app configuration for testing struct AppConfig: Codable { let name: String @@ -123,18 +147,17 @@ struct UserConfig: Codable { /// Represents the complete test configuration struct TestConfig: Codable { - let loginHost: String + let loginHosts: [LoginHostConfig] let apps: [AppConfig] - let users: [UserConfig] } // MARK: - Configuration Utility -/// Utility class to parse and access test configuration from test_config.json in the test bundle -class TestConfigUtils { +/// Utility class to parse and access test configuration from ui_test_config.json in the test bundle +class UITestConfigUtils { /// Shared singleton instance - static let shared = TestConfigUtils() + static let shared = UITestConfigUtils() /// Parsed test configuration (nil if not provided or parsing failed) private(set) var config: TestConfig? @@ -148,23 +171,23 @@ class TestConfigUtils { // MARK: - Configuration Parsing - /// Parses the test configuration from test_config.json file in the test bundle + /// Parses the test configuration from ui_test_config.json file in the test bundle private func parseConfiguration() { // Get the bundle for this class - let bundle = Bundle(for: TestConfigUtils.self) + let bundle = Bundle(for: UITestConfigUtils.self) - // Look for test_config.json file - guard let configPath = bundle.path(forResource: "test_config", ofType: "json") else { - print("⚠️ test_config.json file not found in test bundle") + // Look for ui_test_config.json file + guard let configPath = bundle.path(forResource: "ui_test_config", ofType: "json") else { + print("⚠️ ui_test_config.json file not found in test bundle") return } // Read the file contents guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: configPath)) else { - let error = NSError(domain: "TestConfigUtils", code: 1, - userInfo: [NSLocalizedDescriptionKey: "Failed to read test_config.json file"]) + let error = NSError(domain: "UITestConfigUtils", code: 1, + userInfo: [NSLocalizedDescriptionKey: "Failed to read ui_test_config.json file"]) parseError = error - print("❌ Failed to read test_config.json file") + print("❌ Failed to read ui_test_config.json file") return } @@ -172,10 +195,9 @@ class TestConfigUtils { do { let decoder = JSONDecoder() config = try decoder.decode(TestConfig.self, from: jsonData) - print("✅ Test configuration loaded successfully from test_config.json") - print(" Login Host: \(config?.loginHost ?? "none")") + print("✅ Test configuration loaded successfully from ui_test_config.json") + print(" Login Hosts: \(config?.loginHosts.count ?? 0)") print(" Apps: \(config?.apps.count ?? 0)") - print(" Users: \(config?.users.count ?? 0)") } catch { parseError = error print("❌ Failed to parse test configuration: \(error.localizedDescription)") @@ -189,15 +211,9 @@ class TestConfigUtils { return config != nil } - /// Returns the login host from configuration - var loginHost: String? { - return config?.loginHost - } - - var loginHostNoProtocol: String? { - return loginHost? - .replacingOccurrences(of: "https://", with: "") - .replacingOccurrences(of: "http://", with: "") + /// Returns all login hosts from configuration + var loginHosts: [LoginHostConfig] { + return config?.loginHosts ?? [] } /// Returns all apps from configuration @@ -205,11 +221,6 @@ class TestConfigUtils { return config?.apps ?? [] } - /// Returns all users from configuration - var users: [UserConfig] { - return config?.users ?? [] - } - // MARK: - Scope Utilities /// Removes a scope from a space-separated scope string. @@ -231,41 +242,51 @@ class TestConfigUtils { // MARK: - Throwing Accessors - /// Returns a user by their position (first or second) or throws an error if not found - func getUser(_ user: KnownUserConfig) throws -> UserConfig { + /// Returns a login host configuration by its name or throws an error if not found + func getLoginHost(_ loginHost: KnownLoginHostConfig) throws -> LoginHostConfig { + guard let hostConfig = config?.loginHosts.first(where: { $0.name == loginHost.rawValue }) else { + throw TestConfigError.loginHostNotFound(loginHost.rawValue) + } + return hostConfig + } + + /// Returns a user by their position (first, second, etc.) for a specific login host or throws an error if not found + func getUser(_ loginHost: KnownLoginHostConfig, _ user: KnownUserConfig) throws -> UserConfig { + let hostConfig = try getLoginHost(loginHost) + switch user { case .first: - guard let firstUser = config?.users.first else { + guard let firstUser = hostConfig.users.first else { throw TestConfigError.noPrimaryUser } return firstUser case .second: - guard let users = config?.users, users.count >= 2 else { + guard hostConfig.users.count >= 2 else { throw TestConfigError.noSecondaryUser } - return users[1] + return hostConfig.users[1] case .third: - guard let users = config?.users, users.count >= 3 else { + guard hostConfig.users.count >= 3 else { throw TestConfigError.noThirdUser } - return users[2] + return hostConfig.users[2] case .fourth: - guard let users = config?.users, users.count >= 4 else { + guard hostConfig.users.count >= 4 else { throw TestConfigError.noFourthUser } - return users[3] + return hostConfig.users[3] case .fifth: - guard let users = config?.users, users.count >= 5 else { + guard hostConfig.users.count >= 5 else { throw TestConfigError.noFifthUser } - return users[4] + return hostConfig.users[4] } } - /// Returns a known user config by their username or throws an error if not found - func getKnownUserConfig(byUsername username: String) throws -> KnownUserConfig { - guard let users = config?.users, - let index = users.firstIndex(where: { $0.username == username }) else { + /// Returns a known user config by their username for a specific login host or throws an error if not found + func getKnownUserConfig(_ loginHost: KnownLoginHostConfig, byUsername username: String) throws -> KnownUserConfig { + let hostConfig = try getLoginHost(loginHost) + guard let index = hostConfig.users.firstIndex(where: { $0.username == username }) else { throw TestConfigError.userNotFound(username) } switch index { @@ -301,10 +322,9 @@ class TestConfigUtils { /// Returns expected scopes granted func getExpectedScopesGranted(for appConfig:AppConfig, _ scopeSelection: ScopeSelection) -> String { switch(scopeSelection) { - case .empty: return appConfig.scopes // that assumes the scopes in test_config.json match the server config + case .empty: return appConfig.scopes // that assumes the scopes in ui_test_config.json match the server config case .subset: return removeScope(scopes: appConfig.scopes, scopeToRemove: "sfap_api") // that assumes the selected ca/eca/beacon has the sfap_api scope case .all: return appConfig.scopes } } } - diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/test_config.json.sample b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/ui_test_config.json.sample similarity index 60% rename from native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/test_config.json.sample rename to native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/ui_test_config.json.sample index 109a70d952..f04ec91927 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/test_config.json.sample +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/ui_test_config.json.sample @@ -1,5 +1,58 @@ { - "loginHost": "https://testorg.my.salesforce.com", + "loginHosts": [ + { + "name": "regular_auth", + "url": "https://your-test-org.my.salesforce.com", + "users": [ + { + "username": "testios1@testorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios2@testorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios3@testorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios4@testorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios5@testorg.com", + "password": "yourPasswordHere" + } + ] + }, + { + "name": "advanced_auth", + "url": "https://your-advanced-test-org.my.salesforce.com", + "users": [ + { + "username": "testios1@advancedtestorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios2@advancedtestorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios3@advancedtestorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios4@advancedtestorg.com", + "password": "yourPasswordHere" + }, + { + "username": "testios5@advancedtestorg.com", + "password": "yourPasswordHere" + } + ] + } + ], "apps": [ { "name": "ca_basic_opaque", @@ -53,7 +106,7 @@ "name": "beacon_basic_opaque", "consumerKey": "your_consumer_key_here", "redirectUri": "beaconbasicopaque://success/done", - "scopes": "api id refresh_token" + "scopes": "api profile refresh_token" }, { "name": "beacon_basic_jwt", @@ -65,23 +118,13 @@ "name": "beacon_advanced_opaque", "consumerKey": "your_consumer_key_here", "redirectUri": "beaconadvancedopaque://success/done", - "scopes": "api content id lightning refresh_token sfap_api visualforce web" + "scopes": "api content id lightning refresh_token sfap_api web" }, { "name": "beacon_advanced_jwt", "consumerKey": "your_consumer_key_here", "redirectUri": "beaconadvancedjwt://success/done", - "scopes": "api content id lightning refresh_token sfap_api visualforce web" - } - ], - "users": [ - { - "username": "testios1@testorg.com", - "password": "yourPasswordHere" - }, - { - "username": "testios2@testorg.com", - "password": "yourPasswordHere" + "scopes": "api content id lightning refresh_token sfap_api web" } ] } From e44143768db748bbe6b618f10e4c9233c236c3e2 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 23 Jan 2026 16:41:54 -0800 Subject: [PATCH 2/3] Switching to LSC only in tests that use advanced auth --- .../PageObjects/LoginPageObject.swift | 1 + .../Tests/AdvancedAuthBeaconLoginTests.swift | 4 ++++ .../Tests/BaseAuthFlowTesterTest.swift | 17 ++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift index df66938f30..5f419e5c4f 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift @@ -292,6 +292,7 @@ class LoginPageObject { } textField.typeText(value) + tapIfPresent(toolbarDoneButton()) } private func setSwitchField(_ switchField: XCUIElement, value: Bool) { diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift index d7f0a59d5f..e63e483f23 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/AdvancedAuthBeaconLoginTests.swift @@ -40,4 +40,8 @@ class AdvancedAuthBeaconLoginTests: BeaconLoginTests { override func loginHostConfig() -> KnownLoginHostConfig { return .advancedAuth } + + override func postLogoutCleanup() { + switchToLSCIfShowingAdvancedAuthentication() + } } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift index 1da6ba2075..0ff7810b7e 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/BaseAuthFlowTesterTest.swift @@ -37,7 +37,6 @@ class BaseAuthFlowTesterTest: XCTestCase { // Test configuration private let testConfig = UITestConfigUtils.shared - private var loginHostConfiguredAlready = false override func setUp() { super.setUp() @@ -46,9 +45,14 @@ class BaseAuthFlowTesterTest: XCTestCase { override func tearDown() { logout() + postLogoutCleanup() super.tearDown() } + func postLogoutCleanup() { + // Some tests might need to do more e.g. switch back to login host that uses regular auth so that login settings is shown + } + // MARK: - Public API for Subclasses /// Launches the application and ensures it starts in a logged-out state. @@ -90,11 +94,6 @@ class BaseAuthFlowTesterTest: XCTestCase { useWebServerFlow: Bool = true, useHybridFlow: Bool = true, ) { - // The login settings button is shown only for regular authentication - // If the configured login host uses advanced authentication - // we need to switch a login host that does not use it (login.salesforce.com) - loginPage.switchToLSCIfShowingAdvancedAuthentication() - let userConfig = getUser(loginHost: loginHost, user: user) let staticAppConfig = getAppConfig(named: staticAppConfigName) let dynamicAppConfig = dynamicAppConfigName == nil ? nil : getAppConfig(named: dynamicAppConfigName!) @@ -110,7 +109,7 @@ class BaseAuthFlowTesterTest: XCTestCase { useHybridFlow: useHybridFlow, ) - // NBL Configuring login host last + // Configuring login host last // When the configured login host requires advanced authentication // the login settings button is no longer available on the screen let hostConfig = try! testConfig.getLoginHost(loginHost) @@ -411,6 +410,10 @@ class BaseAuthFlowTesterTest: XCTestCase { ) } + func switchToLSCIfShowingAdvancedAuthentication() { + loginPage.switchToLSCIfShowingAdvancedAuthentication() + } + // MARK: - Private Helpers @discardableResult From 1345f2899bb4a368ae33d79753de2f87461fc71d Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 23 Jan 2026 18:05:56 -0800 Subject: [PATCH 3/3] When testing refresh token migration, using import button to specify the new app config (faster and more reliable) --- .../AuthFlowTesterMainPageObject.swift | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/AuthFlowTesterMainPageObject.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/AuthFlowTesterMainPageObject.swift index 09db0f839e..5b1756b630 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/AuthFlowTesterMainPageObject.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/AuthFlowTesterMainPageObject.swift @@ -27,6 +27,7 @@ import Foundation import XCTest +import SalesforceSDKCore // MARK: - Label Constants (mirroring the app's Labels structs for JSON parsing) @@ -270,10 +271,9 @@ class AuthFlowTesterMainPageObject { // Tap Change Key button to open the sheet tap(bottomBarChangeKeyButton()) - // Fill in the text fields - setTextField(consumerKeyTextField(), value: appConfig.consumerKey) - setTextField(callbackUrlTextField(), value: appConfig.redirectUri) - setTextField(scopesTextField(), value: scopesToRequest) + // Build JSON config and import it + let configJSON = buildConfigJSON(consumerKey: appConfig.consumerKey, redirectUri: appConfig.redirectUri, scopes: scopesToRequest) + importConfig(configJSON) // Tap the migrate button tap(migrateRefreshTokenButton()) @@ -289,6 +289,35 @@ class AuthFlowTesterMainPageObject { return true } + // MARK: - Config Import Helpers + + private func buildConfigJSON(consumerKey: String, redirectUri: String, scopes: String) -> String { + let config: [String: String] = [ + BootConfigJSONKeys.consumerKey: consumerKey, + BootConfigJSONKeys.redirectUri: redirectUri, + BootConfigJSONKeys.scopes: scopes + ] + guard let jsonData = try? JSONSerialization.data(withJSONObject: config, options: []), + let jsonString = String(data: jsonData, encoding: .utf8) else { + return "{}" + } + return jsonString + } + + private func importConfig(_ jsonString: String) { + tap(importConfigButton()) + + // Wait for alert to appear + let alert = importConfigAlert() + _ = alert.waitForExistence(timeout: timeout) + + // Type into the alert's text field + let textField = importConfigTextField() + textField.typeText(jsonString) + + tap(importAlertButton()) + } + // MARK: - UI Element Accessors private func navigationTitle() -> XCUIElement { @@ -373,6 +402,25 @@ class AuthFlowTesterMainPageObject { return buttons.matching(predicate).firstMatch } + // Config import + + private func importConfigButton() -> XCUIElement { + return app.buttons.matching(identifier: "importConfigButton").firstMatch + } + + private func importConfigAlert() -> XCUIElement { + return app.alerts["Import Configuration"] + } + + private func importConfigTextField() -> XCUIElement { + // Access text field through the alert - SwiftUI alert TextFields are accessed this way + return importConfigAlert().textFields.firstMatch + } + + private func importAlertButton() -> XCUIElement { + return importConfigAlert().buttons["Import"] + } + // User switching private func newUserButton() -> XCUIElement {