amf3_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package wire
  2. import (
  3. "testing"
  4. "time"
  5. goAMF3 "github.com/breign/goAMF3"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // marshalAMF3Map encodes v and decodes it back as an AMF3 object.
  10. func marshalAMF3Map(t *testing.T, v any) map[string]any {
  11. t.Helper()
  12. b, err := MarshalAMF3(v)
  13. require.NoError(t, err)
  14. decoded := goAMF3.DecodeAMF3(b)
  15. m, ok := decoded.(map[string]any)
  16. require.True(t, ok, "decoded as %T, want an object", decoded)
  17. return m
  18. }
  19. // amf3EventType stands in for the named string types that carry an event's type
  20. // constant.
  21. type amf3EventType string
  22. type amf3Tagged struct {
  23. Sender string `json:"sender" amf3:"source"`
  24. AutoResp bool `json:"autoResponse,omitempty" amf3:"autoresponse"`
  25. Message string `json:"message"`
  26. State string `json:"state,omitempty" amf3:"state"`
  27. Secret string `json:"-"`
  28. Hidden string `amf3:"-" json:"hidden"`
  29. MsgID string `json:"msgId,omitempty"`
  30. unseen string
  31. }
  32. // An amf3 tag replaces the json tag, which is how the same struct spells a field
  33. // one way for the Web AIM client and another for the documented JSON API.
  34. func TestMarshalAMF3TagOverridesJSON(t *testing.T) {
  35. m := marshalAMF3Map(t, amf3Tagged{
  36. Sender: "chattingchuck", Message: "hi", Secret: "s", Hidden: "h", unseen: "u",
  37. })
  38. assert.Equal(t, "chattingchuck", m["source"])
  39. assert.NotContains(t, m, "sender")
  40. assert.Equal(t, "hi", m["message"])
  41. // An amf3 tag carrying no omitempty keeps a field the JSON encoding drops.
  42. assert.Contains(t, m, "autoresponse")
  43. assert.Equal(t, false, m["autoresponse"])
  44. assert.Contains(t, m, "state")
  45. assert.Equal(t, "", m["state"])
  46. // "-" suppresses the field whichever tag spells it, and unexported fields
  47. // never appear.
  48. assert.NotContains(t, m, "secret")
  49. assert.NotContains(t, m, "hidden")
  50. assert.NotContains(t, m, "unseen")
  51. // A json omitempty still applies when no amf3 tag overrides it.
  52. assert.NotContains(t, m, "msgId")
  53. }
  54. // AMF3 stores whole numbers in 29 bits, so anything wider has to arrive as a
  55. // double or it is silently truncated on the wire.
  56. func TestMarshalAMF3IntegerRange(t *testing.T) {
  57. tests := []struct {
  58. name string
  59. value any
  60. want any
  61. }{
  62. {"zero", 0, int32(0)},
  63. {"negative", -5, int32(-5)},
  64. {"max int29", maxInt29, int32(maxInt29)},
  65. {"min int29", minInt29, int32(minInt29)},
  66. {"one past max int29", int64(maxInt29 + 1), float64(maxInt29 + 1)},
  67. {"one past min int29", int64(minInt29 - 1), float64(minInt29 - 1)},
  68. {"unix timestamp", int64(1700000000), float64(1700000000)},
  69. {"uint64 in range", uint64(42), int32(42)},
  70. {"uint64 out of range", uint64(1) << 40, float64(uint64(1) << 40)},
  71. {"uint64 max", uint64(1<<64 - 1), float64(uint64(1<<64 - 1))},
  72. {"uint32 out of range", uint32(4000000000), float64(4000000000)},
  73. {"float stays a double", 1.5, 1.5},
  74. {"seqNum", uint64(7), int32(7)},
  75. }
  76. for _, tt := range tests {
  77. t.Run(tt.name, func(t *testing.T) {
  78. m := marshalAMF3Map(t, map[string]any{"n": tt.value})
  79. assert.Equal(t, tt.want, m["n"])
  80. })
  81. }
  82. }
  83. type amf3Embedded struct {
  84. AimID string `json:"aimId"`
  85. }
  86. type amf3WithEmbedded struct {
  87. amf3Embedded
  88. State string `json:"state"`
  89. }
  90. type amf3Nested struct {
  91. Source amf3Embedded `json:"source"`
  92. Ptr *amf3Embedded `json:"ptr,omitempty"`
  93. Absent *amf3Embedded `json:"absent,omitempty"`
  94. Required *amf3Embedded `json:"required"`
  95. Boxed any `json:"boxed"`
  96. List []amf3Embedded `json:"list"`
  97. NilList []string `json:"capabilities"`
  98. Counts map[string]int `json:"counts"`
  99. Any any `json:"any"`
  100. When time.Time `json:"when"`
  101. }
  102. func TestMarshalAMF3NestedValues(t *testing.T) {
  103. when := time.Unix(1700000000, 0).UTC()
  104. m := marshalAMF3Map(t, amf3Nested{
  105. Source: amf3Embedded{AimID: "chuck"},
  106. Ptr: &amf3Embedded{AimID: "fred"},
  107. List: []amf3Embedded{{AimID: "one"}},
  108. Counts: map[string]int{"unread": 3},
  109. Any: amf3Embedded{AimID: "boxed"},
  110. When: when,
  111. })
  112. assert.Equal(t, map[string]any{"aimId": "chuck"}, m["source"])
  113. assert.Equal(t, map[string]any{"aimId": "fred"}, m["ptr"])
  114. assert.Equal(t, []any{map[string]any{"aimId": "one"}}, m["list"])
  115. // A map of a concrete value type reaches the wire; the writer takes only
  116. // map[string]any on its own.
  117. assert.Equal(t, map[string]any{"unread": int32(3)}, m["counts"])
  118. assert.Equal(t, map[string]any{"aimId": "boxed"}, m["any"])
  119. // An AMF3 date is a bare UTC epoch, so the instant survives but the zone
  120. // does not.
  121. decodedWhen, ok := m["when"].(time.Time)
  122. require.True(t, ok, "when decoded as %T", m["when"])
  123. assert.True(t, when.Equal(decodedWhen), "got %s, want %s", decodedWhen, when)
  124. // A nil list is sent as an empty one because the client iterates lists such
  125. // as capabilities unconditionally.
  126. assert.Equal(t, []any{}, m["capabilities"])
  127. // omitempty is what drops a nil, so the client's merge leaves whatever it
  128. // already holds alone.
  129. assert.NotContains(t, m, "absent")
  130. // Without omitempty the field is one the client dereferences on sight, so it
  131. // arrives as an empty object rather than an absent key.
  132. assert.Equal(t, map[string]any{}, m["required"])
  133. assert.Equal(t, map[string]any{}, m["boxed"])
  134. }
  135. // An untagged embedded struct contributes its fields to the enclosing object.
  136. func TestMarshalAMF3PromotesEmbeddedFields(t *testing.T) {
  137. m := marshalAMF3Map(t, amf3WithEmbedded{amf3Embedded: amf3Embedded{AimID: "chuck"}, State: "online"})
  138. assert.Equal(t, map[string]any{"aimId": "chuck", "state": "online"}, m)
  139. }
  140. // A named string encodes as its underlying string, which is what carries an
  141. // event's type constant.
  142. func TestMarshalAMF3NamedString(t *testing.T) {
  143. m := marshalAMF3Map(t, map[string]any{"type": amf3EventType("presence")})
  144. assert.Equal(t, "presence", m["type"])
  145. }
  146. // The AMF3 writer emits nothing for a value it cannot handle, truncating the
  147. // enclosing object mid-key, so an unsupported type has to be an error instead.
  148. func TestMarshalAMF3RejectsUnsupportedTypes(t *testing.T) {
  149. tests := []struct {
  150. name string
  151. value any
  152. }{
  153. {"channel", make(chan int)},
  154. {"func", func() {}},
  155. {"complex", complex(1, 2)},
  156. {"struct carrying a channel", struct {
  157. Ch chan int `json:"ch"`
  158. }{Ch: make(chan int)}},
  159. {"slice of channels", []chan int{make(chan int)}},
  160. }
  161. for _, tt := range tests {
  162. t.Run(tt.name, func(t *testing.T) {
  163. _, err := MarshalAMF3(tt.value)
  164. assert.Error(t, err)
  165. })
  166. }
  167. }
  168. // Every response is an object, so a nil payload is an empty one rather than a
  169. // null the client would dereference.
  170. func TestMarshalAMF3NilIsAnEmptyObject(t *testing.T) {
  171. assert.Equal(t, map[string]any{}, marshalAMF3Map(t, nil))
  172. assert.Equal(t, map[string]any{}, marshalAMF3Map(t, (*amf3Nested)(nil)))
  173. assert.Equal(t, map[string]any{}, marshalAMF3Map(t, struct{}{}))
  174. }
  175. // Byte slices are the one slice written as an AMF3 byte array.
  176. func TestMarshalAMF3ByteSlice(t *testing.T) {
  177. b, err := MarshalAMF3(map[string]any{"raw": []byte{1, 2, 3}})
  178. require.NoError(t, err)
  179. m, ok := goAMF3.DecodeAMF3(b).(map[string]any)
  180. require.True(t, ok)
  181. assert.Equal(t, []byte{1, 2, 3}, m["raw"])
  182. }
  183. // TestMarshalAMF3_OmitZero pins the omitzero option: unlike omitempty it suppresses
  184. // only the zero value, so an empty-but-non-nil slice still encodes.
  185. func TestMarshalAMF3_OmitZero(t *testing.T) {
  186. type payload struct {
  187. Users []string `json:"users,omitzero"`
  188. }
  189. tests := []struct {
  190. name string
  191. value payload
  192. want string
  193. }{
  194. {name: "nil is omitted", value: payload{}, want: ""},
  195. {name: "empty is kept", value: payload{Users: []string{}}, want: "users"},
  196. {name: "populated is kept", value: payload{Users: []string{"bob"}}, want: "users"},
  197. }
  198. for _, tt := range tests {
  199. t.Run(tt.name, func(t *testing.T) {
  200. b, err := MarshalAMF3(tt.value)
  201. assert.NoError(t, err)
  202. if tt.want == "" {
  203. assert.NotContains(t, string(b), "users")
  204. } else {
  205. assert.Contains(t, string(b), "users")
  206. }
  207. })
  208. }
  209. }