4
0

amf_shape_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. package webapi
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. goAMF3 "github.com/breign/goAMF3"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. )
  10. // decodeAMF decodes an AMF3 body as the object every Web API reply is.
  11. func decodeAMF(t *testing.T, body []byte) map[string]any {
  12. t.Helper()
  13. decoded := goAMF3.DecodeAMF3(body)
  14. m, ok := decoded.(map[string]any)
  15. require.True(t, ok, "decoded as %T, want an object", decoded)
  16. return m
  17. }
  18. // renderAMF sends payload through the f=amf3 path and returns the response body.
  19. func renderAMF(t *testing.T, data any) map[string]any {
  20. t.Helper()
  21. resp := BaseResponse{}
  22. resp.Response.StatusCode = 200
  23. resp.Response.StatusText = "Ok"
  24. resp.Response.Data = data
  25. rr := httptest.NewRecorder()
  26. SendResponse(rr, httptest.NewRequest(http.MethodGet, "/x?f=amf3&r=123", nil), resp, nil)
  27. require.Contains(t, rr.Header().Get("Content-Type"), "amf")
  28. envelope := decodeAMF(t, rr.Body.Bytes())
  29. body, ok := envelope["response"].(map[string]any)
  30. require.True(t, ok, "response = %#v, want an object", envelope["response"])
  31. return body
  32. }
  33. // object asserts that key holds a nested AMF3 object and returns it.
  34. func object(t *testing.T, m map[string]any, key string) map[string]any {
  35. t.Helper()
  36. v, ok := m[key].(map[string]any)
  37. require.True(t, ok, "%s = %#v, want an object", key, m[key])
  38. return v
  39. }
  40. // firstEvent returns the single event a fetchEvents payload carries.
  41. func firstEvent(t *testing.T, event Event) (map[string]any, map[string]any) {
  42. t.Helper()
  43. body := renderAMF(t, &FetchEventsData{Events: []Event{event}, LastSeqNum: event.SeqNum})
  44. events, ok := object(t, body, "data")["events"].([]any)
  45. require.True(t, ok, "events is not an array")
  46. require.Len(t, events, 1)
  47. wrapper, ok := events[0].(map[string]any)
  48. require.True(t, ok, "events[0] = %#v, want an object", events[0])
  49. return wrapper, object(t, wrapper, "eventData")
  50. }
  51. // The envelope nests the body under "response", where XML flattens it to the
  52. // document root.
  53. func TestAMFEnvelopeMatchesSpec(t *testing.T) {
  54. body := renderAMF(t, struct {
  55. AimSID string `json:"aimsid"`
  56. }{AimSID: "opaquedata"})
  57. assert.Equal(t, int32(200), body["statusCode"])
  58. assert.Equal(t, "Ok", body["statusText"])
  59. assert.Equal(t, "123", body["requestId"])
  60. assert.Equal(t, map[string]any{"aimsid": "opaquedata"}, object(t, body, "data"))
  61. }
  62. // Every method sends a data object even when it carries no payload; the client
  63. // dereferences response.data regardless of outcome.
  64. func TestAMFAlwaysRendersData(t *testing.T) {
  65. rr := httptest.NewRecorder()
  66. resp := BaseResponse{}
  67. resp.Response.StatusCode = 200
  68. resp.Response.StatusText = "Ok"
  69. SendResponse(rr, httptest.NewRequest(http.MethodGet, "/aim/endSession?f=amf3", nil), resp, nil)
  70. body := object(t, decodeAMF(t, rr.Body.Bytes()), "response")
  71. assert.Equal(t, map[string]any{}, object(t, body, "data"))
  72. }
  73. // A failure carries the same envelope, including the data object a client
  74. // callback reaches for on any outcome.
  75. func TestAMFErrorEnvelope(t *testing.T) {
  76. rr := httptest.NewRecorder()
  77. SendErrorDetail(rr, httptest.NewRequest(http.MethodGet, "/x?f=amf3&r=123", nil),
  78. http.StatusUnauthorized, statusMoreAuthRequired, detailBadPassword, "invalid password")
  79. body := object(t, decodeAMF(t, rr.Body.Bytes()), "response")
  80. assert.Equal(t, int32(statusMoreAuthRequired), body["statusCode"])
  81. assert.Equal(t, int32(detailBadPassword), body["statusDetailCode"])
  82. assert.Equal(t, "invalid password", body["statusText"])
  83. assert.Equal(t, "123", body["requestId"])
  84. assert.Equal(t, map[string]any{}, object(t, body, "data"))
  85. }
  86. // AMF3 stores whole numbers in 29 bits, which a Unix timestamp overflows, so
  87. // every time value has to reach the client as a double.
  88. func TestAMFTimestampsAreDoubles(t *testing.T) {
  89. wrapper, eventData := firstEvent(t, Event{
  90. Type: EventTypeIM,
  91. SeqNum: 7,
  92. Timestamp: 1700000000,
  93. Data: IMEvent{
  94. Source: UserInfo{AimID: "chattingchuck", OnlineTime: 1699999000},
  95. Message: "hi",
  96. Timestamp: 1700000000,
  97. },
  98. })
  99. assert.Equal(t, float64(1700000000), wrapper["timestamp"])
  100. assert.Equal(t, float64(1700000000), eventData["timestamp"])
  101. assert.Equal(t, float64(1699999000), object(t, eventData, "source")["onlineTime"])
  102. // A sequence number is small enough to stay a compact AMF3 integer.
  103. assert.Equal(t, int32(7), wrapper["seqNum"])
  104. }
  105. // myInfo carries the timestamps most likely to overflow, and reaches the encoder
  106. // as a struct rather than a map.
  107. func TestAMFMyInfoTimestampsAreDoubles(t *testing.T) {
  108. mi := buildMyInfo("ChattingChuck", "online", "")
  109. mi.OnlineTime = 1700000000
  110. mi.MemberSince = 1500000000
  111. mi.Self = &MyInfoSelf{InstNum: 1, LoginTime: 1700000001, Events: []string{}, AssertCaps: []string{}}
  112. body := renderAMF(t, mi)
  113. data := object(t, body, "data")
  114. assert.Equal(t, float64(1700000000), data["onlineTime"])
  115. assert.Equal(t, float64(1500000000), data["memberSince"])
  116. assert.Equal(t, float64(1700000001), object(t, data, "self")["loginTime"])
  117. // A list the client iterates unconditionally is sent even when empty.
  118. assert.Equal(t, []any{}, data["capabilities"])
  119. }
  120. // The client reads the sender of a sentIM from "source" and the flag from
  121. // "autoresponse", which is not how the JSON spec names either one.
  122. func TestAMFSentIMUsesClientFieldNames(t *testing.T) {
  123. _, eventData := firstEvent(t, Event{
  124. Type: EventTypeSentIM,
  125. SeqNum: 1,
  126. Data: SentIMEvent{
  127. Sender: UserInfo{AimID: "chattingchuck", DisplayID: "ChattingChuck", UserType: "aim", State: "online"},
  128. Dest: UserInfo{AimID: "fred", DisplayID: "Fred", Friendly: "Freddy", UserType: "aim", State: "online"},
  129. Message: "hi",
  130. MsgID: "beefcafe",
  131. },
  132. })
  133. source := object(t, eventData, "source")
  134. assert.Equal(t, "chattingchuck", source["aimId"])
  135. assert.Equal(t, "online", source["state"])
  136. assert.NotContains(t, eventData, "sender")
  137. dest := object(t, eventData, "dest")
  138. assert.Equal(t, "fred", dest["aimId"])
  139. // The client's merge deletes any alias it holds before applying a user map,
  140. // so an alias has to be repeated on every one.
  141. assert.Equal(t, "Freddy", dest["friendly"])
  142. assert.Equal(t, "beefcafe", eventData["msgId"])
  143. // Always sent, false included, and never under the JSON spelling.
  144. assert.Equal(t, false, eventData["autoresponse"])
  145. assert.NotContains(t, eventData, "autoResponse")
  146. }
  147. // Presence is the whole user object the client's parseUser reads, not a subset.
  148. func TestAMFPresenceCarriesEveryField(t *testing.T) {
  149. _, eventData := firstEvent(t, Event{
  150. Type: EventTypePresence,
  151. SeqNum: 1,
  152. Data: PresenceEvent{
  153. AimID: "mikekelly",
  154. Friendly: "Mike",
  155. State: "away",
  156. StatusMsg: "at lunch",
  157. AwayMsg: "back soon",
  158. IdleTime: 5,
  159. OnlineTime: 1700000000,
  160. UserType: "aim",
  161. BuddyIcon: "http://host/icon",
  162. },
  163. })
  164. assert.Equal(t, "mikekelly", eventData["aimId"])
  165. assert.Equal(t, "Mike", eventData["friendly"])
  166. assert.Equal(t, "away", eventData["state"])
  167. assert.Equal(t, "at lunch", eventData["statusMsg"])
  168. assert.Equal(t, "back soon", eventData["awayMsg"])
  169. assert.Equal(t, int32(5), eventData["idleTime"])
  170. assert.Equal(t, float64(1700000000), eventData["onlineTime"])
  171. assert.Equal(t, "aim", eventData["userType"])
  172. assert.Equal(t, "http://host/icon", eventData["buddyIcon"])
  173. }
  174. // An empty buddyIcon is left out so the client's merge keeps the icon it holds;
  175. // the placeholder URL, not "", is what clears one.
  176. func TestAMFPresenceOmitsEmptyBuddyIcon(t *testing.T) {
  177. _, eventData := firstEvent(t, Event{
  178. Type: EventTypePresence,
  179. SeqNum: 1,
  180. Data: PresenceEvent{AimID: "mikekelly", State: "offline", UserType: "aim"},
  181. })
  182. assert.NotContains(t, eventData, "buddyIcon")
  183. }
  184. // An offline sender is identified by aimId and friendly alone, and the client
  185. // keys its conversation list by msgId.
  186. func TestAMFOfflineIM(t *testing.T) {
  187. _, eventData := firstEvent(t, Event{
  188. Type: EventTypeOfflineIM,
  189. SeqNum: 1,
  190. Data: OfflineIMEvent{
  191. AimID: "mikekelly",
  192. Friendly: "Mike Kelly",
  193. Message: "sent while you were out",
  194. MsgID: "beefcafe",
  195. Timestamp: 1700000000,
  196. },
  197. })
  198. assert.Equal(t, "mikekelly", eventData["aimId"])
  199. assert.Equal(t, "Mike Kelly", eventData["friendly"])
  200. assert.Equal(t, "sent while you were out", eventData["message"])
  201. assert.Equal(t, "beefcafe", eventData["msgId"])
  202. assert.Equal(t, float64(1700000000), eventData["timestamp"])
  203. assert.Equal(t, false, eventData["autoresponse"])
  204. }
  205. // A payload of nested structs and lists survives whole; the AMF3 writer emits
  206. // nothing for a value it cannot take, truncating the object mid-key.
  207. func TestAMFNestedPayloadSurvives(t *testing.T) {
  208. _, eventData := firstEvent(t, Event{
  209. Type: EventTypeBuddyList,
  210. SeqNum: 1,
  211. Data: &BuddyListData{Groups: []BuddyGroup{{
  212. Name: "Friends",
  213. Buddies: []BuddyInfo{{
  214. AimID: "chattingchuck",
  215. DisplayID: "ChattingChuck",
  216. Capabilities: []string{"200A0000A28911D3A52D001083341CFA"},
  217. }},
  218. }}},
  219. })
  220. groups, ok := eventData["groups"].([]any)
  221. require.True(t, ok, "groups = %#v, want an array", eventData["groups"])
  222. require.Len(t, groups, 1)
  223. group, ok := groups[0].(map[string]any)
  224. require.True(t, ok)
  225. assert.Equal(t, "Friends", group["name"])
  226. buddies, ok := group["buddies"].([]any)
  227. require.True(t, ok)
  228. require.Len(t, buddies, 1)
  229. buddy, ok := buddies[0].(map[string]any)
  230. require.True(t, ok)
  231. assert.Equal(t, "chattingchuck", buddy["aimId"])
  232. assert.Equal(t, []any{"200A0000A28911D3A52D001083341CFA"}, buddy["capabilities"])
  233. }
  234. // A typed nil payload is an empty object rather than a truncated stream.
  235. func TestAMFNilEventDataIsAnObject(t *testing.T) {
  236. wrapper, _ := firstEvent(t, Event{
  237. Type: EventTypeBuddyList,
  238. SeqNum: 3,
  239. Data: (*BuddyListData)(nil),
  240. })
  241. assert.Equal(t, int32(3), wrapper["seqNum"])
  242. }