@W-23290467: [MSDK Android] Push deregistration for one user can deregister all users after app upgrade from a pre-14.0 build - #2957
Conversation
Manual verification — on-device legacy→fixed upgrade testIn addition to the automated instrumented tests ( Why a special harness was neededThe defect only manifests when a push work request enqueued by an older build (which persisted the full account under the legacy
The mechanism that makes the test deterministic: because the legacy deregister request requires network, performing the logout offline leaves the job PENDING — it never runs — so it can be carried across the upgrade and then released to the new worker by restoring connectivity. Procedure and results1. Seed two accounts on the legacy build. Logged in User A, then added User B (two distinct users). Both registered for push successfully: 2. Queue a single-user deregister for User A, offline. Enabled airplane mode, switched to User A, then logged A out. The legacy build enqueued a deregister work request carrying A's legacy 3. In-place upgrade to the fixed build, still offline. 4. Restore connectivity → the fixed worker runs the pending legacy job. Disabled airplane mode. The pending deregister ran against the new worker: The worker migrated the legacy 5. Bystander survived. User B remained authenticated and registered throughout — the failed single-user deregister did not touch it. This is the fix: pre-fix, this job would have widened to every authenticated user and deregistered B. 6. Fixed build's push path remains healthy. On the fixed build, a re-registration resolved to exactly the one authenticated user and succeeded, and a test push was received: Confirming the change does not regress normal register / notification-delivery behavior. Outcome
The on-device result matches the deterministic instrumented tests: the migration correctly scopes a legacy single-user deregister to the intended user only. This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce. |
Job Summary for GradlePull Request :: test-android |
wmathurin
left a comment
There was a problem hiding this comment.
Review findings — W-23290467: push deregistration wrong-user bug after upgrade
[suggestion] PushNotificationsRegistrationChangeWorkerTest.kt — The regression test constructs the legacy blob using UserAccount.toJson(). Worth confirming that this method produces the exact same JSON structure that the pre-14.0 SDK wrote into the work request payload — if the field names or structure differ between SDK versions, the test would pass but the production migration path would still fail for real upgraded devices.
[question] resolveTargetAccounts() — If the legacy blob contains a valid orgId/userId but that user no longer exists in UserAccountManager (e.g. they were removed between the upgrade and the worker running), the method returns Fail and the deregistration is silently dropped. Is that the intended behavior, or should it log a warning? A silent no-op means the push token for that user remains registered on the server indefinitely.
|
Good call-out. Two things pin the fidelity down: 1. The unit test uses the same serializer the legacy SDK used. The pre-14.0 enqueue path built the payload with 2. The on-device test didn't use a synthetic blob at all — it used a real pre-migration build. For the manual verification (posted above), the legacy job was produced by building and running the last pre-migration commit ( So the field names/structure match by construction (same This wasn't a hands-off verification — there was substantial human-in-the-loop work behind it: reviewing the unit tests, and manually driving the in-app scenario across the old and new builds through the actual upgrade (seeding the accounts, staging the offline deregister, performing the This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce. |
|
Intended — and it's actually the central case this fix targets, not just an edge. A single-user deregister is enqueued because that user logged out, so by the time a legacy pre-14.0 deregister job survives an in-place upgrade and runs against the new worker, that account is normally already gone from It's worth adding why the client can't just complete the deregister anyway: pre-14.0 it could, but only because it persisted the full account — including the auth/refresh token — in WorkManager's unencrypted storage and rebuilt a REST client straight from that blob. Removing those plaintext credentials was the whole point of the preceding hardening work; once we re-resolve from secure storage instead, a departed user has no token to authenticate a deregister, so fail-closed is the honest outcome. The silent no-op is the accepted cost of not keeping credentials at rest, not a regression. On the "token stays registered on the server indefinitely" concern — I went and checked the server side (the Given that, proactive client-side cleanup of a departed user's registration would be redundant with the server's own age-out — so I've kept this fix scoped to the wrong-user defect rather than adding anything there. This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce. |
…o deregister targets only the intended user (Resolve target accounts from legacy USER_ACCOUNT blob reading only org/user id; add instrumented migration tests)
8f28659 to
ce2c211
Compare
|
Merging with This PR is being merged with admin privileges because the one remaining red check, The relevant checks pass. The failing check is a dev-wide, pre-existing issue, not a regression from this PR.
Every failing test is in the This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce. |
9616951
into
forcedotcom:dev
What & why
PushNotificationsRegistrationChangeWorkerresolves its target account from discreteORG_ID/USER_IDinput keys, treating both absent as "all authenticated users" (the periodic re-register path). But an app upgrading in place from a pre-14.0 build can carry a queued push work request whose payload uses the oldUSER_ACCOUNTblob key — with noORG_ID/USER_ID. When the new worker runs that job, it hits the absent-identifiers branch and widens scope: a single-user deregister enqueued by the old build deregisters every authenticated user.The fix
Account selection is extracted into a
@VisibleForTesting internal fun resolveTargetAccounts(): TargetAccountsthat runs before the register/deregister action. It migrates a legacyUSER_ACCOUNTpayload by reading only the org id and user id from the blob — never the auth token, refresh token, or session cookies it also carries — then re-resolves the account from secure storage. This preserves the worker's data-minimization design: no plaintext credentials are re-persisted to WorkManager's unencrypted storage.Resolution outcomes are a small sealed type:
Result.failure(). The work fails rather than silently widening scope; this is safe because the SDK re-enqueues registration work with REPLACE on the next foreground/login.Testing
Instrumented suite (
PushNotificationsRegistrationChangeWorkerTest, emulator, no FCM) —resolveTargetAccounts()+TargetAccountsat 100% line/branch/instruction coverage:USER_ACCOUNT-only deregister, users A+B seeded → targets only A (written first; fails against pre-fix code).USER_ACCOUNT-only register → resolves to the one user.USER_ACCOUNT→failure()(no widening).Manual on-device verification (the path automated tests model but cannot exercise across a real process upgrade) — see the manual-test comment below: a legacy build enqueues a single-user deregister offline (pinned PENDING by the job's
CONNECTEDconstraint), an in-placeinstall -rupgrade preserves the WorkManager DB, and on restored connectivity the fixed worker runs the migrated job →resolveTargetAccounts … → FAIL,Worker result FAILURE, with the bystander account left registered and still receiving pushes.Notes
@VisibleForTestingseam only;PushService's surface is untouched.This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.