Skip to content

fix(identities): send unlink provider as a query param - #93

Merged
Adron merged 2 commits into
mainfrom
fix/unlink-identity-query-param
Sep 17, 2026
Merged

Adron merged 2 commits into
mainfrom
fix/unlink-identity-query-param

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Summary

Closes #77. Settings -> Connected accounts -> Disconnect always failed with 400 {"error":"provider is required"}.

APIClient.unlinkIdentity sent the provider in a camelCase JSON body via deleteCamel, but DELETE /api/user/identities reads it from the query string (searchParams.get('provider')) and never parses a body, so the value was always null. The call now uses the bodyless delete(_:) against /api/user/identities?provider=<encoded>.

providerId is gone from the signature: the route never reads it and deletes by { userId, provider } via deleteMany, so it removes every identity row stored under that provider value. That is recorded in a doc comment on the method rather than worked around here. In practice Mastodon rows are stored per instance (mastodon:techhub.social), so distinct instances are unaffected; the existing confirmation copy ("You'll no longer be able to sign in with X") is already provider-scoped.

What's included

  • unlinkIdentity(provider:) sends the provider as a query parameter and an empty body.
  • Percent-encoding uses .urlQueryAllowed minus +&=?#/. That set permits the sub-delimiters, and the backend reads params through URLSearchParams, which decodes a literal + as a space.
  • Call site in LinkedIdentitiesView updated; its 401 path still goes through authState.handleUnauthorized(), unchanged.
  • deleteCamel's doc comment no longer lists identity unlink as a body-scoped route.
  • Nine unlinkIdentity tests in APIClientIdentitiesTests.

No new files, so no project.pbxproj change. Out of scope by design: no token-health / "check connection" work (that is #92), and verifyIdentity is untouched.

Testing

  • Full suite, serialized, E2E skipped: 1153 tests, 0 failures (-destination id=D9B21F4D-933E-4871-98AE-B3E65427BCF5, dedicated -derivedDataPath, -parallel-testing-enabled NO, -skip-testing:InterlinedListTests/E2EReadOnlyTests). No compiler warnings.
  • Regression check: with the production method reverted to the body-based call, the four new assertions fail (sendsProviderAsQueryParameter, sendsEmptyBody, mastodonProvider_keepsInstanceSuffix, providerWithQueryDelimiters_percentEncodesThem), so the tests bite.
  • New coverage: query parameter present, body is nil, verb DELETE + path /api/user/identities, mastodon:instance survives encoding, + & = are percent-encoded and round-trip through URLComponents, plus the 401 / 403 / 404 error paths (404 surfaces the route's Identity not found).
  • Not verified: the live disconnect against the real API — that needs a signed-in account with a linked identity. Confirm by disconnecting an account in Settings and refreshing.

🤖 Generated with Claude Code

Settings -> Connected accounts -> Disconnect always failed with
400 {"error":"provider is required"}. `unlinkIdentity` sent the provider in a
camelCase JSON body via `deleteCamel`, but DELETE /api/user/identities reads it
from the query string (`searchParams.get('provider')`) and never parses a body,
so the value was always null.

Switch to the bodyless `delete(_:)` with `?provider=<encoded>`. The value is
percent-encoded with `.urlQueryAllowed` minus `+&=?#/`: that set permits the
sub-delimiters, and the backend reads params through `URLSearchParams`, which
decodes a literal `+` as a space.

Drop `providerId` — the route never reads it and deletes by
`{userId, provider}`, removing every identity stored under that provider value.
That is recorded on the method, not worked around here.

Tests cover the query parameter, an empty body, the verb and path, the
mastodon:instance provider form, delimiter encoding, and the 401/403/404 paths.
The four new assertions fail against the previous body-based call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1
One conflict, in APIClient.swift: this branch adds the `queryValueAllowed`
character set used by `unlinkIdentity`, and #90 (the dead-endpoint sweep, now on
main) added a doc comment above `verifyIdentity` at the same point. Both kept,
in that order, so the doc comment stays attached to the function it documents —
verified after resolving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017bss5MgZa7Jvj2m9zdaUd1
@Adron
Adron merged commit 7cfec66 into main Sep 17, 2026
1 check passed
@Adron
Adron deleted the fix/unlink-identity-query-param branch September 17, 2026 08:46
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.

Bug: unlinking a connected account always fails — provider sent in the body, route reads the query string

1 participant