4
0

common_test.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package handlers
  2. import (
  3. "encoding/xml"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. )
  11. func TestNormalizeEnvelope(t *testing.T) {
  12. t.Run("sets requestId from r query param", func(t *testing.T) {
  13. req := httptest.NewRequest("GET", "/buddylist/addBuddy?r=abc123", nil)
  14. data := normalizeEnvelope(req, BaseResponse{
  15. Response: ResponseBody{
  16. StatusCode: 200,
  17. StatusText: "OK",
  18. },
  19. })
  20. br, ok := data.(BaseResponse)
  21. assert.True(t, ok)
  22. assert.Equal(t, "abc123", br.Response.RequestID)
  23. })
  24. t.Run("preserves explicit requestId", func(t *testing.T) {
  25. req := httptest.NewRequest("GET", "/buddylist/addBuddy?r=abc123", nil)
  26. data := normalizeEnvelope(req, BaseResponse{
  27. Response: ResponseBody{
  28. StatusCode: 200,
  29. StatusText: "OK",
  30. RequestID: "existing",
  31. },
  32. })
  33. br, ok := data.(BaseResponse)
  34. assert.True(t, ok)
  35. assert.Equal(t, "existing", br.Response.RequestID)
  36. })
  37. t.Run("no-op without r param", func(t *testing.T) {
  38. req := httptest.NewRequest("GET", "/buddylist/addBuddy", nil)
  39. data := normalizeEnvelope(req, BaseResponse{
  40. Response: ResponseBody{
  41. StatusCode: 200,
  42. StatusText: "OK",
  43. },
  44. })
  45. br, ok := data.(BaseResponse)
  46. assert.True(t, ok)
  47. assert.Empty(t, br.Response.RequestID)
  48. })
  49. }
  50. func TestSendResponseIncludesRequestID(t *testing.T) {
  51. req := httptest.NewRequest("GET", "/buddylist/addBuddy?r=req-42&f=json", nil)
  52. rr := httptest.NewRecorder()
  53. resp := BaseResponse{
  54. Response: ResponseBody{
  55. StatusCode: 200,
  56. StatusText: "OK",
  57. Data: map[string]string{"resultCode": "success"},
  58. },
  59. }
  60. SendResponse(rr, req, resp, slog.Default())
  61. assert.Equal(t, http.StatusOK, rr.Code)
  62. body := strings.TrimSpace(rr.Body.String())
  63. assert.Equal(t, `{"response":{"statusCode":200,"statusText":"OK","requestId":"req-42","data":{"resultCode":"success"}}}`, body)
  64. }
  65. // A JSONP error must arrive as an executable callback, not as bare JSON in a
  66. // <script> tag, which the Web AIM client reports as "Failed to load script tag".
  67. func TestSendErrorJSONP(t *testing.T) {
  68. t.Run("wraps the envelope in the callback", func(t *testing.T) {
  69. req := httptest.NewRequest("GET", "/im/sendIM?c=_callbacks_._0mq8&r=42", nil)
  70. w := httptest.NewRecorder()
  71. SendError(w, req, http.StatusUnauthorized, "invalid or expired session")
  72. body := w.Body.String()
  73. assert.True(t, strings.HasPrefix(body, "_callbacks_._0mq8("), "body should open with the callback: %s", body)
  74. assert.True(t, strings.HasSuffix(body, ");"), "body should close the call: %s", body)
  75. assert.Contains(t, body, `"statusCode":401`)
  76. assert.Contains(t, body, `"statusText":"invalid or expired session"`)
  77. assert.Contains(t, w.Header().Get("Content-Type"), "javascript")
  78. })
  79. t.Run("uses HTTP 200 so the script executes", func(t *testing.T) {
  80. req := httptest.NewRequest("GET", "/im/sendIM?c=cb&r=42", nil)
  81. w := httptest.NewRecorder()
  82. SendError(w, req, http.StatusUnauthorized, "nope")
  83. assert.Equal(t, http.StatusOK, w.Code)
  84. })
  85. t.Run("echoes requestId so the client can match the reply", func(t *testing.T) {
  86. req := httptest.NewRequest("GET", "/im/sendIM?c=cb&r=42", nil)
  87. w := httptest.NewRecorder()
  88. SendError(w, req, http.StatusBadRequest, "bad")
  89. assert.Contains(t, w.Body.String(), `"requestId":"42"`)
  90. })
  91. t.Run("accepts the callback alias", func(t *testing.T) {
  92. req := httptest.NewRequest("GET", "/im/sendIM?callback=cb", nil)
  93. w := httptest.NewRecorder()
  94. SendError(w, req, http.StatusBadRequest, "bad")
  95. assert.True(t, strings.HasPrefix(w.Body.String(), "cb("))
  96. })
  97. t.Run("rejects an unsafe callback name rather than reflecting it", func(t *testing.T) {
  98. req := httptest.NewRequest("GET", "/im/sendIM?c=alert(1)//", nil)
  99. w := httptest.NewRecorder()
  100. SendError(w, req, http.StatusBadRequest, "bad")
  101. assert.NotContains(t, w.Body.String(), "alert(1)")
  102. assert.Contains(t, w.Header().Get("Content-Type"), "json")
  103. })
  104. }
  105. // Without a callback the response stays plain JSON carrying the HTTP status.
  106. func TestSendErrorJSONFallback(t *testing.T) {
  107. req := httptest.NewRequest("GET", "/im/sendIM?r=42", nil)
  108. w := httptest.NewRecorder()
  109. SendError(w, req, http.StatusNotFound, "not found")
  110. assert.Equal(t, http.StatusNotFound, w.Code)
  111. assert.Contains(t, w.Header().Get("Content-Type"), "json")
  112. assert.Contains(t, w.Body.String(), `"statusCode":404`)
  113. }
  114. // An error takes the format the client asked for in "f", the same signal
  115. // SendResponse honors. A client that gets a format it cannot parse reports an
  116. // unreadable response instead of the statusText.
  117. func TestSendErrorHonorsRequestedFormat(t *testing.T) {
  118. t.Run("xml", func(t *testing.T) {
  119. req := httptest.NewRequest("GET", "/im/sendIM?f=xml", nil)
  120. w := httptest.NewRecorder()
  121. SendError(w, req, http.StatusNotFound, "not found")
  122. assert.Contains(t, w.Header().Get("Content-Type"), "xml")
  123. assert.Contains(t, w.Body.String(), "<statusCode>404</statusCode>")
  124. })
  125. // AMF is binary, so the envelope is unreadable as text; the Content-Type is
  126. // what says the encoder ran rather than the JSON fallback.
  127. for _, format := range []string{"amf", "amf3"} {
  128. t.Run(format, func(t *testing.T) {
  129. req := httptest.NewRequest("GET", "/im/sendIM?f="+format, nil)
  130. w := httptest.NewRecorder()
  131. SendError(w, req, http.StatusNotFound, "not found")
  132. assert.Contains(t, w.Header().Get("Content-Type"), "amf")
  133. assert.NotEmpty(t, w.Body.Bytes())
  134. })
  135. }
  136. // A callback outranks the format: the client is on the <script> transport
  137. // and needs executable JS whatever "f" says.
  138. t.Run("a callback outranks f", func(t *testing.T) {
  139. req := httptest.NewRequest("GET", "/im/sendIM?f=xml&c=cb", nil)
  140. w := httptest.NewRecorder()
  141. SendError(w, req, http.StatusNotFound, "not found")
  142. assert.Contains(t, w.Header().Get("Content-Type"), "javascript")
  143. assert.Contains(t, w.Body.String(), "cb(")
  144. })
  145. }
  146. // The Web API nests the envelope under a "response" key in JSON but renders it
  147. // as a flat <response> root in XML. MarshalXML reconciles the two so one struct
  148. // can describe a response in both formats.
  149. func TestEnvelopeMarshalXML(t *testing.T) {
  150. t.Run("renders the flat response root", func(t *testing.T) {
  151. resp := BaseResponse{}
  152. resp.Response.StatusCode = 200
  153. resp.Response.StatusText = "Ok"
  154. resp.Response.RequestID = "123"
  155. resp.Response.Data = struct {
  156. AimSID string `json:"aimsid" xml:"aimsid"`
  157. }{AimSID: "opaquedata"}
  158. out, err := xml.Marshal(resp)
  159. assert.NoError(t, err)
  160. assert.Equal(t,
  161. "<response><statusCode>200</statusCode><statusText>Ok</statusText>"+
  162. "<requestId>123</requestId><data><aimsid>opaquedata</aimsid></data></response>",
  163. string(out))
  164. })
  165. t.Run("renders an empty data element for a response with no payload", func(t *testing.T) {
  166. req := httptest.NewRequest("GET", "/aim/endSession", nil)
  167. resp := BaseResponse{}
  168. resp.Response.StatusCode = 200
  169. resp.Response.StatusText = "Ok"
  170. out, err := xml.Marshal(normalizeEnvelope(req, resp))
  171. assert.NoError(t, err)
  172. assert.Contains(t, string(out), "<data></data>")
  173. })
  174. t.Run("error envelopes share the shape", func(t *testing.T) {
  175. out, err := xml.Marshal(newErrorResponse(400, "bad request"))
  176. assert.NoError(t, err)
  177. assert.Equal(t,
  178. "<response><statusCode>400</statusCode><statusText>bad request</statusText>"+
  179. "<data></data></response>",
  180. string(out))
  181. })
  182. }
  183. // A handler that sets no data still sends one, because the client dereferences
  184. // response.data on any success.
  185. func TestNormalizeEnvelopeSuppliesEmptyData(t *testing.T) {
  186. req := httptest.NewRequest("GET", "/aim/endSession", nil)
  187. got := normalizeEnvelope(req, BaseResponse{}).(BaseResponse)
  188. assert.Equal(t, struct{}{}, got.Response.Data)
  189. }