Skip to content

@W-23075124@: Add community login UI tests - #3055

Draft
wmathurin wants to merge 2 commits into
forcedotcom:devfrom
wmathurin:community-login-tests
Draft

wmathurin wants to merge 2 commits into
forcedotcom:devfrom
wmathurin:community-login-tests

Conversation

@wmathurin

@wmathurin wmathurin commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Draft: the community is now set up in our test1 test org (authflowtesting.msdk.sdb38.com, W-24381845). The suite is being run and fixed against it, and this PR stays a draft until all 12 tests pass there. CI still skips the suite until community_auth is added to the CI ui_test_config.json.

Depends on #3054. This branch is based on fix-dpop-nonce-cross-host. Without that fix, DPoP logins against a community host fail on Android. Until #3054 merges, this PR also shows the #3054 commits; the changes specific to this PR are the commits on top of it.

Summary

Adds CommunityLoginTests to the AuthFlowTester UI tests. It logs in as a community (Experience Cloud site) user through the community URL, with and without DPoP, across these scenarios:

  • hybrid and non-hybrid flow
  • refresh-token rotation
  • app restart
  • logout and relogin
  • DPoP upgrade and downgrade
  • multi-user isolation against a regular org user
  • the in-app WebView login

The community is https://authflowtestingmsdksdb38.test1.my.pc-rnd.site.com/customerportal, in the test1 org. These match the iOS suite (forcedotcom/SalesforceMobileSDK-iOS#4184). Spec/plan: SalesforceMobileSDK-Workspace specs/W-23075121-W-23075124-community-login-tests/.

Changes

  • ui_test_config.json.sample:
    • A new community_auth login host with user tandroid@community.authflowtesting.msdk.sdb38.com. No new app.
    • Also fixes the sample to match iOS: removes a trailing comma that made it invalid JSON, adds loginPoolHost, and uses placeholder org URLs.
  • servers.xml (AuthFlowTester): a UITests Community entry for the test1 org community, so the tests select it from the server list like the other test hosts.
  • CommunityLoginTests.kt: 12 scenarios. Each uses the same existing app as its DPoPLoginTests / RTRLoginTests equivalent: ECA_OPAQUE, ECA_JWT_DPOP, ECA_JWT_DPOP_RTR and ECA_JWT.
  • Skipping: the suite skips (JUnit assumption) when community_auth isn't configured, so CI is unaffected.
  • Test helpers:
    • Community hosts are reported as login server L5 (Other), not L4 (My Domain), and the expected user-agent marker now reflects that.
    • COMMUNITY_AUTH is handled in tapAllowAfterLogin.
    • switchToUserAndValidateUser now forwards the login host.
    • The Custom Tab "Log in" button match is case-insensitive.
  • README.md: documents the community tests and the new config entry.

Testing

  • The build passes. The suite skips cleanly (AssumptionViolatedException) when community_auth isn't configured.
  • Against the test1 org community: in progress. Results will be added here.

Community/Experience Cloud resource servers (identity, REST) never issue
their own DPoP-Nonce challenge; they hard-reject a proof with no nonce
instead. The client must reuse the nonce most recently issued at the
/token endpoint. DPoPNonceCache was keyed strictly by
(credentialsIdentifier, host) with no fallback, so a nonce harvested for
the login/token host was never available when building a proof for a
different resource host, causing community DPoP logins to fail.

DPoPNonceCache.get() now falls back to the most recently stored nonce for
the credentialsIdentifier (any host) when there is no entry for the exact
host. An exact host match always takes precedence. clear()/clearAll() also
remove the fallback entry. This mirrors the credential-scoped fallback
already used by the iOS implementation.

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce left a comment

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.

Scoped this review to the top commit (the one specific to this PR, on top of the
fix-dpop-nonce-cross-host branch it's stacked on) — not re-reviewing the DPoP nonce-cache
change underneath it.

Skip mechanism — verified, works as described. Pulled the actual JUnit XML from the live
ui-tests-pr run rather than trusting the PR description: all 12 CommunityLoginTests
scenarios report skipped="12" with
org.junit.AssumptionViolatedException: community_auth login host not present in ui_test_config.json; skipping community login tests at CommunityLoginTests.kt:81, on a shard
with failures="0". CI is genuinely unaffected by this suite today.

I also tried to break the gate itself rather than just confirming today's happy path: hasLoginHost
(UITestConfig.kt:97-99) only checks that a LoginHost entry with a matching name exists in
loginHosts — it doesn't check that entry has any configured users. users has no default, so a
community_auth block missing that key entirely fails at JSON decode time, for every test, not
just these — but a community_auth block that's present with an empty or too-short users list
would pass hasLoginHost and assumeTrue cleanly, then crash with an IndexOutOfBoundsException
on getUser's users[knownUserConfig.ordinal] instead of skipping. See the Requested Changes
below.

One thing worth asking about while we're looking at that run: 8 of the 12 skipped scenarios take
45-47 seconds to reach the skip, while the other 4 take about 2 seconds — same @Before, same
one-line assumeTrue. That's roughly 6 minutes of wall-clock per CI run spent on tests that
should be a no-op. I couldn't find a code-level reason for the split (no other test class in this
suite uses the assumeTrue-skip pattern to compare against), so it may just be Android Test
Orchestrator's per-test relaunch overhead rather than anything in this file — flagging as a
question rather than a finding.

Both currently-failing CI checks look inherited from the stacked commit, not this one. The
one unit-tests-pr (SalesforceSDK) failure is AuthenticationUtilitiesIntegrationUserTest
asserting on pre-redirect-host nonce storage — that's nonce-cache behavior this commit doesn't
touch. The ui-tests-pr failures are concentrated in DPoP-flavored assertions in
DPoPLoginTests/MultiUserLoginTests, which also doesn't match anything in this commit's diff
(this commit never touches production nonce/auth code — just test helpers). Both read as
carried over from the DPoP nonce-reuse fix underneath this branch (or pre-existing harness
flake, in the MultiUserLoginTests username-timeout case) rather than something to resolve here.

Shared-helper regression check. Read AuthFlowTest.kt's three new COMMUNITY_AUTH branches
and the switchToUser call-site change directly, since the PR touches helpers that
DPoPLoginTests/RTRLoginTests also depend on:

  • The three expectedLMarker branches only add a new case when
    knownLoginHostConfig == COMMUNITY_AUTH; every existing call site passes REGULAR_AUTH or
    ADVANCED_AUTH, so the existing else branches are unreached by this change. No behavior
    change for existing suites.
  • switchToUserAndValidateUser now forwards knownLoginHostConfig into
    app.switchToUser(...), whose second parameter already defaulted to REGULAR_AUTH before this
    PR. Existing callers that rely on the default get the same value passed explicitly — no
    behavior change.
  • The Custom Tab button match change (textContains("Log In") →
    textMatches("(?i).*log in.*")) is a strict superset of the old match (same literal text,
    case-insensitive), so it can't stop matching a button the old code already found.

No regression found in any of the three.

Requested Changes

  1. CommunityLoginTests.kt:412-418 (and the equivalent legs elsewhere in the file) — none of
    the 12 scenarios assert that the refresh/API request actually targets the community host. The
    closest check is the FEATURE_LOGIN_SERVER_OTHER (L5) user-agent marker added in
    AuthFlowTest.kt:953, which confirms the SDK's login-server classification telemetry, not
    the literal resolved request host. The app's own UI already exposes this as a readable node —
    UserCredentialsView.kt renders both Instance URL and API Instance URL
    (currentUser?.instanceServer / apiInstanceServer) — and validateOAuthValues/
    validateSIDs already read sibling domain nodes (CONTENT_DOMAIN, LIGHTNING_DOMAIN) the
    same way. Since the whole point of this suite is confirming the refresh leg actually lands on
    the community host rather than silently falling back to the login pool, consider adding an
    explicit assertion against the Instance URL node, or note in the class doc why the L5-marker
    check is treated as sufficient in its place.

  2. UITestConfig.kt:97-99 — hasLoginHost only checks that a LoginHost entry with the
    right name exists, not that it has any configured users. A community_auth entry present but
    with an empty (or shorter than expected) users list would pass the assumeTrue gate and then
    crash in getUser with an IndexOutOfBoundsException rather than skipping cleanly — the same
    failure mode the skip gate exists to avoid. Consider having hasLoginHost also check
    users.isNotEmpty() (or whatever minimum the caller needs), so a partially-provisioned host
    fails closed into a skip instead of a crash.

Open Questions

  1. The ui_test_config.json.sample change also fixes a pre-existing issue unrelated to community
    coverage — a trailing comma that made the sample invalid JSON, plus placeholder org URLs
    replacing real-looking subdomains. Worth confirming this bundling is intentional rather than
    something that should be a separate, smaller PR — it doesn't block this one either way.
  2. The 45-47s vs. ~2s skip-duration split noted above — is this expected Test Orchestrator
    overhead, or worth a closer look before this suite is running on every PR permanently?

This review was drafted by an AI agent on behalf of @JohnsonEricAtSalesforce.

isJwt = true,
)
app.validateOAuthValues(knownAppConfig = ECA_JWT_DPOP, scopeSelection = EMPTY)
assertRevokeAndRefreshWorks(

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.

This multi-host scenario (and the other 11 in this file) confirms the refresh/revoke cycle
succeeds and the user-agent carries the L5 "Other" marker, but doesn't assert that the
request actually targeted the community host rather than, say, a silent fallback to the
login pool. UserCredentialsView.kt already exposes Instance URL / API Instance URL as
readable nodes (the same way CONTENT_DOMAIN/LIGHTNING_DOMAIN are read in
validateOAuthValues/validateSIDs) — worth asserting against one of those directly here.

This review was drafted by an AI agent on behalf of @JohnsonEricAtSalesforce.

useLoginPoolHost -> Features.FEATURE_LOGIN_SERVER_PRODUCTION
// community_auth is a *.my.site.com Experience Cloud site, not a *.my.salesforce.com
// My Domain host, so the SDK classifies it L5 (Other) rather than L4 (My Domain).
knownLoginHostConfig == COMMUNITY_AUTH -> Features.FEATURE_LOGIN_SERVER_OTHER

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.

This marker confirms the SDK's login-server classification telemetry (L5 vs. L4), which is a
reasonable proxy but not the same as asserting the actual resolved request host — see the
note on CommunityLoginTests.kt's multi-host scenario.

This review was drafted by an AI agent on behalf of @JohnsonEricAtSalesforce.

// via Assume.assumeTrue(testConfig.hasLoginHost(...)) rather than fail.
fun hasLoginHost(knownLoginHostConfig: KnownLoginHostConfig): Boolean = loginHosts.any {
(name, _, _) -> name == knownLoginHostConfig.name.toLowerCase(Locale.current)
}

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.

This only checks that a LoginHost entry with this name exists, not that it has any
configured users. A community_auth entry present with an empty or too-short users list
would pass this check and assumeTrue, then crash in getUser with an
IndexOutOfBoundsException instead of skipping cleanly — worth also checking
users.isNotEmpty() here so a partially-provisioned host fails closed into a skip.

This review was drafted by an AI agent on behalf of @JohnsonEricAtSalesforce.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants