feat: Multiple SDK-level fixes for anonymous-to-password conversion (iOS parity)#1136
feat: Multiple SDK-level fixes for anonymous-to-password conversion (iOS parity)#1136chadpav wants to merge 9 commits into
Conversation
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.
|
🚀 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
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. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds local reconciliation for anonymous authData in ParseUser: setting ChangesAnonymous Auth Data Stripping and Session-Token Adoption
Network header pipeline and tests
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1136 +/- ##
==========================================
+ Coverage 44.42% 46.60% +2.18%
==========================================
Files 62 62
Lines 3730 3761 +31
==========================================
+ Hits 1657 1753 +96
+ Misses 2073 2008 -65 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart (1)
63-105: Please add anupdate()variant of this regression.
ParseUser.update()now has its own cleanup hook too, but the suite only exercisessave(). A matchingupdate()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 emptyauthDatain the unsaved-user branch.If
anonymouswas the only provider, Lines 539-545 leaveauthDataas{}in local state. RemovingkeyVarAuthDataentirely 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
📒 Files selected for processing (2)
packages/dart/lib/src/objects/parse_user.dartpackages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart
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.
There was a problem hiding this comment.
🧹 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_objectDataand_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
📒 Files selected for processing (2)
packages/dart/lib/src/objects/parse_user.dartpackages/dart/test/src/objects/parse_user/parse_user_anonymous_link_test.dart
|
@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. |
|
@parse-community/ios-sdk-maintenance do we have any active maintainers on this SDK? I can help where possible. |
|
@chadpav it can take some time until your PR gets reviewed; in the meantime you can test this change out and make sure there are no issues. |
ok, no problem. I'm running my fork in production. I just wanted to make sure someone was still watching these. |
Parse Server mints a fresh session row when password is set on an existing _User and revokeSessionOnPasswordReset is true (default since 9.x). The new sessionToken is embedded in the save response. Without adopting it, the global session in ParseCoreData keeps pointing at the prior session — which the server just destroyed — and every subsequent request fails with invalidSessionToken until the next login. Mirrors PFUser._mergeFromServerWithResult on iOS, which reads PFUserSessionTokenRESTKey out of the response and installs it. The helper is a no-op when the response carries no sessionToken (e.g. a plain field update with no password change), preserving the existing behavior for non-auth saves.
Match iOS PFURLSessionCommandRunner behaviour, which attaches the install ID header to every outgoing request. Previously, only the three user-creation methods (signUp, login, loginAnonymous, loginWith) built the header inline; every other call — including PUT save on an existing user — went out without it. Without the install ID on writes, parse-server cannot bind the _Session row it mints during an authData/password change to the device's installation, and destroyDuplicatedSessions skips cleanup of prior sessions on the same install. Empirically this leaves one orphan _Session row per anonymous-to-password link. Changes: * Add `sendInstallationId` flag to `ParseNetworkOptions` (null/true → attach, false → suppress) so the existing `doNotSendInstallationID` parameter on user methods continues to opt out cleanly. * Hoist a shared `buildHeaders` helper onto the abstract `ParseClient` so both `ParseHTTPClient` and `ParseDioClient` route through one implementation. `@protected`; no public API surface change. * Both clients call `await buildHeaders(options)` from each of get, put, post, postBytes, getBytes, and delete. * `ParseUser.signUp/login/loginAnonymous/_loginWith` drop their inline `_getInstallationId()` + conditional header building and pass `sendInstallationId: !doNotSendInstallationID` through `ParseNetworkOptions` instead. Removes the unused `_getInstallationId` helper. Tests: * `parse_client_test.dart` (new): 4 unit tests for `buildHeaders` — default attaches, sendInstallationId:false suppresses, caller- supplied header preserved, custom-header + auto install ID coexist. * `parse_dio_client_test.dart`: 1 integration test via a custom `HttpClientAdapter` that captures outgoing request headers, proves `buildHeaders` output reaches dio's wire. * `parse_http_client_test.dart`: copy-pasted "ParseDioClient" labels corrected to "ParseHTTPClient".
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/dart/lib/src/objects/parse_user.dart (1)
46-46: 💤 Low valueConsider using
_keyAuthAnonymousconstant at line 306 for consistency.The
loginAnonymousmethod uses the string literal'anonymous'on line 306, while the new constant_keyAuthAnonymousis defined for this purpose. Using the constant would improve maintainability.♻️ Suggested fix
data: jsonEncode(<String, dynamic>{ 'authData': <String, dynamic>{ - 'anonymous': <String, dynamic>{'id': uuid.v4()}, + _keyAuthAnonymous: <String, dynamic>{'id': uuid.v4()}, }, }),Also applies to: 304-308
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/dart/lib/src/objects/parse_user.dart` at line 46, Replace the string literal 'anonymous' used inside the loginAnonymous method with the existing constant _keyAuthAnonymous to ensure consistency and maintainability; locate the loginAnonymous function and update any occurrences (around where the payload/credential map is built at lines ~304-308) to reference the static const _keyAuthAnonymous instead of the literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/dart/lib/src/objects/parse_user.dart`:
- Line 46: Replace the string literal 'anonymous' used inside the loginAnonymous
method with the existing constant _keyAuthAnonymous to ensure consistency and
maintainability; locate the loginAnonymous function and update any occurrences
(around where the payload/credential map is built at lines ~304-308) to
reference the static const _keyAuthAnonymous instead of the literal.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7c216132-3cce-4766-a7e6-74c71830e9d1
📒 Files selected for processing (8)
packages/dart/lib/src/network/options.dartpackages/dart/lib/src/network/parse_client.dartpackages/dart/lib/src/network/parse_dio_client.dartpackages/dart/lib/src/network/parse_http_client.dartpackages/dart/lib/src/objects/parse_user.dartpackages/dart/test/src/network/parse_client_test.dartpackages/dart/test/src/network/parse_dio_client_test.dartpackages/dart/test/src/network/parse_http_client_test.dart
✅ Files skipped from review due to trivial changes (1)
- packages/dart/test/src/network/parse_http_client_test.dart
Addresses upstream PR parse-community#1136 review feedback. The loginAnonymous request body literally wrote 'anonymous' while the rest of the file (stripAnonymity branches at lines 537/541/543) references the `_keyAuthAnonymous` constant defined at line 46. One-line change brings the body into alignment.
We don't know when parse-server first defaulted revokeSessionOnPasswordReset to true. "default since 9.x" overclaims historical knowledge. Reword to "current default in 9.x" — accurate, no implicit version-history assertion.
Adding the protected buildHeaders helper to ParseClient (commit 5b2cd09) requires regenerating MockParseClient via build_runner. CI's pub publish --dry-run check rejects dirty generated files; this commit lands the up-to-date mocks.
|
The failing CI build is unrelated to this PR. Do you guys need me to address that? |
|
@coderabbitai should the issues addressed here be split into separate PRs, to satisfy the rules in #1136 (comment)? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 1732
PR 1 — Core fix (the stated purpose of this PR)Anonymous-to-email/credential conversion reconciliation
PR 2 — Session token adoptionAdopt server-returned
This is a generally useful improvement that was bundled in while touching PR 3 — Network layer: installation ID header control
This is an entirely separate network-layer concern with no dependency on the anonymous fix. Splitting this way would keep each PR small and focused, matching the contribution guidelines, and make each change much easier to review and merge independently. |
|
Per @mtrezza and CodeRabbit's guidance above, this PR has been split into three independent PRs against
Each branch is cut from current Closing this PR in favor of the split. |
|
@chadpav Is there any specific order in which these PRs need to or should get merged? |
|
@mtrezza No required order — the three PRs are functionally independent. There are small textual conflicts in
Merge in whatever order is convenient. I'll rebase the remaining PRs immediately after each merge so the next one lands conflict-free. If you'd like to minimize rebase touchpoints, #1139 → #1140 → #1138 keeps #1138 as the last to land, since it's the only one that conflicts with either of the others. |
Issue
No existing issue; problems described below.
Approach
The Dart SDK's anonymous-to-password user conversion
(
user.username = email; user.password = pw; user.save()afterloginAnonymous()) has three independent gaps vs the iOS SDK. Allthree surfaced during empirical testing in a production-mirror setup
as causes of
invalidSessionTokenrejections or orphan_Sessionrow accumulation. This PR ports iOS's behavior into the Dart SDK
across three logically-distinct commits.
Fix 1 — Reconcile local
authDataon conversionWhen a persisted anonymous user has its
usernameset and is saved,parseUser.save()issues aPUT /classes/_User/:objectIdcarryingonly the dirty fields. The Dart SDK treated this as a generic
property update — it neither emitted an
authDataunlink signal inthe request body nor cleaned up the local cache after the save.
The iOS Parse SDK handles this via two helpers in
PFUser.m:stripAnonymity(PFUser.m:593-607): theusernamesetterwrites
authData[anonymous] = NSNullinto the local user. ThePUT body re-injects the map, so the request carries
authData: { anonymous: null }— Parse Server's documentedunlink signal.
cleanUpAuthData(PFUser.m:300-313): after a successfulsave, the local
authDatahas any null entries stripped.Ported to
ParseUseras two private helpers:_stripAnonymity()invoked from theusernamesetter. On apersisted user it sets
authData['anonymous'] = nulllocally; ona lazy user (no
objectId) it removes the entry outright._cleanUpAuthData()invoked fromsave()andupdate()betweenthe response merge and
_onResponseSuccess().A dartdoc paragraph was added to
loginAnonymousdocumenting theconversion path.
Fix 2 — Adopt
sessionTokenfrom the save/update responseParse Server mints a fresh
_Sessionrow whenpasswordis set onan existing
_User(withrevokeSessionOnPasswordReset = true, thecurrent default in parse-server 9.x). The new
sessionTokenisembedded in the save response. Without adopting it, the global
session in
ParseCoreDatakeeps pointing at the prior session —which the server just destroyed — and every subsequent request
fails with
invalidSessionTokenuntil the next login.This mirrors
PFUser._mergeFromServerWithResulton iOS, which readsPFUserSessionTokenRESTKeyout of the response and installs it.Implemented as
_adoptResponseSessionTokenIfChanged(String?)invoked from
save()andupdate(). The helper is a no-op whenthe response carries no
sessionToken(e.g. a plain field update),preserving the existing behavior for non-auth saves.
Fix 3 — Send
X-Parse-Installation-Idon all HTTP requestsiOS's
PFURLSessionCommandRunnerattaches the install ID header toevery outgoing request
(
PFCommandURLRequestConstructor.m:_getURLRequestHeadersAsyncForCommand:).The Dart SDK previously built the header inline only in the four
user-creation methods (
signUp,login,loginAnonymous,loginWith). Every other call — includingPUTsave on anexisting user — went out without it.
Without the install ID on writes, parse-server cannot bind the
_Sessionrow it mints during an authData/password change to thedevice's installation, and
destroyDuplicatedSessionsskipscleanup of prior sessions on the same install. Empirically this
leaves one orphan
_Sessionrow per anonymous-to-password link.Changes:
sendInstallationId: bool?toParseNetworkOptions(null/true → attach, false → suppress).
buildHeadershelper onto the abstractParseClient(@protected); bothParseHTTPClientandParseDioClientinherit and route through it.ParseUser.signUp/login/loginAnonymous/_loginWithdrop theirinline
_getInstallationId()and passsendInstallationId: !doNotSendInstallationIDthrough options —the user-facing opt-out parameter continues to work.
Tasks
Behavior change disclosure
objectIdis setand
authDatacontains ananonymousentry, settingusernameand saving now produces a PUT body that includes
authData: { anonymous: null }. Previously noauthDatafieldwas sent for that save.
usernameonan anonymous-with-
objectIdParseUsernow mutates the localauthDatamap. After a successful save, theanonymousentryis removed entirely.
_Usersave/update response carries a
sessionTokenfield (i.e.parse-server minted a new session), the SDK installs it as the
active session via
ParseCoreData().setSessionId(...).Previously the new token was merged into the local user object
but the global session continued pointing at the prior token.
request now carries
X-Parse-Installation-Idby default.Previously only
signUp,login,loginAnonymous, andloginWithset it. Opt-out is preserved viaParseNetworkOptions(sendInstallationId: false)and theexisting
doNotSendInstallationIDparameter on user methods.The commit prefixes mix
fix:andfeat:. The PR is labeledfeat:overall — the install-ID change is a feature-level defaultchange, even with opt-out preserved.
If maintainers prefer to split commits
83244367and5b2cd09einto separate PRs, they are independent of each other and of the
original three commits and can be cherry-picked onto clean branches.
Documentation follow-up (out of scope for this PR)
This PR adds a dartdoc paragraph to
loginAnonymouscovering theconversion 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 to
packages/dart/test/src/:objects/parse_user/parse_user_anonymous_link_test.dart— 6tests covering Fix 1 (authData reconciliation behavior).
objects/parse_user/parse_user_session_token_test.dart(new) —3 tests covering Fix 2 (sessionToken adoption from save/update
response).
network/parse_client_test.dart(new) — 4 unit tests coveringFix 3's
buildHeadershelper (default attach, opt-out viasendInstallationId, preserve caller-supplied header, merge withcustom headers).
network/parse_dio_client_test.dart— 1 new integration testverifying
buildHeadersoutput reaches dio's request pipelinevia a custom
HttpClientAdapter.network/parse_http_client_test.dart— copy-pasted"ParseDioClient" group label corrected to "ParseHTTPClient".
Total: 14 new tests across 4 test files. All 221 tests in
packages/dart/test/pass.