Skip to content

Commit eb62dd4

Browse files
authored
Merge pull request #4121 from wmathurin/W-23721537-authflowtester-launch-arg-reset
feat(W-23721537): speed up AuthFlowTester UI tests via launch-arg driven SDK reset
2 parents 3b14827 + bd0347e commit eb62dd4

15 files changed

Lines changed: 240 additions & 178 deletions

File tree

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SFSDKAppFeatureMarkers.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ extern NSString * const kSFAppFeatureAppAttestation;
9999
*/
100100
+ (void)loadPersistedFeatures:(nonnull NSSet<NSString *> *)features forUser:(nonnull SFUserAccount *)user;
101101

102+
#if DEBUG
103+
/**
104+
Clears all per-user in-memory feature flags. Intended for UI test resets only.
105+
NOT FOR PRODUCTION USE.
106+
*/
107+
+ (void)resetPerUserFeaturesForUITesting;
108+
#endif
109+
102110
@end
103111

104112
NS_ASSUME_NONNULL_END

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SFSDKAppFeatureMarkers.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,12 @@ + (void)loadPersistedFeatures:(NSSet<NSString *> *)features forUser:(SFUserAccou
141141
});
142142
}
143143

144+
#if DEBUG
145+
+ (void)resetPerUserFeaturesForUITesting {
146+
dispatch_sync(SFSDKAppFeatureDispatchQueue, ^{
147+
[SFSDKPerUserFeatureMarkersMap removeAllObjects];
148+
});
149+
}
150+
#endif
151+
144152
@end

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,22 @@ NS_SWIFT_NAME(SalesforceManager)
391391
*/
392392
- (id <SFNativeLoginManager>)nativeLoginManager;
393393

394+
#if DEBUG
395+
/**
396+
* Resets all local SDK auth state for UI testing.
397+
*
398+
* Logs out all users (including async server refresh-token revocation), resets the selected
399+
* login host to login.salesforce.com, removes persisted custom login servers, and restores
400+
* all SalesforceSDKManager auth flags to their post-init defaults.
401+
*
402+
* Call once at process startup when --resetSDKForUITesting is present in launch arguments,
403+
* after initializeSDK and before the SDK's login flow begins.
404+
*
405+
* NOT FOR PRODUCTION USE.
406+
*/
407+
+ (void)resetForUITesting NS_SWIFT_NAME(resetForUITesting());
408+
#endif
409+
394410
@end
395411

396412
NS_ASSUME_NONNULL_END

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#import "SFSDKSalesforceSDKUpgradeManager.h"
4444
#import <SalesforceSDKCommon/NSUserDefaults+SFAdditions.h>
4545
#import "SFSDKWindowManager+Internal.h"
46+
#import "SFSDKLoginHostStorage.h"
4647

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

232+
- (void)resetAuthFlags {
233+
self.useEphemeralSessionForAdvancedAuth = YES;
234+
self.useWebServerAuthentication = YES;
235+
self.useHybridAuthentication = YES;
236+
self.useDPoP = NO;
237+
self.sdk_forceAdvancedAuthentication = YES;
238+
self.blockSalesforceIntegrationUser = NO;
239+
}
240+
241+
#if DEBUG
242+
+ (void)resetForUITesting {
243+
// 1. Log out all users — clears on-disk account data, DPoP keychain keys, in-memory maps.
244+
// Refresh-token revocation fires asynchronously in the background.
245+
[[SFUserAccountManager sharedInstance] logoutAllUsers];
246+
247+
// 2. Clear per-user in-memory feature flags (RT, DP, etc.) so flags from the previous test
248+
// do not bleed into the next one when the same user logs in again.
249+
[SFSDKAppFeatureMarkers resetPerUserFeaturesForUITesting];
250+
251+
// 3. Reset selected login host to production default and persist it.
252+
[SFUserAccountManager sharedInstance].loginHost = @"login.salesforce.com";
253+
254+
// 4. Remove custom login servers in-memory; save flushes the empty list to msdkUserDefaults
255+
// (SalesforceLoginHostListPrefs) so custom hosts do not reappear on next cold start.
256+
// Production (login.salesforce.com) and Sandbox (test.salesforce.com) are preserved.
257+
SFSDKLoginHostStorage *storage = [SFSDKLoginHostStorage sharedInstance];
258+
[storage removeAllLoginHosts];
259+
[storage save];
260+
261+
// 5. Reset all auth flags to their -init defaults.
262+
SalesforceSDKManager *mgr = [SalesforceSDKManager sharedManager];
263+
[mgr resetAuthFlags];
264+
mgr.simulatedDomainDiscoveryResult = nil;
265+
}
266+
#endif
267+
231268
+ (instancetype)sharedManager {
232269
static dispatch_once_t pred;
233270
static SalesforceSDKManager *sdkManager = nil;
@@ -328,12 +365,7 @@ - (instancetype)init {
328365
[self computeWebViewUserAgent]; // web view user agent is computed asynchronously so very first call to self.userAgentString(...) will be missing it
329366
self.userAgentString = [self defaultUserAgentString];
330367
self.URLCacheType = kSFURLCacheTypeEncrypted;
331-
self.useEphemeralSessionForAdvancedAuth = YES;
332-
self.useWebServerAuthentication = YES;
333-
self.blockSalesforceIntegrationUser = NO;
334-
self.useHybridAuthentication = YES;
335-
self.useDPoP = NO;
336-
self.sdk_forceAdvancedAuthentication = YES;
368+
[self resetAuthFlags];
337369
[self setupServiceConfiguration];
338370
_snapshotViewControllers = [SFSDKSafeMutableDictionary new];
339371
_nativeLoginViewControllers = [SFSDKSafeMutableDictionary new];

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,9 @@ - (void)finalizeAuthCompletion:(SFSDKAuthSession *)authSession {
21822182
SFOAuthType completedAuthType = authSession.oauthCoordinator.authInfo.authType;
21832183
if (completedAuthType == SFOAuthTypeAdvancedBrowser) {
21842184
[SFSDKAppFeatureMarkers registerAppFeature:kSFAppFeatureSafariBrowserForLogin forUser:userAccount];
2185+
} else if (completedAuthType == SFOAuthTypeRefreshTokenMigration) {
2186+
// Migration exchanges the token but does not change how the user originally
2187+
// authenticated. Preserve the existing per-user BW flag rather than clearing it.
21852188
} else {
21862189
[SFSDKAppFeatureMarkers unregisterAppFeature:kSFAppFeatureSafariBrowserForLogin forUser:userAccount];
21872190
}

native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/AppDelegate.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4040
super.init()
4141

4242
SalesforceManager.initializeSDK()
43+
#if DEBUG
44+
if CommandLine.arguments.contains("--resetSDKForUITesting") {
45+
SalesforceManager.resetForUITesting()
46+
}
47+
#endif
4348
SalesforceManager.shared.appDisplayName = "Auth Flow Tester"
4449
UserAccountManager.shared.navigationPolicyForAction = { webView, action in
4550
if let url = action.request.url, url.absoluteString == "https://www.salesforce.com/us/company/privacy" {

native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ struct UserCredentialsView: View {
282282
result[CredentialsLabels.tokens] = [
283283
CredentialsLabels.accessToken: accessToken,
284284
CredentialsLabels.refreshToken: refreshToken,
285-
CredentialsLabels.tokenFormat: tokenFormat,
285+
CredentialsLabels.tokenFormat: tokenFormatRaw,
286286
CredentialsLabels.jwt: jwt,
287287
CredentialsLabels.authCode: authCode,
288288
CredentialsLabels.challengeString: challengeString,
@@ -401,9 +401,12 @@ struct UserCredentialsView: View {
401401
return credentials?.refreshToken ?? ""
402402
}
403403

404+
private var tokenFormatRaw: String {
405+
return credentials?.tokenFormat ?? ""
406+
}
407+
404408
private var tokenFormat: String {
405-
let value = credentials?.tokenFormat ?? ""
406-
return value.isEmpty ? "Opaque" : value
409+
return tokenFormatRaw.isEmpty ? "Opaque" : tokenFormatRaw
407410
}
408411

409412
private var jwt: String {

native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/PageObjects/LoginPageObject.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ class LoginPageObject {
208208
return app.webViews.webViews.webViews.textFields.firstMatch.waitForExistence(timeout: timeout)
209209
}
210210

211+
/// True when the in-app login view controller (`SFLoginViewController`) is showing, detected
212+
/// by its "Log In" navigation bar title. This appears as soon as the view controller is
213+
/// presented — before the WKWebView has loaded the login page — so it is faster and more
214+
/// reliable than `isShowingInAppLoginForm()` for asserting that the SDK chose the in-app
215+
/// WebView modality rather than the external browser. Pass `UITestTimeouts.short` for the
216+
/// negative assertion (not showing).
217+
func isShowingLoginViewController(timeout: TimeInterval = UITestTimeouts.long) -> Bool {
218+
return loginNavigationBar().waitForExistence(timeout: timeout)
219+
}
220+
211221
/// True when the Settings gear is present on the current login nav bar. Under forced advanced
212222
/// auth the gear lives on the host list; on the legacy path it lives on the in-app WebView
213223
/// ("Log In"). Both expose the same accessibility identifier "settings".
@@ -434,7 +444,7 @@ class LoginPageObject {
434444

435445
private func hasHost(host: String) -> Bool {
436446
let row = hostRow(host: host)
437-
return row.waitForExistence(timeout: UITestTimeouts.long)
447+
return row.waitForExistence(timeout: UITestTimeouts.short)
438448
}
439449
}
440450

native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/DPoPLoginTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ class DPoPLoginTests: BaseAuthFlowTester {
112112
staticScopeSelection: .subset,
113113
migrationAppConfigName: .ecaJwtDpop,
114114
migrationScopeSelection: .all,
115-
forceAdvancedAuthentication: false,
116115
useDPoP: true
117116
)
118117

@@ -131,7 +130,6 @@ class DPoPLoginTests: BaseAuthFlowTester {
131130
staticAppConfigName: .ecaJwtDpop,
132131
migrationAppConfigName: .ecaJwtDpopRtr,
133132
migrationUseHybridFlow: false,
134-
forceAdvancedAuthentication: false,
135133
useDPoP: true
136134
)
137135

native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/Tests/ForceAdvancedAuthTests.swift

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,6 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
8888
"Forced advanced auth on the standard server should launch the external browser")
8989
XCTAssertFalse(isShowingInAppLoginForm(timeout: UITestTimeouts.short),
9090
"The in-app WebView login form must NOT be shown when advanced auth is forced on")
91-
92-
// Never completed login — remain on the browser surface; the next launch() self-heals.
93-
skipLogoutAtTearDown()
9491
}
9592

9693
/// Flag OFF (imported explicitly), standard server.
@@ -102,20 +99,21 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
10299
launch()
103100

104101
// Reach the host list (fresh launch shows the browser), pin the standard server, then
105-
// import forceAdvancedAuthentication = false via the gear → Login Options JSON hook. A valid
106-
// app config is imported alongside it so the WebView can load a real login page. Closing
107-
// Login Options restarts authentication, now in the legacy WebView.
102+
// import forceAdvancedAuthentication = false via the gear → Login Options JSON hook. No
103+
// app config override — the bootconfig.plist consumer key is an org-specific test CA that
104+
// may not resolve on login.salesforce.com, so we don't wait for the login page to finish
105+
// loading. Instead we assert that the SDK chose the in-app WebView modality (the "Log In"
106+
// nav bar appears as soon as SFLoginViewController is presented, before the WKWebView
107+
// completes the page load). Closing Login Options restarts authentication, now in WebView.
108108
returnToLoginHostList(expectingBrowser: true)
109109
configureLoginHost(productionHostDisplayName)
110110
returnToLoginHostList(expectingBrowser: true)
111-
setForceAdvancedAuthentication(false, staticAppConfigName: .ecaOpaque)
111+
setForceAdvancedAuthentication(false)
112112

113-
XCTAssertTrue(isShowingInAppLoginForm(),
114-
"With advanced auth disabled, the standard server should use the in-app WebView login form")
113+
XCTAssertTrue(isShowingLoginViewController(),
114+
"With advanced auth disabled, the standard server should present the in-app login view controller")
115115
XCTAssertFalse(isShowingBrowserLogin(timeout: UITestTimeouts.short),
116116
"The external browser must NOT be shown when advanced auth is disabled")
117-
118-
skipLogoutAtTearDown()
119117
}
120118

121119
/// Flag ON (default), `regular_auth` My Domain that does NOT opt into browser login.
@@ -125,12 +123,13 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
125123
/// `launchLoginAndValidate`). Advanced auth always pairs with the web server flow, so this is a
126124
/// web-server-flow login.
127125
func testForceAdvancedAuth_MyDomainRegularHost_RemainsBrowser() throws {
128-
// Default flag (nil) inherits the production default (advanced auth ON), so login runs in the
129-
// external browser; validation asserts credentials and a REST round-trip.
126+
// Pass `true` to explicitly force advanced auth ON so login runs in the external browser;
127+
// validation asserts credentials and a REST round-trip.
130128
launchLoginAndValidate(
131129
loginHost: .regularAuth,
132130
user: .first,
133-
staticAppConfigName: .ecaOpaque
131+
staticAppConfigName: .ecaOpaque,
132+
forceAdvancedAuthentication: true
134133
)
135134
}
136135

@@ -144,11 +143,12 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
144143
/// Pre-fix regression this guards: the forced path created the picker with `hidesCancelButton =
145144
/// YES` and no back control, stranding the add-user flow.
146145
func testForceAdvancedAuth_AddAdditionalUser_BackButtonAccessible() throws {
147-
// Log in the first user under the default flag (external browser).
146+
// Log in the first user with advanced auth explicitly ON (external browser).
148147
launchAndLogin(
149148
loginHost: .regularAuth,
150149
user: .first,
151-
staticAppConfigName: .ecaOpaque
150+
staticAppConfigName: .ecaOpaque,
151+
forceAdvancedAuthentication: true
152152
)
153153

154154
// Trigger Add New Account (Switch User → New User): under forced advanced auth this launches
@@ -187,8 +187,6 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
187187
openLoginOptions()
188188
XCTAssertTrue(isShowingAuthFlowTypesView(),
189189
"The picker's Login Options entry should open the Auth Flow Types dev screen")
190-
191-
skipLogoutAtTearDown()
192190
}
193191

194192
/// §5a/§5b parity — Flag OFF. With one user already logged in, adding another account on the
@@ -214,7 +212,5 @@ class ForceAdvancedAuthTests: BaseAuthFlowTester {
214212
"On the legacy WebView path, adding a user should show the same accessible back control")
215213
XCTAssertTrue(isShowingLoginSettingsGear(),
216214
"On the legacy WebView path, adding a user should show the same dev-menu gear")
217-
218-
skipLogoutAtTearDown()
219215
}
220216
}

0 commit comments

Comments
 (0)