messaging_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "strings"
  9. "testing"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/mock"
  12. "github.com/mk6i/open-oscar-server/state"
  13. "github.com/mk6i/open-oscar-server/wire"
  14. )
  15. // MockICBMService is a mock implementation of ICBMService
  16. type MockICBMService struct {
  17. mock.Mock
  18. }
  19. func (m *MockICBMService) ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error) {
  20. args := m.Called(ctx, instance, inFrame, inBody)
  21. if msg := args.Get(0); msg != nil {
  22. return msg.(*wire.SNACMessage), args.Error(1)
  23. }
  24. return nil, args.Error(1)
  25. }
  26. func (m *MockICBMService) ClientEvent(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x14_ICBMClientEvent) error {
  27. args := m.Called(ctx, instance, inFrame, inBody)
  28. return args.Error(0)
  29. }
  30. // createTestSessionManager creates a WebAPISessionManager with a pre-populated session.
  31. func createTestSessionManager(screenName string) (*state.WebAPISessionManager, string) {
  32. return createTestSessionManagerWithOSCAR(screenName, nil)
  33. }
  34. // createTestSessionManagerWithOSCAR creates a WebAPISessionManager with an OSCAR session instance set.
  35. func createTestSessionManagerWithOSCAR(screenName string, oscarSession *state.SessionInstance) (*state.WebAPISessionManager, string) {
  36. mgr := state.NewWebAPISessionManager()
  37. session, _ := mgr.CreateSession(
  38. context.Background(),
  39. state.DisplayScreenName(screenName),
  40. "test-dev",
  41. []string{"im", "presence", "buddylist", "sentIM", "typing"},
  42. oscarSession,
  43. slog.Default(),
  44. )
  45. return mgr, session.AimSID
  46. }
  47. func TestMessagingHandler_SendIM(t *testing.T) {
  48. oscarInstance := state.NewSession().AddInstance()
  49. tests := []struct {
  50. name string
  51. queryParams string
  52. setupMocks func(*MockICBMService)
  53. expectedStatusCode int
  54. checkResponse func(*testing.T, string)
  55. }{
  56. {
  57. name: "Success",
  58. queryParams: "t=recipient&message=hello+world",
  59. setupMocks: func(is *MockICBMService) {
  60. is.On("ChannelMsgToHost", mock.Anything, oscarInstance, mock.AnythingOfType("wire.SNACFrame"), mock.AnythingOfType("wire.SNAC_0x04_0x06_ICBMChannelMsgToHost")).
  61. Return(nil, nil)
  62. },
  63. expectedStatusCode: http.StatusOK,
  64. checkResponse: func(t *testing.T, body string) {
  65. assert.Contains(t, body, `"statusCode":200`)
  66. assert.Contains(t, body, `"msgId"`)
  67. assert.Contains(t, body, `"state":"delivered"`)
  68. },
  69. },
  70. {
  71. name: "Error_MissingRecipient",
  72. queryParams: "message=hello",
  73. setupMocks: func(is *MockICBMService) {},
  74. expectedStatusCode: http.StatusBadRequest,
  75. checkResponse: func(t *testing.T, body string) {
  76. assert.Contains(t, body, "missing required parameter: t")
  77. },
  78. },
  79. {
  80. name: "Error_MissingMessage",
  81. queryParams: "t=recipient",
  82. setupMocks: func(is *MockICBMService) {},
  83. expectedStatusCode: http.StatusBadRequest,
  84. checkResponse: func(t *testing.T, body string) {
  85. assert.Contains(t, body, "missing required parameter: message")
  86. },
  87. },
  88. }
  89. for _, tt := range tests {
  90. t.Run(tt.name, func(t *testing.T) {
  91. icbmService := &MockICBMService{}
  92. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  93. handler := &MessagingHandler{
  94. SessionManager: sessionMgr,
  95. ICBMService: icbmService,
  96. Logger: slog.Default(),
  97. }
  98. tt.setupMocks(icbmService)
  99. reqURL := "/im/sendIM?aimsid=" + aimsid + "&" + tt.queryParams
  100. req, err := http.NewRequest("GET", reqURL, nil)
  101. assert.NoError(t, err)
  102. rr := httptest.NewRecorder()
  103. handler.SendIM(rr, req)
  104. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  105. responseBody := strings.TrimSpace(rr.Body.String())
  106. if tt.checkResponse != nil {
  107. tt.checkResponse(t, responseBody)
  108. }
  109. icbmService.AssertExpectations(t)
  110. })
  111. }
  112. }
  113. func TestMessagingHandler_SendIM_POST(t *testing.T) {
  114. oscarInstance := state.NewSession().AddInstance()
  115. icbmService := &MockICBMService{}
  116. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  117. icbmService.On("ChannelMsgToHost", mock.Anything, oscarInstance, mock.AnythingOfType("wire.SNACFrame"), mock.AnythingOfType("wire.SNAC_0x04_0x06_ICBMChannelMsgToHost")).
  118. Return(nil, nil)
  119. handler := &MessagingHandler{
  120. SessionManager: sessionMgr,
  121. ICBMService: icbmService,
  122. Logger: slog.Default(),
  123. }
  124. body := strings.NewReader("message=" + url.QueryEscape("hello from post"))
  125. req, err := http.NewRequest(http.MethodPost, "/im/sendIM?aimsid="+aimsid+"&f=json&t=recipient&r=1", body)
  126. assert.NoError(t, err)
  127. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  128. rr := httptest.NewRecorder()
  129. handler.SendIM(rr, req)
  130. assert.Equal(t, http.StatusOK, rr.Code)
  131. assert.Contains(t, rr.Body.String(), `"msgId"`)
  132. icbmService.AssertExpectations(t)
  133. }
  134. func TestMessagingHandler_SendIM_MissingAimsid(t *testing.T) {
  135. handler := &MessagingHandler{
  136. SessionManager: state.NewWebAPISessionManager(),
  137. Logger: slog.Default(),
  138. }
  139. req, err := http.NewRequest("GET", "/im/sendIM", nil)
  140. assert.NoError(t, err)
  141. rr := httptest.NewRecorder()
  142. handler.SendIM(rr, req)
  143. assert.Equal(t, http.StatusBadRequest, rr.Code)
  144. assert.Contains(t, rr.Body.String(), "missing required parameter: aimsid")
  145. }
  146. func TestMessagingHandler_SendIM_InvalidSession(t *testing.T) {
  147. handler := &MessagingHandler{
  148. SessionManager: state.NewWebAPISessionManager(),
  149. Logger: slog.Default(),
  150. }
  151. req, err := http.NewRequest("GET", "/im/sendIM?aimsid=nonexistent&t=someone&message=hi", nil)
  152. assert.NoError(t, err)
  153. rr := httptest.NewRecorder()
  154. handler.SendIM(rr, req)
  155. assert.Equal(t, http.StatusUnauthorized, rr.Code)
  156. assert.Contains(t, rr.Body.String(), "invalid or expired session")
  157. }
  158. func TestMessagingHandler_SetTyping(t *testing.T) {
  159. oscarInstance := state.NewSession().AddInstance()
  160. tests := []struct {
  161. name string
  162. queryParams string
  163. setupMocks func(*MockICBMService)
  164. expectedStatusCode int
  165. checkResponse func(*testing.T, string)
  166. }{
  167. {
  168. name: "Success_TypingStarted",
  169. queryParams: "t=recipient&typingStatus=typing",
  170. setupMocks: func(is *MockICBMService) {
  171. is.On("ClientEvent", mock.Anything, oscarInstance, wire.SNACFrame{}, wire.SNAC_0x04_0x14_ICBMClientEvent{
  172. ChannelID: wire.ICBMChannelIM,
  173. ScreenName: "recipient",
  174. Event: 0x0002,
  175. }).Return(nil)
  176. },
  177. expectedStatusCode: http.StatusOK,
  178. checkResponse: func(t *testing.T, body string) {
  179. assert.Contains(t, body, `"statusCode":200`)
  180. },
  181. },
  182. {
  183. name: "Success_TypingPaused",
  184. queryParams: "t=recipient&typingStatus=typed",
  185. setupMocks: func(is *MockICBMService) {
  186. is.On("ClientEvent", mock.Anything, oscarInstance, wire.SNACFrame{}, wire.SNAC_0x04_0x14_ICBMClientEvent{
  187. ChannelID: wire.ICBMChannelIM,
  188. ScreenName: "recipient",
  189. Event: 0x0001,
  190. }).Return(nil)
  191. },
  192. expectedStatusCode: http.StatusOK,
  193. },
  194. {
  195. name: "Success_TypingStopped",
  196. queryParams: "t=recipient&typingStatus=none",
  197. setupMocks: func(is *MockICBMService) {
  198. is.On("ClientEvent", mock.Anything, oscarInstance, wire.SNACFrame{}, wire.SNAC_0x04_0x14_ICBMClientEvent{
  199. ChannelID: wire.ICBMChannelIM,
  200. ScreenName: "recipient",
  201. Event: 0x0000,
  202. }).Return(nil)
  203. },
  204. expectedStatusCode: http.StatusOK,
  205. },
  206. {
  207. name: "Error_MissingRecipient",
  208. queryParams: "typingStatus=typing",
  209. setupMocks: func(is *MockICBMService) {},
  210. expectedStatusCode: http.StatusBadRequest,
  211. checkResponse: func(t *testing.T, body string) {
  212. assert.Contains(t, body, "missing required parameter: t")
  213. },
  214. },
  215. }
  216. for _, tt := range tests {
  217. t.Run(tt.name, func(t *testing.T) {
  218. icbmService := &MockICBMService{}
  219. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  220. handler := &MessagingHandler{
  221. SessionManager: sessionMgr,
  222. ICBMService: icbmService,
  223. Logger: slog.Default(),
  224. }
  225. tt.setupMocks(icbmService)
  226. reqURL := "/im/setTyping?aimsid=" + aimsid + "&" + tt.queryParams
  227. req, err := http.NewRequest("GET", reqURL, nil)
  228. assert.NoError(t, err)
  229. rr := httptest.NewRecorder()
  230. handler.SetTyping(rr, req)
  231. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  232. if tt.checkResponse != nil {
  233. responseBody := strings.TrimSpace(rr.Body.String())
  234. tt.checkResponse(t, responseBody)
  235. }
  236. icbmService.AssertExpectations(t)
  237. })
  238. }
  239. }
  240. func TestMessagingHandler_SetTyping_MissingAimsid(t *testing.T) {
  241. handler := &MessagingHandler{
  242. SessionManager: state.NewWebAPISessionManager(),
  243. Logger: slog.Default(),
  244. }
  245. req, err := http.NewRequest("GET", "/im/setTyping", nil)
  246. assert.NoError(t, err)
  247. rr := httptest.NewRecorder()
  248. handler.SetTyping(rr, req)
  249. assert.Equal(t, http.StatusBadRequest, rr.Code)
  250. assert.Contains(t, rr.Body.String(), "missing required parameter: aimsid")
  251. }