Skip to content

Push activation: full implementation (RSH2/RSH3/RSH4-5/RSH6/RSH8) with UTS-derived tests - #13

Draft
paddybyers wants to merge 2 commits into
mainfrom
feat/uts-push
Draft

paddybyers wants to merge 2 commits into
mainfrom
feat/uts-push

Conversation

@paddybyers

Copy link
Copy Markdown
Member

Implements the activation half of the push spec — the state machine, LocalDevice, device authentication, and platform primitives — fully conformant with the extended spec of ably/specification#513 (including Push#updateToken, the APNs token-variant slots, and the unified PATCH registration sync), with zero recorded deviations. Every test is derived from the UTS suite in that PR; the sibling derived suite for ably-js is ably/ably-pubsub-js#2283.

Public API

  • PushPlatformConfig on ClientOptions.pushPlatform — the injectable platform primitives: PushKeyValueStorage (getItem/setItem/removeItem, AsyncStorage-shaped), requestToken() → PushDeviceToken {transportType, token, apnsTokenType?}, plus platform/formFactor. The core stays pure Dart; concrete providers (shared_preferences storage, firebase_messaging/APNs token providers) are a future companion package.
  • Push gains activate({registerCallback, updatedCallback}) (RSH2a), deactivate({deregisterCallback}) (RSH2b), updateToken(token) (RSH2f — the delivery path for FCM onTokenRefresh/APNs re-registration/ActivityKit tokens, including the token-variant slots per PCP3a/RSH8l2).
  • RestClient/RealtimeClient gain Future<LocalDevice> getDevice() (RSH8/RSH8a).

Implementation highlights (lib/src/impl/push_activation_impl.dart)

  • The seven-state/ten-event machine with the RSH4 pending-event queue (peek/consume/put-back), RSH5 sequential processing, and coalescing completer lists (concurrent activates/deactivates all resolve).
  • The RSH3a2a re-activation validation (61002 clientId check; RSH3d3b PATCH carrying the complete recipient), RSH3d2c1 deregistration classification (2xx/401/40005 → Deregistered; otherwise failure + rollback per RSH3g3b), registrar callbacks, and updatedCallback delivery (RSH3e2c/RSH3e3d).
  • LocalDevice per RSH8: UUIDv4 id + base64(sha256) secret (RSH3a2b), partial-state loading (RSH8a), corruption discard incl. unknown persisted state names (RSH8a1/RSH3h), raw X-Ably-DeviceToken / X-Ably-DeviceSecret device auth (RSH6a/b), admin own-device auth (RSH1b/RSH1c clauses), identity reset on deregistration, and RSH8d/RSH8e late-clientId wiring via a new AuthImpl.onTokenChanged hook.

⚠️ Behavioural change — auth (RSA7e2)

Supplying clientId alongside an API key no longer forces token auth: basic auth is retained and the client identifies via a base64 X-Ably-ClientId header. This is required by the push specs (an identified client's activation makes no token requests) and matches RSA7e2. The pre-existing test "RSA4b - key + clientId triggers token auth" asserted the old behaviour but cited a UTS id that actually specifies the authCallback case; it is rewritten to assert RSA7e2. Please review this change specifically.

Tests

75 UTS-derived tests, each // UTS:-tagged:

  • Unit (62): full state-machine matrix, LocalDevice, persistence, event queue, updateToken (incl. token variants), device auth, push types. MockPushStorage added to test/helpers (docs + self-tests).
  • Integration (7): live sandbox, ablyChannel-recipient design, incl. end-to-end delivery of an admin publish and the RSH6a raw-token server-acceptance check.
  • Proxy (6): uts-proxy fault injection — classification proven client-side, rollback/retry, RSH4 queueing against a delayed registration.

Results: unit suite 1274 passing / 0 failing; integration + proxy 10 passing + 3 skipped; dart analyze clean. The 3 skips are pending the sandbox deploy of ably/realtime#8591 (a server bug rejecting registration-update PATCHes for stored-ablyChannel-recipient devices, found by this suite) — unskip once deployed.

Completion matrix: push notifications 8/8 Full, push types 4/4 Full.

🤖 Generated with Claude Code

paddybyers and others added 2 commits August 8, 2026 21:11
… UTS-derived tests

Implement the activation half of the push spec, fully conformant with the
extended spec of ably/specification#513 (including Push#updateToken, the
APNs token-variant slots, and the unified PATCH registration sync), with
zero deviations. All 75 UTS-derived tests pass (62 unit, 7 integration,
6 proxy), validated against the live sandbox and through uts-proxy fault
injection.

Public API:
- lib/src/push/push_platform.dart: the portable platform primitives —
  PushKeyValueStorage (getItem/setItem/removeItem), PushDeviceToken
  (transportType, token, optional apnsTokenType per PDT1-PDT4),
  PushPlatformConfig (platform, formFactor, storage, requestToken), and
  the activation callback types (RegisterCallback returning a
  DeviceRegistrationResult, DeregisterCallback, UpdatedCallback).
- Push gains activate({registerCallback, updatedCallback}) (RSH2a),
  deactivate({deregisterCallback}) (RSH2b) and updateToken(token) (RSH2f);
  ClientOptions gains pushPlatform; RestClient and RealtimeClient gain
  Future<LocalDevice> getDevice() (RSH8/RSH8a).

Implementation (lib/src/impl/push_activation_impl.dart):
- LocalDeviceManager: RSH8 load/generate/persist under the standard
  ably.push.* keys; UUIDv4 id and base64(sha256(secure random)) secret
  (RSH3a2b); RSH8a partial-state loading ("to the extent that they
  exist"); RSH8a1 corrupt-state discard (incl. unknown persisted machine
  state names falling back to NotActivated); RSH6a raw X-Ably-DeviceToken
  and RSH6b X-Ably-DeviceSecret device auth; PCP3a/RSH8l2 apnsDeviceTokens
  slot handling preserving unregistered variants; identity reset on
  deregistration (RSH3g2a).
- ActivationStateMachine: the seven states and ten events, the RSH4
  pending-event queue (peek/consume/put-back) with RSH5 sequential
  processing, coalescing completer lists so concurrent activate/deactivate
  calls all resolve (RSH3b1a/RSH3c1a/RSH3g1a), the RSH3a2a re-activation
  validation (61002 clientId check; RSH3d3b PATCH sync carrying the
  complete recipient), RSH3d2c1 deregistration classification (2xx/401/
  40005 -> Deregistered, otherwise DeregistrationFailed with rollback per
  RSH3g3b), registrar callbacks, updatedCallback delivery (RSH3e2c/
  RSH3e3d), and serialized fire-and-forget persistence.
- Admin own-device push auth (RSH1b1/b3/b5, RSH1c3/c4) in the push admin
  implementations; channel push operations hydrate the LocalDevice on
  first use (RSH8a) via a loadDevice hook threaded through the channel
  factories.
- RSH8d/RSH8e late identification: AuthImpl.onTokenChanged fires after
  authorize(), the device clientId is set and persisted, and a
  GotPushDeviceDetails sync follows when registered.

Behavioural change, auth (RSA7e2): supplying clientId alongside an API key
no longer forces token auth; basic auth is retained and the client
identifies itself with a base64 X-Ably-ClientId header (via a new
additionalAuthHeaders hook on AblyHttpClient). Required by the push specs
(an identified client's activation makes no token requests). The
pre-existing "RSA4b - key + clientId triggers token auth" test asserted
the old behaviour but cited a UTS id that actually specifies the
authCallback case; it is rewritten to assert RSA7e2.

Test infrastructure: MockPushStorage (test/helpers, with docs and
self-tests) implementing the UTS mock spec (dump/seed/fault flags/
onOperation); MockHttpClient's jsonBody now falls back to msgpack
decoding, since the SDK's default useBinaryProtocol encodes msgpack
request bodies.

Three integration/proxy tests are skipped pending the sandbox deploy of
ably/realtime#8591 (a server bug rejecting registration-update PATCHes
for devices whose stored recipient is ablyChannel); unskip once deployed.

Suite results: unit 1274 passing; integration + proxy 10 passing +
3 pending-deploy skips; dart analyze clean. Completion matrix updated
(RSH 8/8 Full, push types 4/4 Full).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formats the 10 files from the previous commit that dart format
--set-exit-if-changed flagged in CI, and restructures three expect()
calls whose reformatting tripped require_trailing_commas.
No behavioural change; analyze clean, tests unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant