Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
54268e0
feat(W-23721537): speed up AuthFlowTester UI tests via launch-arg dri…
wmathurin Aug 5, 2026
7fb62cf
fix(W-23721537): remove skipLogoutAtTearDown calls from ForceAdvanced…
wmathurin Aug 5, 2026
8b3eedf
fix: export raw tokenFormat in credentials JSON (not display-substitu…
wmathurin Aug 5, 2026
29d065c
fix(W-23721537): restartAndValidateUser must use restart() not inline…
wmathurin Aug 5, 2026
14bff4e
perf: reduce hasHost timeout from long(10s) to short(2s) — host list …
wmathurin Aug 5, 2026
0a38bfe
fix(tests): nil forceAdvancedAuthentication should mean WebView, not …
wmathurin Aug 5, 2026
31d093b
fix(tests): pass isMultiUser to switchToUserAndValidate; add isRtr to…
wmathurin Aug 5, 2026
102bc39
fix(tests): thread isRtr through validateUser; add isMultiUser to mig…
wmathurin Aug 5, 2026
55b5ed9
fix(auth): clear per-user feature flags on UI test reset
wmathurin Aug 5, 2026
e8e3011
fix(tests): fix two post-reset assertion failures in auth flow tests
wmathurin Aug 5, 2026
3cf3c42
refactor(tests): remove isRtr from restartAndValidateUser
wmathurin Aug 5, 2026
75161a2
refactor(tests): make forceAdvancedAuthentication non-nullable (Bool …
wmathurin Aug 5, 2026
fbf14b5
test(legacy): add WebServerFlow_InAppWebView variants for CA opaque s…
wmathurin Aug 5, 2026
a8ceed7
refactor: extract resetAuthFlags helper in SalesforceSDKManager
wmathurin Aug 5, 2026
8368803
fix(tests): default forceAdvancedAuthentication to false in migrateAn…
wmathurin Aug 5, 2026
89abe3d
fix(auth): preserve BW feature flag through refresh token migration
wmathurin Aug 5, 2026
e70d1d3
fix(tests): thread isRtr through restartAndValidateUser for UA flag c…
wmathurin Aug 5, 2026
4a2eed8
fix(tests): check LoginViewController nav bar, not WebView text field…
wmathurin Aug 5, 2026
bd0347e
fix(tests): login User A via WebView so BW flag is absent in testAdva…
wmathurin Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ extern NSString * const kSFAppFeatureAppAttestation;
*/
+ (void)loadPersistedFeatures:(nonnull NSSet<NSString *> *)features forUser:(nonnull SFUserAccount *)user;

#if DEBUG
/**
Clears all per-user in-memory feature flags. Intended for UI test resets only.
NOT FOR PRODUCTION USE.
*/
+ (void)resetPerUserFeaturesForUITesting;
#endif

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,12 @@ + (void)loadPersistedFeatures:(NSSet<NSString *> *)features forUser:(SFUserAccou
});
}

#if DEBUG
+ (void)resetPerUserFeaturesForUITesting {
dispatch_sync(SFSDKAppFeatureDispatchQueue, ^{
[SFSDKPerUserFeatureMarkersMap removeAllObjects];
});
}
#endif

@end
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,22 @@ NS_SWIFT_NAME(SalesforceManager)
*/
- (id <SFNativeLoginManager>)nativeLoginManager;

#if DEBUG
/**
* Resets all local SDK auth state for UI testing.
*
* Logs out all users (including async server refresh-token revocation), resets the selected
* login host to login.salesforce.com, removes persisted custom login servers, and restores
* all SalesforceSDKManager auth flags to their post-init defaults.
*
* Call once at process startup when --resetSDKForUITesting is present in launch arguments,
* after initializeSDK and before the SDK's login flow begins.
*
* NOT FOR PRODUCTION USE.
*/
+ (void)resetForUITesting NS_SWIFT_NAME(resetForUITesting());
#endif

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#import "SFSDKSalesforceSDKUpgradeManager.h"
#import <SalesforceSDKCommon/NSUserDefaults+SFAdditions.h>
#import "SFSDKWindowManager+Internal.h"
#import "SFSDKLoginHostStorage.h"

// Error constants
NSString * const kSalesforceSDKManagerErrorDomain = @"com.salesforce.sdkmanager.error";
Expand Down Expand Up @@ -228,6 +229,42 @@ + (void)initializeSDKWithClass:(Class)className {
[SalesforceSDKManager sharedManager];
}

- (void)resetAuthFlags {
self.useEphemeralSessionForAdvancedAuth = YES;
self.useWebServerAuthentication = YES;
self.useHybridAuthentication = YES;
self.useDPoP = NO;
self.sdk_forceAdvancedAuthentication = YES;
self.blockSalesforceIntegrationUser = NO;
}

#if DEBUG
+ (void)resetForUITesting {
// 1. Log out all users — clears on-disk account data, DPoP keychain keys, in-memory maps.
// Refresh-token revocation fires asynchronously in the background.
[[SFUserAccountManager sharedInstance] logoutAllUsers];

// 2. Clear per-user in-memory feature flags (RT, DP, etc.) so flags from the previous test
// do not bleed into the next one when the same user logs in again.
[SFSDKAppFeatureMarkers resetPerUserFeaturesForUITesting];

// 3. Reset selected login host to production default and persist it.
[SFUserAccountManager sharedInstance].loginHost = @"login.salesforce.com";

// 4. Remove custom login servers in-memory; save flushes the empty list to msdkUserDefaults
// (SalesforceLoginHostListPrefs) so custom hosts do not reappear on next cold start.
// Production (login.salesforce.com) and Sandbox (test.salesforce.com) are preserved.
SFSDKLoginHostStorage *storage = [SFSDKLoginHostStorage sharedInstance];
[storage removeAllLoginHosts];
[storage save];

// 5. Reset all auth flags to their -init defaults.
SalesforceSDKManager *mgr = [SalesforceSDKManager sharedManager];
[mgr resetAuthFlags];
mgr.simulatedDomainDiscoveryResult = nil;
Comment thread
wmathurin marked this conversation as resolved.
}
#endif

+ (instancetype)sharedManager {
static dispatch_once_t pred;
static SalesforceSDKManager *sdkManager = nil;
Expand Down Expand Up @@ -328,12 +365,7 @@ - (instancetype)init {
[self computeWebViewUserAgent]; // web view user agent is computed asynchronously so very first call to self.userAgentString(...) will be missing it
self.userAgentString = [self defaultUserAgentString];
self.URLCacheType = kSFURLCacheTypeEncrypted;
self.useEphemeralSessionForAdvancedAuth = YES;
self.useWebServerAuthentication = YES;
self.blockSalesforceIntegrationUser = NO;
self.useHybridAuthentication = YES;
self.useDPoP = NO;
self.sdk_forceAdvancedAuthentication = YES;
[self resetAuthFlags];
[self setupServiceConfiguration];
_snapshotViewControllers = [SFSDKSafeMutableDictionary new];
_nativeLoginViewControllers = [SFSDKSafeMutableDictionary new];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,9 @@ - (void)finalizeAuthCompletion:(SFSDKAuthSession *)authSession {
SFOAuthType completedAuthType = authSession.oauthCoordinator.authInfo.authType;
if (completedAuthType == SFOAuthTypeAdvancedBrowser) {
[SFSDKAppFeatureMarkers registerAppFeature:kSFAppFeatureSafariBrowserForLogin forUser:userAccount];
} else if (completedAuthType == SFOAuthTypeRefreshTokenMigration) {
// Migration exchanges the token but does not change how the user originally
// authenticated. Preserve the existing per-user BW flag rather than clearing it.
Comment on lines +2185 to +2187

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

} else {
[SFSDKAppFeatureMarkers unregisterAppFeature:kSFAppFeatureSafariBrowserForLogin forUser:userAccount];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
super.init()

SalesforceManager.initializeSDK()
#if DEBUG
if CommandLine.arguments.contains("--resetSDKForUITesting") {
SalesforceManager.resetForUITesting()
}
#endif
SalesforceManager.shared.appDisplayName = "Auth Flow Tester"
UserAccountManager.shared.navigationPolicyForAction = { webView, action in
if let url = action.request.url, url.absoluteString == "https://www.salesforce.com/us/company/privacy" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ struct UserCredentialsView: View {
result[CredentialsLabels.tokens] = [
CredentialsLabels.accessToken: accessToken,
CredentialsLabels.refreshToken: refreshToken,
CredentialsLabels.tokenFormat: tokenFormat,
CredentialsLabels.tokenFormat: tokenFormatRaw,
CredentialsLabels.jwt: jwt,
CredentialsLabels.authCode: authCode,
CredentialsLabels.challengeString: challengeString,
Expand Down Expand Up @@ -401,9 +401,12 @@ struct UserCredentialsView: View {
return credentials?.refreshToken ?? ""
}

private var tokenFormatRaw: String {
return credentials?.tokenFormat ?? ""
}

private var tokenFormat: String {
let value = credentials?.tokenFormat ?? ""
return value.isEmpty ? "Opaque" : value
return tokenFormatRaw.isEmpty ? "Opaque" : tokenFormatRaw
}

private var jwt: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ class LoginPageObject {

private func hasHost(host: String) -> Bool {
let row = hostRow(host: host)
return row.waitForExistence(timeout: UITestTimeouts.long)
return row.waitForExistence(timeout: UITestTimeouts.short)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class DPoPLoginTests: BaseAuthFlowTester {
staticScopeSelection: .subset,
migrationAppConfigName: .ecaJwtDpop,
migrationScopeSelection: .all,
forceAdvancedAuthentication: false,
useDPoP: true
)

Expand All @@ -131,7 +130,6 @@ class DPoPLoginTests: BaseAuthFlowTester {
staticAppConfigName: .ecaJwtDpop,
migrationAppConfigName: .ecaJwtDpopRtr,
migrationUseHybridFlow: false,
forceAdvancedAuthentication: false,
useDPoP: true
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
"Forced advanced auth on the standard server should launch the external browser")
XCTAssertFalse(isShowingInAppLoginForm(timeout: UITestTimeouts.short),
"The in-app WebView login form must NOT be shown when advanced auth is forced on")

// Never completed login — remain on the browser surface; the next launch() self-heals.
skipLogoutAtTearDown()
}

/// Flag OFF (imported explicitly), standard server.
Expand All @@ -102,20 +99,20 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
launch()

// Reach the host list (fresh launch shows the browser), pin the standard server, then
// import forceAdvancedAuthentication = false via the gear → Login Options JSON hook. A valid
// app config is imported alongside it so the WebView can load a real login page. Closing
// Login Options restarts authentication, now in the legacy WebView.
// import forceAdvancedAuthentication = false via the gear → Login Options JSON hook. No
// app config override — the default bootconfig.plist consumer key is valid for
// login.salesforce.com, so the WebView loads the real login form without importing a
// My-Domain-specific ECA key that would trigger invalid_client_id on the standard server.
// Closing Login Options restarts authentication, now in the legacy WebView.
returnToLoginHostList(expectingBrowser: true)
configureLoginHost(productionHostDisplayName)
returnToLoginHostList(expectingBrowser: true)
setForceAdvancedAuthentication(false, staticAppConfigName: .ecaOpaque)
setForceAdvancedAuthentication(false)

XCTAssertTrue(isShowingInAppLoginForm(),
"With advanced auth disabled, the standard server should use the in-app WebView login form")
XCTAssertFalse(isShowingBrowserLogin(timeout: UITestTimeouts.short),
"The external browser must NOT be shown when advanced auth is disabled")

skipLogoutAtTearDown()
}

/// Flag ON (default), `regular_auth` My Domain that does NOT opt into browser login.
Expand All @@ -125,12 +122,13 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
/// `launchLoginAndValidate`). Advanced auth always pairs with the web server flow, so this is a
/// web-server-flow login.
func testForceAdvancedAuth_MyDomainRegularHost_RemainsBrowser() throws {
// Default flag (nil) inherits the production default (advanced auth ON), so login runs in the
// external browser; validation asserts credentials and a REST round-trip.
// Pass `true` to explicitly force advanced auth ON so login runs in the external browser;
// validation asserts credentials and a REST round-trip.
launchLoginAndValidate(
loginHost: .regularAuth,
user: .first,
staticAppConfigName: .ecaOpaque
staticAppConfigName: .ecaOpaque,
forceAdvancedAuthentication: true
)
}

Expand All @@ -144,11 +142,12 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
/// Pre-fix regression this guards: the forced path created the picker with `hidesCancelButton =
/// YES` and no back control, stranding the add-user flow.
func testForceAdvancedAuth_AddAdditionalUser_BackButtonAccessible() throws {
// Log in the first user under the default flag (external browser).
// Log in the first user with advanced auth explicitly ON (external browser).
launchAndLogin(
loginHost: .regularAuth,
user: .first,
staticAppConfigName: .ecaOpaque
staticAppConfigName: .ecaOpaque,
forceAdvancedAuthentication: true
)

// Trigger Add New Account (Switch User → New User): under forced advanced auth this launches
Expand Down Expand Up @@ -187,8 +186,6 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
openLoginOptions()
XCTAssertTrue(isShowingAuthFlowTypesView(),
"The picker's Login Options entry should open the Auth Flow Types dev screen")

skipLogoutAtTearDown()
}

/// §5a/§5b parity — Flag OFF. With one user already logged in, adding another account on the
Expand All @@ -214,7 +211,5 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
"On the legacy WebView path, adding a user should show the same accessible back control")
XCTAssertTrue(isShowingLoginSettingsGear(),
"On the legacy WebView path, adding a user should show the same dev-menu gear")

skipLogoutAtTearDown()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ class LegacyLoginTests: BaseAuthFlowTester {
launchLoginAndValidate(staticAppConfigName: .caOpaque, staticScopeSelection: .all, useHybridFlow: useHybridFlow())
}

// MARK: - CA Web Server Flow Tests (In-App WebView)

/// Login with CA opaque using default scopes, web server flow, and in-app WebView (advanced auth disabled).
func testCAOpaque_DefaultScopes_WebServerFlow_InAppWebView() throws {
launchLoginAndValidate(staticAppConfigName: .caOpaque, useHybridFlow: useHybridFlow(), forceAdvancedAuthentication: false)
}

/// Login with CA opaque using subset of scopes, web server flow, and in-app WebView (advanced auth disabled).
func testCAOpaque_SubsetScopes_WebServerFlow_InAppWebView() throws {
launchLoginAndValidate(staticAppConfigName: .caOpaque, staticScopeSelection: .subset, useHybridFlow: useHybridFlow(), forceAdvancedAuthentication: false)
}

/// Login with CA opaque using all scopes, web server flow, and in-app WebView (advanced auth disabled).
func testCAOpaque_AllScopes_WebServerFlow_InAppWebView() throws {
launchLoginAndValidate(staticAppConfigName: .caOpaque, staticScopeSelection: .all, useHybridFlow: useHybridFlow(), forceAdvancedAuthentication: false)
}

// MARK: - CA User Agent Flow Tests

/// Login with CA opaque using default scopes and user agent flow.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,21 +173,23 @@ class MultiUserLoginTests: BaseAuthFlowTester {
loginHost: .regularAuth,
user: .fourth,
staticAppConfigName: .ecaOpaque,
userAppConfigName: .ecaOpaque
userAppConfigName: .ecaOpaque,
isMultiUser: true
)

// Switch back to other user
switchToUserAndValidate(
loginHost: .regularAuth,
user: .fifth,
staticAppConfigName: .ecaOpaque,
userAppConfigName: .ecaJwt,
isMultiUser: true
)

// Logout second user
logout()
}

/// First user dynamic config, second user static config, different apps, same scopes (default).
func testFirstDynamic_SecondStatic_DifferentApps() throws {
// Initial user
Expand All @@ -210,21 +212,23 @@ class MultiUserLoginTests: BaseAuthFlowTester {
loginHost: .regularAuth,
user: .fourth,
staticAppConfigName: .ecaOpaque,
userAppConfigName: .ecaJwt
userAppConfigName: .ecaJwt,
isMultiUser: true
)

// Switch back to other user
switchToUserAndValidate(
loginHost: .regularAuth,
user: .fifth,
staticAppConfigName: .ecaOpaque,
userAppConfigName: .ecaOpaque,
isMultiUser: true
)

// Logout second user
logout()
}

// MARK: - Both Users Dynamic Config

/// Both users use dynamic config, different apps, same scopes (default).
Expand All @@ -250,17 +254,19 @@ class MultiUserLoginTests: BaseAuthFlowTester {
loginHost: .regularAuth,
user: .fourth,
staticAppConfigName: .caOpaque, // not used - but using other config for validation
userAppConfigName: .ecaOpaque
userAppConfigName: .ecaOpaque,
isMultiUser: true
)

// Switch back to other user
switchToUserAndValidate(
loginHost: .regularAuth,
user: .fifth,
staticAppConfigName: .caOpaque, // not used - but using other config for validation
userAppConfigName: .ecaJwt,
isMultiUser: true
)

// Logout second user
logout()
}
Expand Down
Loading
Loading