Skip to content

fix: Reconcile local authData on anonymous-to-email conversion#1136

Open
chadpav wants to merge 3 commits intoparse-community:masterfrom
chadpav:fix/anonymous-link-cleanup
Open

fix: Reconcile local authData on anonymous-to-email conversion#1136
chadpav wants to merge 3 commits intoparse-community:masterfrom
chadpav:fix/anonymous-link-cleanup

Conversation

@chadpav
Copy link
Copy Markdown
Contributor

@chadpav chadpav commented Apr 28, 2026

Issue

No existing issue; problem described below.

Approach

When a persisted anonymous user has its username (and typically password) set and is saved, parseUser.save() issues a PUT /classes/_User/:objectId carrying only the dirty fields. The Dart SDK treats this as a generic property update — it neither emits an authData unlink signal in the request body nor cleans up the local cache after the save. As a result, parseUser.authData continues to contain { anonymous: { id: '...' } } after a successful conversion and is persisted to disk via _onResponseSuccess in that stale shape.

The iOS Parse SDK handles this client-side via two helpers in PFUser.m:

  1. stripAnonymity (PFUser.m:593-607): the username setter writes authData[anonymous] = NSNull into the local user. The PUT body re-injects the map, so the request carries authData: { anonymous: null } — Parse Server's documented unlink signal for that provider.
  2. cleanUpAuthData (PFUser.m:300-313): after a successful save, the local authData has any null/NSNull entries stripped, then PFCurrentUserController.saveCurrentObjectAsync: persists the cleaned user.

This PR ports the same pattern to the Dart SDK as two private helpers in ParseUser:

  • _stripAnonymity() is invoked from the username setter (matching iOS — only username triggers the strip; password/email setters are unchanged). On a persisted user it sets authData['anonymous'] = null locally; on a lazy user (no objectId) it removes the entry outright.
  • _cleanUpAuthData() is invoked from save() and update() between the response merge and _onResponseSuccess(), removing null entries from local authData.

A dartdoc paragraph was added to loginAnonymous documenting the conversion path, which previously had no inline coverage in the Dart SDK source.

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)

Behavior change disclosure

Two observable changes for the anonymous-conversion case:

  1. Wire: when objectId is set and authData contains an anonymous entry, setting username and saving now produces a PUT body that includes authData: { anonymous: null }. Previously no authData field was sent for that save. Standard Parse Server treats the null as the unlink request it always was; custom beforeSave triggers on _User that explicitly reject null authData.anonymous would behave differently.
  2. Local cache: setting username on an anonymous-with-objectId ParseUser now mutates the local authData map (writes null for the anonymous key). After a successful save, the entry is removed entirely. Code that inspects parseUser.authData between .username = ... and .save() and expects the original anonymous payload will observe a difference.

The commit prefix is fix: (patch bump under semantic-release). Rationale:

  • No public API changes — no signatures changed, no methods removed, no new public surface.
  • The previous behavior was an undocumented divergence between client cache and server state on the conversion path.
  • The equivalent client-side behavior has shipped in the iOS Parse SDK for years.

If maintainers prefer a stronger version signal, the prefix can be amended to fix!: to trigger a major bump.

Documentation follow-up (out of scope for this PR)

This PR adds a dartdoc paragraph to loginAnonymous covering the conversion pattern. The companion user guide at https://docs.parseplatform.org/dart/guide/ (separate repo) has no section equivalent to the iOS guide's "Linking Anonymous Users". Mirroring that section into the Dart guide is a natural follow-up once this PR lands.

Tests

Added packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart with 6 tests:

  1. Setting username on a persisted anonymous user marks authData['anonymous'] = null locally.
  2. After a successful save, user.authData no longer contains anonymous.
  3. The PUT request body carries authData: { anonymous: null }.
  4. Non-anonymous authData entries (e.g. facebook) survive the conversion.
  5. On a lazy (no objectId) user, setting username removes the anonymous entry without leaving a null marker.
  6. Regression: setting username on a non-anonymous user does not synthesize authData in the request body.

All 212 existing tests in packages/dart/test/ continue to pass.

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved authentication data reconciliation during username assignment.
    • Enhanced cleanup of authentication provider entries during save/update operations.
    • Expanded documentation for anonymous login functionality.
  • Tests

    • Added comprehensive test coverage for authentication data conversion and cleanup workflows.

Setting username on a persisted anonymous user and calling save() now
unlinks the anonymous provider server-side and leaves the local
authData consistent with the cleaned server record — without requiring
a follow-up GET.

Previously, the save's PUT response carried only the dirty fields
written by the client (no authData), so the additive response merge
in _handleSingleResult could not reconcile the local authData copy.
Apps had to follow up with GET /users/me to refresh local state.

This mirrors iOS PFUser's stripAnonymity + cleanUpAuthData pattern.
No public API changes; the new helpers are private.
@parse-github-assistant
Copy link
Copy Markdown

parse-github-assistant Bot commented Apr 28, 2026

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

Adds local reconciliation for anonymous auth data in ParseUser: setting username strips or nulls anonymous provider entries, and post-save/update cleanup removes staged nulls. Tests added to validate conversion, cleanup, and request serialization.

Changes

Cohort / File(s) Summary
Anonymous Auth Data Stripping
packages/dart/lib/src/objects/parse_user.dart
username setter now calls _stripAnonymity() to remove or nullify authData['anonymous']. Introduces _stripAnonymity() and _cleanUpAuthData() to stage and fully remove anonymous-provider nullifications. save() and update() call cleanup after successful server writes.
Anonymous Conversion Tests
packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart
Adds comprehensive tests for anonymous→credential conversion: persisted and unpersisted anonymous users, staging of anonymous: null, cleanup after save()/update(), preservation of other providers, and verification of PUT request payloads.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Engage In Review Feedback ❓ Inconclusive Cannot verify engagement with GitHub PR review feedback comments without access to the GitHub PR interface containing review comments and discussion threads. Access the GitHub PR directly to review PR comments, discussion threads, and timeline to verify whether review feedback exists and whether the user engaged appropriately.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title begins with the required 'fix:' prefix and accurately summarizes the main change: reconciling local authData when an anonymous user converts to email authentication.
Description check ✅ Passed The pull request description comprehensively addresses all required template sections: Issue (problem described), Approach (detailed explanation of changes), and Tasks (both marked complete with checkbox ticks).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed Security analysis reveals no problematic code patterns creating vulnerabilities. Implementation safely manages authentication state with proper type safety and null checks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 86.95652% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.25%. Comparing base (785f76b) to head (64ec48a).

Files with missing lines Patch % Lines
packages/dart/lib/src/objects/parse_user.dart 86.95% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1136      +/-   ##
==========================================
+ Coverage   44.42%   45.25%   +0.83%     
==========================================
  Files          62       62              
  Lines        3730     3752      +22     
==========================================
+ Hits         1657     1698      +41     
+ Misses       2073     2054      -19     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart (1)

63-105: Please add an update() variant of this regression.

ParseUser.update() now has its own cleanup hook too, but the suite only exercises save(). A matching update() case would keep those two paths from drifting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`
around lines 63 - 105, Add a parallel test that mirrors the existing "after a
successful save..." case but invokes ParseUser.update() instead of save(); use
anonymousUserWithObjectId() to create the user, set username/password the same
way, stub the same network response (matching the current mocked call pattern -
e.g. client.put/putPath or client.patch if update uses PATCH) and assert that
user.authData no longer contains 'anonymous' after a successful update response;
include the same expect checks and reason text so update()'s cleanup hook is
covered the same as save().
packages/dart/lib/src/objects/parse_user.dart (1)

539-545: Drop empty authData in the unsaved-user branch.

If anonymous was the only provider, Lines 539-545 leave authData as {} in local state. Removing keyVarAuthData entirely here would keep lazy users fully reconciled instead of carrying a synthetic empty map forward.

♻️ Proposed change
   if (objectId == null) {
     authData.remove(_keyAuthAnonymous);
+    if (authData.isEmpty) {
+      _objectData.remove(keyVarAuthData);
+      _unsavedChanges.remove(keyVarAuthData);
+      return;
+    }
   } else {
     authData[_keyAuthAnonymous] = null;
   }
   _unsavedChanges[keyVarAuthData] = authData;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dart/lib/src/objects/parse_user.dart` around lines 539 - 545, The
branch that updates auth providers leaves an empty map in
_unsavedChanges[keyVarAuthData] when anonymous was the only provider; update the
logic in the function handling objectId/authData (referencing objectId,
authData, _keyAuthAnonymous, _unsavedChanges, keyVarAuthData) so that after
removing the anonymous provider you check if authData.isEmpty and, if so, remove
keyVarAuthData from _unsavedChanges instead of assigning an empty map; otherwise
keep assigning authData as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/dart/lib/src/objects/parse_user.dart`:
- Around line 539-545: The branch that updates auth providers leaves an empty
map in _unsavedChanges[keyVarAuthData] when anonymous was the only provider;
update the logic in the function handling objectId/authData (referencing
objectId, authData, _keyAuthAnonymous, _unsavedChanges, keyVarAuthData) so that
after removing the anonymous provider you check if authData.isEmpty and, if so,
remove keyVarAuthData from _unsavedChanges instead of assigning an empty map;
otherwise keep assigning authData as before.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`:
- Around line 63-105: Add a parallel test that mirrors the existing "after a
successful save..." case but invokes ParseUser.update() instead of save(); use
anonymousUserWithObjectId() to create the user, set username/password the same
way, stub the same network response (matching the current mocked call pattern -
e.g. client.put/putPath or client.patch if update uses PATCH) and assert that
user.authData no longer contains 'anonymous' after a successful update response;
include the same expect checks and reason text so update()'s cleanup hook is
covered the same as save().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bfa836a-98b4-4656-84cf-277a08836115

📥 Commits

Reviewing files that changed from the base of the PR and between 785f76b and 0860324.

📒 Files selected for processing (2)
  • packages/dart/lib/src/objects/parse_user.dart
  • packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart

coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 28, 2026
chadpav added 2 commits April 28, 2026 09:43
save() and update() both run _cleanUpAuthData() in their success branch
but were patched independently. Add a parallel test that invokes
update() instead of save() so a future regression in either path is
caught.

Also tightened test descriptions to focus on client-side behavior.
On the lazy (no objectId) path of _stripAnonymity, removing the
anonymous provider could leave an empty map in _unsavedChanges
under keyVarAuthData. The empty map then serialized into the POST
body via toJson(forApiRQ: true) on signUp, sending wire noise the
SDK shouldn't emit. Mirrors the isEmpty handling already present
in _cleanUpAuthData.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/dart/lib/src/objects/parse_user.dart (1)

551-568: Consider extracting duplicated null-pruning into a tiny helper.

_cleanUpAuthData() repeats the same “remove null providers, drop map if empty” flow for _objectData and _unsavedChanges. A helper would reduce drift risk.

♻️ Optional refactor sketch
+  void _pruneNullAuthProviders(Map<String, dynamic> map) {
+    map.removeWhere((_, dynamic value) => value == null);
+  }
+
   void _cleanUpAuthData() {
     final Map<String, dynamic>? authData =
         _objectData[keyVarAuthData] as Map<String, dynamic>?;
     if (authData != null) {
-      authData.removeWhere((_, dynamic value) => value == null);
+      _pruneNullAuthProviders(authData);
       if (authData.isEmpty) {
         _objectData.remove(keyVarAuthData);
       }
     }
     final Map<String, dynamic>? dirty =
         _unsavedChanges[keyVarAuthData] as Map<String, dynamic>?;
     if (dirty != null) {
-      dirty.removeWhere((_, dynamic value) => value == null);
+      _pruneNullAuthProviders(dirty);
       if (dirty.isEmpty) {
         _unsavedChanges.remove(keyVarAuthData);
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dart/lib/src/objects/parse_user.dart` around lines 551 - 568,
Extract the repeated "remove null entries and drop empty map" logic in
_cleanUpAuthData into a small helper (e.g., _pruneNullMap) that takes a
Map<String, dynamic>? and the container map to remove the key from (or returns a
bool indicating emptiness); then call it for both _objectData[keyVarAuthData]
and _unsavedChanges[keyVarAuthData] instead of duplicating the removeWhere +
isEmpty checks. Ensure the helper uses the same null-safe casts and removes
keyVarAuthData from the appropriate parent map when the child map becomes empty,
preserving current behavior of _cleanUpAuthData.
packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart (1)

18-23: Optional: de-duplicate repeated PUT stubbing and provider key literals.

A small local helper (and a shared const anonymousProvider = 'anonymous') would make these tests easier to evolve.

Also applies to: 67-82, 105-120, 146-161, 192-205, 244-247, 266-281

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`
around lines 18 - 23, Duplicate literals and repeated PUT stubbing in
parse_user_anonymous_link_test.dart should be consolidated: introduce a shared
const anonymousProvider = 'anonymous' and a small helper function (e.g.,
buildPutPath(userObjectId) or stubPutForUser(userObjectId, body)) to return the
putPath or register the PUT stub using existing symbols keyEndPointClasses,
keyClassUser, userObjectId and anonymousId; replace repeated inline
Uri.parse(...) and literal 'anonymous' occurrences with the new helper/const and
update tests that reference putPath, userObjectId, anonymousId to call the
helper to remove duplication and make stubbing consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/dart/lib/src/objects/parse_user.dart`:
- Around line 551-568: Extract the repeated "remove null entries and drop empty
map" logic in _cleanUpAuthData into a small helper (e.g., _pruneNullMap) that
takes a Map<String, dynamic>? and the container map to remove the key from (or
returns a bool indicating emptiness); then call it for both
_objectData[keyVarAuthData] and _unsavedChanges[keyVarAuthData] instead of
duplicating the removeWhere + isEmpty checks. Ensure the helper uses the same
null-safe casts and removes keyVarAuthData from the appropriate parent map when
the child map becomes empty, preserving current behavior of _cleanUpAuthData.

In
`@packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart`:
- Around line 18-23: Duplicate literals and repeated PUT stubbing in
parse_user_anonymous_link_test.dart should be consolidated: introduce a shared
const anonymousProvider = 'anonymous' and a small helper function (e.g.,
buildPutPath(userObjectId) or stubPutForUser(userObjectId, body)) to return the
putPath or register the PUT stub using existing symbols keyEndPointClasses,
keyClassUser, userObjectId and anonymousId; replace repeated inline
Uri.parse(...) and literal 'anonymous' occurrences with the new helper/const and
update tests that reference putPath, userObjectId, anonymousId to call the
helper to remove duplication and make stubbing consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12dc8694-f354-42fe-8a92-7a38f83366ba

📥 Commits

Reviewing files that changed from the base of the PR and between 0860324 and 64ec48a.

📒 Files selected for processing (2)
  • packages/dart/lib/src/objects/parse_user.dart
  • packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart

@chadpav
Copy link
Copy Markdown
Contributor Author

chadpav commented Apr 28, 2026

@mtrezza There are a few other PR's open. Do we have people merging PR's? You can add me as a maintainer if needed.

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.

1 participant