xtraz_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. package wire
  2. import (
  3. "encoding/binary"
  4. "encoding/hex"
  5. "math"
  6. "strings"
  7. "testing"
  8. "github.com/google/uuid"
  9. "github.com/stretchr/testify/assert"
  10. )
  11. func TestUnmangleXtrazXML(t *testing.T) {
  12. tests := []struct {
  13. name string
  14. mangled string
  15. want string
  16. }{
  17. {
  18. name: "unmangle basic entities",
  19. mangled: "<N><QUERY></QUERY></N>",
  20. want: "<N><QUERY></QUERY></N>",
  21. },
  22. {
  23. name: "unmangle all entities",
  24. mangled: "&lt;tag attr=&quot;value&quot;&gt;text &amp; more&lt;/tag&gt;",
  25. want: `<tag attr="value">text & more</tag>`,
  26. },
  27. {
  28. name: "plain text unchanged",
  29. mangled: "plain text",
  30. want: "plain text",
  31. },
  32. {
  33. name: "empty string",
  34. mangled: "",
  35. want: "",
  36. },
  37. }
  38. for _, tt := range tests {
  39. t.Run(tt.name, func(t *testing.T) {
  40. got := UnmangleXtrazXML(tt.mangled)
  41. assert.Equal(t, tt.want, got)
  42. })
  43. }
  44. }
  45. func TestMangleXtrazXML(t *testing.T) {
  46. tests := []struct {
  47. name string
  48. plain string
  49. want string
  50. }{
  51. {
  52. name: "mangle basic XML",
  53. plain: "<N><QUERY></QUERY></N>",
  54. want: "&lt;N&gt;&lt;QUERY&gt;&lt;/QUERY&gt;&lt;/N&gt;",
  55. },
  56. {
  57. name: "mangle special chars",
  58. plain: `<tag attr="value">text & more</tag>`,
  59. want: "&lt;tag attr=&#34;value&#34;&gt;text &amp; more&lt;/tag&gt;",
  60. },
  61. {
  62. name: "plain text unchanged",
  63. plain: "plain text",
  64. want: "plain text",
  65. },
  66. {
  67. name: "empty string",
  68. plain: "",
  69. want: "",
  70. },
  71. }
  72. for _, tt := range tests {
  73. t.Run(tt.name, func(t *testing.T) {
  74. got := MangleXtrazXML(tt.plain)
  75. assert.Equal(t, tt.want, got)
  76. })
  77. }
  78. }
  79. func TestParseXtrazNotifyRequest(t *testing.T) {
  80. tests := []struct {
  81. name string
  82. xml string
  83. want *XtrazNotifyRequest
  84. wantErr bool
  85. }{
  86. {
  87. name: "parse valid XStatus request",
  88. xml: `<N><QUERY><PluginID>srvMng</PluginID></QUERY>` +
  89. `<NOTIFY><srv><id>cAwaySrv</id>` +
  90. `<req><id>AwayStat</id><trans>1</trans><senderId>123456</senderId></req>` +
  91. `</srv></NOTIFY></N>`,
  92. want: &XtrazNotifyRequest{
  93. PluginID: "srvMng",
  94. ServiceID: "cAwaySrv",
  95. RequestID: "AwayStat",
  96. TransID: "1",
  97. SenderID: "123456",
  98. },
  99. wantErr: false,
  100. },
  101. {
  102. name: "parse invalid XML",
  103. xml: "<invalid>",
  104. want: nil,
  105. wantErr: true,
  106. },
  107. {
  108. name: "parse empty XML",
  109. xml: "",
  110. want: nil,
  111. wantErr: true,
  112. },
  113. }
  114. for _, tt := range tests {
  115. t.Run(tt.name, func(t *testing.T) {
  116. got, err := ParseXtrazNotifyRequest([]byte(tt.xml))
  117. if tt.wantErr {
  118. assert.Error(t, err)
  119. return
  120. }
  121. assert.NoError(t, err)
  122. assert.Equal(t, tt.want, got)
  123. })
  124. }
  125. }
  126. func TestParseXtrazNotifyResponse(t *testing.T) {
  127. tests := []struct {
  128. name string
  129. xml string
  130. want *XtrazNotifyResponse
  131. wantErr bool
  132. }{
  133. {
  134. name: "parse valid XStatus response",
  135. xml: `<NR><RES><ret event="OnRemoteNotification"><srv><id></id>` +
  136. `<val srv_id="cAwaySrv"><Root><CASXtraSetAwayMessage></CASXtraSetAwayMessage>` +
  137. `<uin>123456</uin><index>5</index><title>Having a beer</title>` +
  138. `<desc>Cheers!</desc></Root></val></srv></ret></RES></NR>`,
  139. want: &XtrazNotifyResponse{
  140. UIN: "123456",
  141. Index: 5,
  142. Title: "Having a beer",
  143. Message: "Cheers!",
  144. },
  145. wantErr: false,
  146. },
  147. {
  148. name: "parse XML without Root element",
  149. xml: "<NR><RES></RES></NR>",
  150. want: nil,
  151. wantErr: true,
  152. },
  153. {
  154. name: "parse empty XML",
  155. xml: "",
  156. want: nil,
  157. wantErr: true,
  158. },
  159. }
  160. for _, tt := range tests {
  161. t.Run(tt.name, func(t *testing.T) {
  162. got, err := ParseXtrazNotifyResponse([]byte(tt.xml))
  163. if tt.wantErr {
  164. assert.Error(t, err)
  165. return
  166. }
  167. assert.NoError(t, err)
  168. assert.Equal(t, tt.want, got)
  169. })
  170. }
  171. }
  172. func TestBuildXtrazNotifyRequest(t *testing.T) {
  173. senderUIN := "123456"
  174. result := BuildXtrazNotifyRequest(senderUIN)
  175. // Unmangle and verify the structure
  176. unmangled := UnmangleXtrazXML(result)
  177. assert.Contains(t, unmangled, "<N>")
  178. assert.Contains(t, unmangled, "<PluginID>srvMng</PluginID>")
  179. assert.Contains(t, unmangled, "<senderId>123456</senderId>")
  180. assert.Contains(t, unmangled, "<id>AwayStat</id>")
  181. assert.Contains(t, unmangled, "<id>cAwaySrv</id>")
  182. // Verify it can be parsed back
  183. parsed, err := ParseXtrazNotifyRequest([]byte(unmangled))
  184. assert.NoError(t, err)
  185. assert.Equal(t, "srvMng", parsed.PluginID)
  186. assert.Equal(t, "cAwaySrv", parsed.ServiceID)
  187. assert.Equal(t, "AwayStat", parsed.RequestID)
  188. assert.Equal(t, senderUIN, parsed.SenderID)
  189. }
  190. func TestBuildXtrazNotifyResponse(t *testing.T) {
  191. uin := "123456"
  192. index := uint8(5)
  193. title := "Having a beer"
  194. message := "Cheers!"
  195. result := BuildXtrazNotifyResponse(uin, index, title, message)
  196. // Unmangle and verify the structure
  197. unmangled := UnmangleXtrazXML(result)
  198. assert.Contains(t, unmangled, "<NR>")
  199. assert.Contains(t, unmangled, "<uin>123456</uin>")
  200. assert.Contains(t, unmangled, "<index>5</index>")
  201. // Verify it can be parsed back
  202. parsed, err := ParseXtrazNotifyResponse([]byte(unmangled))
  203. assert.NoError(t, err)
  204. assert.Equal(t, uin, parsed.UIN)
  205. assert.Equal(t, index, parsed.Index)
  206. }
  207. func TestXtrazCapabilityGUID(t *testing.T) {
  208. // Verify the GUID matches the expected GUID
  209. expected := "3b60b3ef-d82a-6c45-a4e0-9c5a5e67e865"
  210. assert.Equal(t, expected, CapXtrazScript.String())
  211. }
  212. func TestXStatusConstants(t *testing.T) {
  213. assert.Equal(t, uint8(1), XStatusAngry)
  214. assert.Equal(t, uint8(32), XStatusCoffee2)
  215. }
  216. func TestMoodByID(t *testing.T) {
  217. tests := []struct {
  218. name string
  219. id string
  220. want Mood
  221. ok bool
  222. }{
  223. {
  224. name: "mood with a real ICQ capability",
  225. id: "0icqmood6",
  226. want: Mood{ID: "0icqmood6", Cap: CapXStatusPlate},
  227. ok: true,
  228. },
  229. {
  230. name: "mood with a placeholder capability",
  231. id: "0icqmood13",
  232. want: Mood{ID: "0icqmood13", Cap: CapMoodHavingFun},
  233. ok: true,
  234. },
  235. {
  236. name: "unknown token",
  237. id: "0icqmood999",
  238. },
  239. {
  240. name: "token missing the leading zero",
  241. id: "icqmood6",
  242. },
  243. {
  244. name: "empty token",
  245. },
  246. }
  247. for _, tt := range tests {
  248. t.Run(tt.name, func(t *testing.T) {
  249. got, ok := MoodByID(tt.id)
  250. assert.Equal(t, tt.ok, ok)
  251. assert.Equal(t, tt.want, got)
  252. })
  253. }
  254. }
  255. func TestMoodByCap(t *testing.T) {
  256. tests := []struct {
  257. name string
  258. cap uuid.UUID
  259. wantID string
  260. ok bool
  261. }{
  262. {
  263. name: "capability used by one mood",
  264. cap: CapXStatusBeer,
  265. wantID: "0icqmood4",
  266. ok: true,
  267. },
  268. {
  269. name: "capability shared by two moods returns the canonical one",
  270. cap: CapXStatusConsole,
  271. wantID: "0icqmood81",
  272. ok: true,
  273. },
  274. {
  275. name: "other shared capability returns the canonical one",
  276. cap: CapXStatusSleeping,
  277. wantID: "0icqmood70",
  278. ok: true,
  279. },
  280. {
  281. name: "placeholder capability",
  282. cap: CapMoodOnTheWay,
  283. wantID: "0icqmood83",
  284. ok: true,
  285. },
  286. {
  287. name: "capability that is not a mood",
  288. cap: CapChat,
  289. },
  290. {
  291. name: "nil capability",
  292. cap: uuid.Nil,
  293. },
  294. }
  295. for _, tt := range tests {
  296. t.Run(tt.name, func(t *testing.T) {
  297. got, ok := MoodByCap(tt.cap)
  298. assert.Equal(t, tt.ok, ok)
  299. assert.Equal(t, tt.wantID, got.ID)
  300. assert.Equal(t, tt.ok, IsMoodCap(tt.cap))
  301. })
  302. }
  303. }
  304. // TestMoodsRoundTrip verifies every token resolves to itself and every
  305. // capability to a token that carries it.
  306. func TestMoodsRoundTrip(t *testing.T) {
  307. canonical := make(map[uuid.UUID]string)
  308. for _, m := range Moods {
  309. t.Run(m.ID, func(t *testing.T) {
  310. byID, ok := MoodByID(m.ID)
  311. assert.True(t, ok)
  312. assert.Equal(t, m, byID)
  313. // A mood with no capability is invisible to other users.
  314. assert.NotEqual(t, uuid.Nil, m.Cap)
  315. byCap, ok := MoodByCap(m.Cap)
  316. assert.True(t, ok)
  317. assert.Equal(t, m.Cap, byCap.Cap)
  318. })
  319. if first, seen := canonical[m.Cap]; seen {
  320. // A shared capability resolves to whichever mood is listed first.
  321. byCap, _ := MoodByCap(m.Cap)
  322. assert.Equal(t, first, byCap.ID)
  323. continue
  324. }
  325. canonical[m.Cap] = m.ID
  326. }
  327. }
  328. func TestMoodIconID(t *testing.T) {
  329. tests := []struct {
  330. name string
  331. id string
  332. want string
  333. }{
  334. {
  335. name: "encodes length prefix and token bytes",
  336. id: "0icqmood6",
  337. want: "0009306963716d6f6f6436",
  338. },
  339. {
  340. name: "encodes a two digit mood",
  341. id: "0icqmood23",
  342. want: "000a306963716d6f6f643233",
  343. },
  344. {
  345. name: "empty token encodes to nothing",
  346. id: "",
  347. want: "",
  348. },
  349. {
  350. name: "oversized token encodes to nothing",
  351. id: strings.Repeat("a", math.MaxUint16+1),
  352. want: "",
  353. },
  354. }
  355. for _, tt := range tests {
  356. t.Run(tt.name, func(t *testing.T) {
  357. assert.Equal(t, tt.want, MoodIconID(tt.id))
  358. })
  359. }
  360. }
  361. // TestMoodIconIDDecodes verifies every token encodes to something ICQ clients can
  362. // decode: even-length hex whose length prefix matches the token that follows.
  363. func TestMoodIconIDDecodes(t *testing.T) {
  364. for _, m := range Moods {
  365. t.Run(m.ID, func(t *testing.T) {
  366. encoded := MoodIconID(m.ID)
  367. assert.Zero(t, len(encoded)%2)
  368. raw, err := hex.DecodeString(encoded)
  369. assert.NoError(t, err)
  370. assert.Equal(t, len(m.ID), int(binary.BigEndian.Uint16(raw[:2])))
  371. assert.Equal(t, m.ID, string(raw[2:]))
  372. })
  373. }
  374. }