xml_shape_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package handlers
  2. import (
  3. "encoding/xml"
  4. "net/http"
  5. "net/http/httptest"
  6. "testing"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. "github.com/mk6i/open-oscar-server/server/webapi/types"
  10. "github.com/mk6i/open-oscar-server/state"
  11. )
  12. // renderXML sends payload through the f=xml path and returns the body.
  13. func renderXML(t *testing.T, data any) string {
  14. t.Helper()
  15. resp := BaseResponse{}
  16. resp.Response.StatusCode = 200
  17. resp.Response.StatusText = "Ok"
  18. resp.Response.RequestID = "123"
  19. resp.Response.Data = data
  20. rr := httptest.NewRecorder()
  21. SendResponse(rr, httptest.NewRequest(http.MethodGet, "/x?f=xml&r=123", nil), resp, nil)
  22. body := rr.Body.String()
  23. require.Contains(t, rr.Header().Get("Content-Type"), "xml")
  24. // A document the client could not parse is the failure this guards against.
  25. var probe any
  26. require.NoError(t, xml.Unmarshal([]byte(body), &probe), "not well-formed: %s", body)
  27. return body
  28. }
  29. // The spec renders the envelope as a flat <response> root carrying statusCode,
  30. // statusText, requestId and data — not the "response"-keyed nesting JSON uses.
  31. func TestXMLEnvelopeMatchesSpec(t *testing.T) {
  32. body := renderXML(t, struct {
  33. AimSID string `xml:"aimsid"`
  34. }{AimSID: "opaquedata"})
  35. assert.Contains(t, body, `<?xml version="1.0" encoding="UTF-8"?>`)
  36. assert.Contains(t, body, "<response><statusCode>200</statusCode><statusText>Ok</statusText>"+
  37. "<requestId>123</requestId><data><aimsid>opaquedata</aimsid></data></response>")
  38. }
  39. // Every method in the spec renders a data element even when it carries no
  40. // payload; the client dereferences response.data regardless of outcome.
  41. func TestXMLAlwaysRendersData(t *testing.T) {
  42. rr := httptest.NewRecorder()
  43. resp := BaseResponse{}
  44. resp.Response.StatusCode = 200
  45. resp.Response.StatusText = "Ok"
  46. SendResponse(rr, httptest.NewRequest(http.MethodGet, "/aim/endSession?f=xml", nil), resp, nil)
  47. assert.Contains(t, rr.Body.String(), "<data></data>")
  48. }
  49. // The item names inside a list element are fixed by the spec and cannot be
  50. // derived from the container's name (allows holds allow, not user).
  51. func TestXMLItemNamesMatchSpec(t *testing.T) {
  52. t.Run("buddy list", func(t *testing.T) {
  53. body := renderXML(t, &BuddyListData{Groups: []WebAPIBuddyGroup{{
  54. Name: "Friends",
  55. Buddies: []WebAPIBuddyInfo{{
  56. AimID: "chattingchuck",
  57. DisplayID: "ChattingChuck",
  58. State: "away",
  59. UserType: "aim",
  60. Capabilities: []string{"200A0000A28911D3A52D001083341CFA"},
  61. }},
  62. }}})
  63. assert.Contains(t, body, "<groups><group><name>Friends</name>")
  64. assert.Contains(t, body, "<buddies><buddy><aimId>chattingchuck</aimId>")
  65. assert.Contains(t, body, "<capabilities><capability>200A0000A28911D3A52D001083341CFA</capability></capabilities>")
  66. // The Go field names must not leak through as element names.
  67. assert.NotContains(t, body, "<AimID>")
  68. assert.NotContains(t, body, "<Buddies>")
  69. })
  70. t.Run("permit deny", func(t *testing.T) {
  71. body := renderXML(t, PermitDenyData{
  72. PDMode: "permitOnList",
  73. PermitList: []string{"ChattingChuck"},
  74. DenyList: []string{"fred"},
  75. })
  76. assert.Contains(t, body, "<pdMode>permitOnList</pdMode>")
  77. assert.Contains(t, body, "<allows><allow>ChattingChuck</allow></allows>")
  78. assert.Contains(t, body, "<blocks><block>fred</block></blocks>")
  79. })
  80. t.Run("presence", func(t *testing.T) {
  81. body := renderXML(t, PresenceData{Users: []BuddyPresenceInfo{{
  82. AimID: "chattingchuck", State: "away", UserType: "aim",
  83. }}})
  84. assert.Contains(t, body, "<users><user><aimId>chattingchuck</aimId>")
  85. })
  86. t.Run("fetch events", func(t *testing.T) {
  87. body := renderXML(t, &FetchEventsData{
  88. Events: []types.Event{{
  89. Type: types.EventTypeTyping,
  90. SeqNum: 7,
  91. Timestamp: 100,
  92. Data: types.TypingEvent{AimID: "chattingchuck", TypingStatus: "typing"},
  93. }},
  94. LastSeqNum: 7,
  95. })
  96. assert.Contains(t, body, "<events><event><type>typing</type><seqNum>7</seqNum>")
  97. assert.Contains(t, body, "<eventData><aimId>chattingchuck</aimId><typingStatus>typing</typingStatus></eventData>")
  98. })
  99. }
  100. // myInfo is the payload the spec documents in the most detail, and the one the
  101. // old hand-built XML truncated to two fields.
  102. func TestXMLMyInfoCarriesEveryField(t *testing.T) {
  103. mi := buildMyInfo(state.DisplayScreenName("ChattingChuck"), "away", "http://host/icon")
  104. mi.OnlineTime = 100
  105. mi.AwayMsg = "I'm busy right now chatting."
  106. body := renderXML(t, mi)
  107. for _, want := range []string{
  108. "<aimId>chattingchuck</aimId>",
  109. "<displayId>ChattingChuck</displayId>",
  110. "<friendly>ChattingChuck</friendly>",
  111. "<state>away</state>",
  112. "<userType>aim</userType>",
  113. "<awayMsg>I&#39;m busy right now chatting.</awayMsg>",
  114. "<buddyIcon>http://host/icon</buddyIcon>",
  115. "<onlineTime>100</onlineTime>",
  116. } {
  117. assert.Contains(t, body, want)
  118. }
  119. }
  120. // Preferences are the one payload that is legitimately partial, so an absent
  121. // preference and one set to zero have to stay distinguishable in XML too.
  122. func TestXMLPreferencesDistinguishAbsentFromZero(t *testing.T) {
  123. prefs := &PreferenceData{}
  124. prefs.Set("showGroups", 0)
  125. body := renderXML(t, prefs)
  126. assert.Contains(t, body, "<showGroups>0</showGroups>")
  127. assert.NotContains(t, body, "<playIMSound>")
  128. }