handler_test.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. package icq_legacy
  2. import (
  3. "log/slog"
  4. "testing"
  5. "github.com/mk6i/open-oscar-server/config"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/mock"
  8. )
  9. // newTestDispatcher creates a ProtocolDispatcher wired with real version
  10. // handlers that share the given mock PacketSender. This lets tests verify
  11. // that the dispatcher routes to the correct handler by asserting on the
  12. // mock sender's SendToSession calls.
  13. func newTestDispatcher(t *testing.T, sender PacketSender) *ProtocolDispatcher {
  14. t.Helper()
  15. logger := slog.Default()
  16. svc := newMockLegacyService(t)
  17. // Create a minimal LegacySessionManager so V5 handler can call GetSession
  18. // without panicking. The map is empty so GetSession returns nil, which is
  19. // handled gracefully by the handlers.
  20. sessions := &LegacySessionManager{
  21. sessions: make(map[uint32]*LegacySession),
  22. addrIndex: make(map[string]*LegacySession),
  23. }
  24. v1 := NewV1Handler(sessions, svc, sender, logger)
  25. v2 := NewV2Handler(sessions, svc, sender, NewV2PacketBuilder(), logger)
  26. v3 := NewV3Handler(sessions, svc, sender, NewV3PacketBuilder(sessions, nil), logger)
  27. v4 := NewV4Handler(sessions, svc, sender, NewV4PacketBuilder(sessions, nil), logger)
  28. v5 := NewV5Handler(sessions, svc, sender, NewV5PacketBuilder(sessions, nil), logger)
  29. cfg := config.ICQLegacyConfig{
  30. SupportedVersions: []int{3, 4, 5},
  31. }
  32. return NewProtocolDispatcher(v1, v2, v3, v4, v5, cfg, logger)
  33. }
  34. // ---------------------------------------------------------------------------
  35. // Task 12.1 — Table-driven tests for ProtocolDispatcher
  36. // ---------------------------------------------------------------------------
  37. func TestProtocolDispatcher_SendUserOnline(t *testing.T) {
  38. tests := []struct {
  39. name string
  40. version uint16
  41. }{
  42. {"V3", ICQLegacyVersionV3},
  43. {"V4", ICQLegacyVersionV4},
  44. {"V5", ICQLegacyVersionV5},
  45. }
  46. for _, tt := range tests {
  47. t.Run(tt.name, func(t *testing.T) {
  48. sender := newMockPacketSender(t)
  49. dispatcher := newTestDispatcher(t, sender)
  50. session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
  51. // The handler will call SendToSession exactly once.
  52. sender.EXPECT().
  53. SendToSession(session, mock.AnythingOfType("[]uint8")).
  54. Return(nil)
  55. err := dispatcher.SendUserOnline(session, 67890, 0x00000000)
  56. assert.NoError(t, err)
  57. })
  58. }
  59. }
  60. func TestProtocolDispatcher_SendUserOnline_NilSession(t *testing.T) {
  61. sender := newMockPacketSender(t)
  62. dispatcher := newTestDispatcher(t, sender)
  63. // nil session should return nil without calling sender
  64. err := dispatcher.SendUserOnline(nil, 67890, 0x00000000)
  65. assert.NoError(t, err)
  66. }
  67. func TestProtocolDispatcher_SendUserOffline(t *testing.T) {
  68. tests := []struct {
  69. name string
  70. version uint16
  71. }{
  72. {"V3", ICQLegacyVersionV3},
  73. {"V4", ICQLegacyVersionV4},
  74. {"V5", ICQLegacyVersionV5},
  75. }
  76. for _, tt := range tests {
  77. t.Run(tt.name, func(t *testing.T) {
  78. sender := newMockPacketSender(t)
  79. dispatcher := newTestDispatcher(t, sender)
  80. session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
  81. sender.EXPECT().
  82. SendToSession(session, mock.AnythingOfType("[]uint8")).
  83. Return(nil)
  84. err := dispatcher.SendUserOffline(session, 67890)
  85. assert.NoError(t, err)
  86. })
  87. }
  88. }
  89. func TestProtocolDispatcher_SendStatusChange(t *testing.T) {
  90. tests := []struct {
  91. name string
  92. version uint16
  93. }{
  94. {"V3", ICQLegacyVersionV3},
  95. {"V4", ICQLegacyVersionV4},
  96. {"V5", ICQLegacyVersionV5},
  97. }
  98. for _, tt := range tests {
  99. t.Run(tt.name, func(t *testing.T) {
  100. sender := newMockPacketSender(t)
  101. dispatcher := newTestDispatcher(t, sender)
  102. session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
  103. sender.EXPECT().
  104. SendToSession(session, mock.AnythingOfType("[]uint8")).
  105. Return(nil)
  106. err := dispatcher.SendStatusChange(session, 67890, 0x00000001)
  107. assert.NoError(t, err)
  108. })
  109. }
  110. }
  111. func TestProtocolDispatcher_SendOnlineMessage(t *testing.T) {
  112. tests := []struct {
  113. name string
  114. version uint16
  115. }{
  116. {"V3", ICQLegacyVersionV3},
  117. {"V4", ICQLegacyVersionV4},
  118. {"V5", ICQLegacyVersionV5},
  119. }
  120. for _, tt := range tests {
  121. t.Run(tt.name, func(t *testing.T) {
  122. sender := newMockPacketSender(t)
  123. dispatcher := newTestDispatcher(t, sender)
  124. session := newTestLegacySession(12345, legacySessionOptVersion(tt.version))
  125. sender.EXPECT().
  126. SendToSession(session, mock.AnythingOfType("[]uint8")).
  127. Return(nil)
  128. err := dispatcher.SendOnlineMessage(session, 67890, 0x0001, "Hello, world!")
  129. assert.NoError(t, err)
  130. })
  131. }
  132. }
  133. // ---------------------------------------------------------------------------
  134. // Task 12.2 — Property test: protocol dispatcher version routing
  135. // **Property 6: Protocol dispatcher routes to version-matching handler**
  136. // For any version V in {1,2,3,4,5} and any dispatch operation, the dispatcher
  137. // invokes the V-specific handler (verified by mock sender being called).
  138. // **Validates: Requirements 6.1, 6.4, 6.7, 6.8**
  139. // ---------------------------------------------------------------------------
  140. func TestProperty_DispatcherRoutesToVersionHandler(t *testing.T) {
  141. versions := []uint16{
  142. ICQLegacyVersionV3,
  143. ICQLegacyVersionV4,
  144. ICQLegacyVersionV5,
  145. }
  146. type dispatchOp struct {
  147. name string
  148. call func(d *ProtocolDispatcher, s *LegacySession) error
  149. }
  150. ops := []dispatchOp{
  151. {
  152. name: "SendUserOnline",
  153. call: func(d *ProtocolDispatcher, s *LegacySession) error {
  154. return d.SendUserOnline(s, 99999, 0x00000000)
  155. },
  156. },
  157. {
  158. name: "SendUserOffline",
  159. call: func(d *ProtocolDispatcher, s *LegacySession) error {
  160. return d.SendUserOffline(s, 99999)
  161. },
  162. },
  163. {
  164. name: "SendStatusChange",
  165. call: func(d *ProtocolDispatcher, s *LegacySession) error {
  166. return d.SendStatusChange(s, 99999, 0x00000001)
  167. },
  168. },
  169. {
  170. name: "SendOnlineMessage",
  171. call: func(d *ProtocolDispatcher, s *LegacySession) error {
  172. return d.SendOnlineMessage(s, 99999, 0x0001, "test message")
  173. },
  174. },
  175. }
  176. for _, v := range versions {
  177. for _, op := range ops {
  178. t.Run(op.name+"/"+versionName(v), func(t *testing.T) {
  179. sender := newMockPacketSender(t)
  180. dispatcher := newTestDispatcher(t, sender)
  181. session := newTestLegacySession(12345, legacySessionOptVersion(v))
  182. // Expect exactly one SendToSession call — proves the
  183. // correct handler was invoked (each handler builds a
  184. // packet and sends it via the shared sender).
  185. sender.EXPECT().
  186. SendToSession(session, mock.AnythingOfType("[]uint8")).
  187. Return(nil)
  188. err := op.call(dispatcher, session)
  189. assert.NoError(t, err)
  190. })
  191. }
  192. }
  193. }
  194. // ---------------------------------------------------------------------------
  195. // Task 12.3 — Property test: cross-version message content preservation
  196. // **Property 7: Cross-version message content preservation**
  197. // For any message content string and type, and any sender/receiver version
  198. // pair, the message is dispatched without error.
  199. // **Validates: Requirements 6.5, 6.6**
  200. // ---------------------------------------------------------------------------
  201. func TestProperty_CrossVersionMessageContentPreservation(t *testing.T) {
  202. versions := []uint16{
  203. ICQLegacyVersionV3,
  204. ICQLegacyVersionV4,
  205. ICQLegacyVersionV5,
  206. }
  207. messages := []struct {
  208. msgType uint16
  209. content string
  210. }{
  211. {0x0001, "Hello"},
  212. {0x0001, ""},
  213. {0x0001, "A longer message with special chars: !@#$%^&*()"},
  214. {0x0004, "URL message"},
  215. {0x000E, "Email express"},
  216. {0x0001, "Unicode: café résumé naïve"},
  217. }
  218. for _, senderVersion := range versions {
  219. for _, receiverVersion := range versions {
  220. for _, msg := range messages {
  221. testName := versionName(senderVersion) + "->" + versionName(receiverVersion) +
  222. "/type_" + msgTypeName(msg.msgType)
  223. t.Run(testName, func(t *testing.T) {
  224. sender := newMockPacketSender(t)
  225. dispatcher := newTestDispatcher(t, sender)
  226. // The receiver session has the receiver's version.
  227. session := newTestLegacySession(12345, legacySessionOptVersion(receiverVersion))
  228. // Allow the send call — we just verify no error.
  229. sender.EXPECT().
  230. SendToSession(session, mock.AnythingOfType("[]uint8")).
  231. Return(nil)
  232. // Dispatch the message as if it came from a sender
  233. // on senderVersion to a receiver on receiverVersion.
  234. err := dispatcher.SendOnlineMessage(session, 67890, msg.msgType, msg.content)
  235. assert.NoError(t, err)
  236. })
  237. }
  238. }
  239. }
  240. }
  241. // versionName returns a human-readable name for a protocol version constant.
  242. func versionName(v uint16) string {
  243. switch v {
  244. case ICQLegacyVersionV1:
  245. return "V1"
  246. case ICQLegacyVersionV2:
  247. return "V2"
  248. case ICQLegacyVersionV3:
  249. return "V3"
  250. case ICQLegacyVersionV4:
  251. return "V4"
  252. case ICQLegacyVersionV5:
  253. return "V5"
  254. default:
  255. return "Unknown"
  256. }
  257. }
  258. // msgTypeName returns a short label for common ICQ message types.
  259. func msgTypeName(t uint16) string {
  260. switch t {
  261. case 0x0001:
  262. return "normal"
  263. case 0x0004:
  264. return "url"
  265. case 0x000E:
  266. return "email"
  267. default:
  268. return "other"
  269. }
  270. }