common_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. // The Web API nests the envelope under a "response" key in JSON but renders it
  115. // as a flat <response> root in XML. MarshalXML reconciles the two so one struct
  116. // can describe a response in both formats.
  117. func TestEnvelopeMarshalXML(t *testing.T) {
  118. t.Run("renders the flat response root", func(t *testing.T) {
  119. resp := BaseResponse{}
  120. resp.Response.StatusCode = 200
  121. resp.Response.StatusText = "Ok"
  122. resp.Response.RequestID = "123"
  123. resp.Response.Data = struct {
  124. AimSID string `json:"aimsid" xml:"aimsid"`
  125. }{AimSID: "opaquedata"}
  126. out, err := xml.Marshal(resp)
  127. assert.NoError(t, err)
  128. assert.Equal(t,
  129. "<response><statusCode>200</statusCode><statusText>Ok</statusText>"+
  130. "<requestId>123</requestId><data><aimsid>opaquedata</aimsid></data></response>",
  131. string(out))
  132. })
  133. t.Run("renders an empty data element for a response with no payload", func(t *testing.T) {
  134. req := httptest.NewRequest("GET", "/aim/endSession", nil)
  135. resp := BaseResponse{}
  136. resp.Response.StatusCode = 200
  137. resp.Response.StatusText = "Ok"
  138. out, err := xml.Marshal(normalizeEnvelope(req, resp))
  139. assert.NoError(t, err)
  140. assert.Contains(t, string(out), "<data></data>")
  141. })
  142. t.Run("error envelopes share the shape", func(t *testing.T) {
  143. out, err := xml.Marshal(newErrorResponse(400, "bad request"))
  144. assert.NoError(t, err)
  145. assert.Equal(t,
  146. "<response><statusCode>400</statusCode><statusText>bad request</statusText>"+
  147. "<data></data></response>",
  148. string(out))
  149. })
  150. }
  151. // A handler that sets no data still sends one, because the client dereferences
  152. // response.data on any success.
  153. func TestNormalizeEnvelopeSuppliesEmptyData(t *testing.T) {
  154. req := httptest.NewRequest("GET", "/aim/endSession", nil)
  155. got := normalizeEnvelope(req, BaseResponse{}).(BaseResponse)
  156. assert.Equal(t, struct{}{}, got.Response.Data)
  157. }