Fix presence get for a status message with an expiry - #73
Merged
Conversation
`GET /me/presence` returns `statusMessage.expiryDateTime` as a dateTimeTimeZone object, not a string, so `presence get` for your own account failed to deserialize whenever the account's status message carried an expiry. The write path already models that shape as `SetStatusExpiry`; the read path now reuses it. The existing round-trip test passed only because its fixture sets `statusMessage` to null. The new test uses the response shape Graph actually sends. Fixes osodevops#69 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GET /users/{id}/presence` returns the same expiry object as `/me/presence`
and deserializes through the same struct, so it failed the same way. It is a
distinct call site, though, and a refactor that gave it a response type of its
own would reintroduce the defect with a fixture test on the shared struct
still passing.
The test therefore serves the body over a mock and reads it back through
`get_user_presence_at`, the seam `get_user_presence` delegates to, following
the pattern the message and channel modules already use for this. Going
through the request is what makes it a guard on that path rather than on the
struct. The body is what Graph sends for a user whose status message carries
an expiry, the `@odata.context` wrapper and `publishedDateTime` included.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C736oc7CeNtRZmAJrXm8sG
The expiry object serves both directions now, so name it after the Graph resource it models rather than the request that first needed it. Its two components become optional on the read path. Every other Graph response model in the crate is optional throughout, and a CLI that fails a whole `presence get` because one nested string was absent is the failure mode osodevops#69 already was. The write path is the reason both components exist, so a test asserts the serialized setStatusMessage body still carries them. `publishedDateTime` is a documented presenceStatusMessage property that serde was discarding; it now reaches the JSON envelope. Adds the CHANGELOG entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqBTdhSRTBabYv7kkEiUkE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
teams presence getfailed for any account whose Teams status message carriesan expiry. The HTTP call succeeded; deserialization did not.
GET /me/presencereturnsstatusMessage.expiryDateTimeas adateTimeTimeZone
object:
PresenceStatusMessage::expiry_date_timewas typedOption<String>, so serderejected the body and the command reported
API error (200). The write pathalready models that shape as
SetStatusExpiry, which now derivesDeserializeand serves both directions.
Which read paths this affects
get_my_presence,get_user_presenceandget_presence_batchalldeserialize into the same
Presencestruct, so the failure is data-dependentrather than path-dependent: any of them breaks the moment the response
actually carries the field.
Two of the three do. Measured against one build, one profile, one minute:
presence get(/me/presence)presence get --user <id>presence get --user <other-id>presence get-batch --user-ids <both>expiryDateTimeentirelyget-batchis unaffected becausePOST /communications/getPresencesByUserIddoes not return the field, not because it parses a different shape. It
returned the same account's
statusMessage.messagewith content andcontentTypeand noexpiryDateTimealongside it.So the corrected statement is that
/me/presenceand/users/{id}/presenceare both affected, for any target whose status message carries an expiry.
A single-user lookup is the call an agent makes most often when checking
whether a colleague is free, so it is worth being precise about.
Tests
The pre-existing round-trip test passed only because its fixture sets
statusMessageto null.Two tests now cover a status message with an expiry. One parses a
/me/presencebody. The other serves a/users/{id}/presencebody over amock and reads it back through
get_user_presence_at, the seamget_user_presencedelegates to, with the@odata.contextwrapper andpublishedDateTimethat Graph actually sends, following the pattern themessage and channel modules already use. Going through the call rather than
parsing a string is what makes it a guard on that path: a refactor giving the
user lookup a response type of its own would otherwise reintroduce the defect
with a fixture test on the shared struct still passing.
Reverting the type change fails both.
Note for whoever merges: #72 also appends a test module to
src/api/presence.rs, so whichever of the two lands second needs a trivialconflict resolved there.
Fixes #69
🤖 Generated with Claude Code
https://claude.ai/code/session_01C736oc7CeNtRZmAJrXm8sG