4
0

xml_shape_test.go 5.0 KB

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