diff --git a/sync3/avatar.go b/sync3/avatar.go new file mode 100644 index 0000000..63e494b --- /dev/null +++ b/sync3/avatar.go @@ -0,0 +1,42 @@ +package sync3 + +import ( + "bytes" + "encoding/json" +) + +// An AvatarChange represents a change to a room's avatar. There are three cases: +// - an empty string represents no change, and should be omitted when JSON-serialised; +// - the sentinel `` represents a room that has never had an avatar, +// or a room whose avatar has been removed. It is JSON-serialised as null. +// - All other strings represent the current avatar of the room and JSON-serialise as +// normal. +type AvatarChange string + +const DeletedAvatar = AvatarChange("") +const UnchangedAvatar AvatarChange = "" + +// NewAvatarChange interprets an optional avatar string as an AvatarChange. +func NewAvatarChange(avatar string) AvatarChange { + if avatar == "" { + return DeletedAvatar + } + return AvatarChange(avatar) +} + +func (a AvatarChange) MarshalJSON() ([]byte, error) { + if a == DeletedAvatar { + return []byte(`null`), nil + } else { + return json.Marshal(string(a)) + } +} + +// Note: the unmarshalling is only used in tests. +func (a *AvatarChange) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("null")) { + *a = DeletedAvatar + return nil + } + return json.Unmarshal(data, (*string)(a)) +} diff --git a/sync3/room.go b/sync3/room.go index 6bf7c35..3e40098 100644 --- a/sync3/room.go +++ b/sync3/room.go @@ -10,6 +10,7 @@ import ( type Room struct { Name string `json:"name,omitempty"` + AvatarChange AvatarChange `json:"avatar,omitempty"` RequiredState []json.RawMessage `json:"required_state,omitempty"` Timeline []json.RawMessage `json:"timeline,omitempty"` InviteState []json.RawMessage `json:"invite_state,omitempty"` diff --git a/sync3/room_test.go b/sync3/room_test.go new file mode 100644 index 0000000..09ec058 --- /dev/null +++ b/sync3/room_test.go @@ -0,0 +1,74 @@ +package sync3 + +import ( + "encoding/json" + "fmt" + "github.com/tidwall/gjson" + "reflect" + "testing" +) + +func TestAvatarChangeMarshalling(t *testing.T) { + var url = "mxc://..." + testCases := []struct { + Name string + AvatarChange AvatarChange + Check func(avatar gjson.Result) error + }{ + { + Name: "Avatar exists", + AvatarChange: NewAvatarChange(url), + Check: func(avatar gjson.Result) error { + if !(avatar.Exists() && avatar.Type == gjson.String && avatar.Str == url) { + return fmt.Errorf("unexpected marshalled avatar: got %#v want %s", avatar, url) + } + return nil + }, + }, + { + Name: "Avatar doesn't exist", + AvatarChange: DeletedAvatar, + Check: func(avatar gjson.Result) error { + if !(avatar.Exists() && avatar.Type == gjson.Null) { + return fmt.Errorf("unexpected marshalled Avatar: got %#v want null", avatar) + } + return nil + }, + }, + { + Name: "Avatar unchanged", + AvatarChange: UnchangedAvatar, + Check: func(avatar gjson.Result) error { + if avatar.Exists() { + return fmt.Errorf("unexpected marshalled Avatar: got %#v want omitted", avatar) + } + return nil + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + room := Room{AvatarChange: tc.AvatarChange} + marshalled, err := json.Marshal(room) + t.Logf("Marshalled to %s", string(marshalled)) + if err != nil { + t.Fatal(err) + } + avatar := gjson.GetBytes(marshalled, "avatar") + if err = tc.Check(avatar); err != nil { + t.Fatal(err) + } + + var unmarshalled Room + err = json.Unmarshal(marshalled, &unmarshalled) + if err != nil { + t.Fatal(err) + } + t.Logf("Unmarshalled to %#v", unmarshalled.AvatarChange) + if !reflect.DeepEqual(unmarshalled, room) { + t.Fatalf("Unmarshalled struct is different from original") + } + }) + } +} diff --git a/testutils/m/match.go b/testutils/m/match.go index eb852f5..f8f6c6a 100644 --- a/testutils/m/match.go +++ b/testutils/m/match.go @@ -39,6 +39,39 @@ func MatchRoomName(name string) RoomMatcher { } } +// MatchRoomAvatar builds a RoomMatcher which checks that the given room response has +// set the room's avatar to the given value. +func MatchRoomAvatar(wantAvatar string) RoomMatcher { + return func(r sync3.Room) error { + if string(r.AvatarChange) != wantAvatar { + return fmt.Errorf("MatchRoomAvatar: got \"%s\" want \"%s\"", r.AvatarChange, wantAvatar) + } + return nil + } +} + +// MatchRoomUnsetAvatar builds a RoomMatcher which checks that the given room has no +// avatar, or has had its avatar deleted. +func MatchRoomUnsetAvatar() RoomMatcher { + return func(r sync3.Room) error { + if r.AvatarChange != sync3.DeletedAvatar { + return fmt.Errorf("MatchRoomAvatar: got \"%s\" want \"%s\"", r.AvatarChange, sync3.DeletedAvatar) + } + return nil + } +} + +// MatchRoomUnchangedAvatar builds a RoomMatcher which checks that the given room has no +// change to its avatar, or has had its avatar deleted. +func MatchRoomUnchangedAvatar() RoomMatcher { + return func(r sync3.Room) error { + if r.AvatarChange != sync3.UnchangedAvatar { + return fmt.Errorf("MatchRoomAvatar: got \"%s\" want \"%s\"", r.AvatarChange, sync3.UnchangedAvatar) + } + return nil + } +} + func MatchJoinCount(count int) RoomMatcher { return func(r sync3.Room) error { if r.JoinedCount != count {