messaging_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/mock"
  11. "github.com/mk6i/open-oscar-server/state"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. // MockMessageRelayer is a mock implementation of MessageRelayer
  15. type MockMessageRelayer struct {
  16. mock.Mock
  17. }
  18. func (m *MockMessageRelayer) RelayToScreenName(ctx context.Context, recipient state.IdentScreenName, msg wire.SNACMessage) {
  19. m.Called(ctx, recipient, msg)
  20. }
  21. // MockOfflineMessageManager is a mock implementation of OfflineMessageManager
  22. type MockOfflineMessageManager struct {
  23. mock.Mock
  24. }
  25. func (m *MockOfflineMessageManager) SaveMessage(ctx context.Context, msg state.OfflineMessage) (int, error) {
  26. args := m.Called(ctx, msg)
  27. return args.Int(0), args.Error(1)
  28. }
  29. // MockSessionRetriever is a mock implementation of SessionRetriever
  30. type MockSessionRetriever struct {
  31. mock.Mock
  32. }
  33. func (m *MockSessionRetriever) AllSessions() []*state.Session {
  34. args := m.Called()
  35. if sessions := args.Get(0); sessions != nil {
  36. return sessions.([]*state.Session)
  37. }
  38. return nil
  39. }
  40. func (m *MockSessionRetriever) RetrieveSession(screenName state.IdentScreenName) *state.Session {
  41. args := m.Called(screenName)
  42. if session := args.Get(0); session != nil {
  43. return session.(*state.Session)
  44. }
  45. return nil
  46. }
  47. // MockRelationshipFetcher is a mock implementation of RelationshipFetcher
  48. type MockRelationshipFetcher struct {
  49. mock.Mock
  50. }
  51. func (m *MockRelationshipFetcher) Relationship(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) (state.Relationship, error) {
  52. args := m.Called(ctx, me, them)
  53. return args.Get(0).(state.Relationship), args.Error(1)
  54. }
  55. // createTestSessionManager creates a WebAPISessionManager with a pre-populated session.
  56. func createTestSessionManager(screenName string) (*state.WebAPISessionManager, string) {
  57. mgr := state.NewWebAPISessionManager()
  58. session, _ := mgr.CreateSession(
  59. context.Background(),
  60. state.DisplayScreenName(screenName),
  61. "test-dev",
  62. []string{"im", "presence", "buddylist", "sentIM"},
  63. nil,
  64. slog.Default(),
  65. )
  66. return mgr, session.AimSID
  67. }
  68. func TestMessagingHandler_SendIM(t *testing.T) {
  69. tests := []struct {
  70. name string
  71. queryParams string
  72. setupMocks func(*MockMessageRelayer, *MockOfflineMessageManager, *MockSessionRetriever, *MockRelationshipFetcher)
  73. expectedStatusCode int
  74. checkResponse func(*testing.T, string)
  75. }{
  76. {
  77. name: "Success_OnlineRecipient",
  78. queryParams: "t=recipient&message=hello+world",
  79. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  80. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("recipient")).
  81. Return(state.Relationship{}, nil)
  82. sr.On("RetrieveSession", state.NewIdentScreenName("recipient")).
  83. Return(&state.Session{})
  84. mr.On("RelayToScreenName", mock.Anything, state.NewIdentScreenName("recipient"), mock.AnythingOfType("wire.SNACMessage")).
  85. Return()
  86. },
  87. expectedStatusCode: http.StatusOK,
  88. checkResponse: func(t *testing.T, body string) {
  89. assert.Contains(t, body, `"statusCode":200`)
  90. assert.Contains(t, body, `"msgId"`)
  91. assert.Contains(t, body, `"state":"delivered"`)
  92. },
  93. },
  94. {
  95. name: "Success_OfflineRecipient_OfflineIM",
  96. queryParams: "t=offlineuser&message=hello&offlineIM=1",
  97. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  98. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
  99. Return(state.Relationship{}, nil)
  100. sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
  101. Return(nil)
  102. om.On("SaveMessage", mock.Anything, mock.AnythingOfType("state.OfflineMessage")).
  103. Return(1, nil)
  104. },
  105. expectedStatusCode: http.StatusOK,
  106. checkResponse: func(t *testing.T, body string) {
  107. assert.Contains(t, body, `"statusCode":200`)
  108. assert.Contains(t, body, `"msgId"`)
  109. },
  110. },
  111. {
  112. name: "Error_MissingRecipient",
  113. queryParams: "message=hello",
  114. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  115. },
  116. expectedStatusCode: http.StatusBadRequest,
  117. checkResponse: func(t *testing.T, body string) {
  118. assert.Contains(t, body, "missing required parameter: t")
  119. },
  120. },
  121. {
  122. name: "Error_MissingMessage",
  123. queryParams: "t=recipient",
  124. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  125. },
  126. expectedStatusCode: http.StatusBadRequest,
  127. checkResponse: func(t *testing.T, body string) {
  128. assert.Contains(t, body, "missing required parameter: message")
  129. },
  130. },
  131. {
  132. name: "Error_BlockedBySender",
  133. queryParams: "t=blockeduser&message=hello",
  134. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  135. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("blockeduser")).
  136. Return(state.Relationship{YouBlock: true}, nil)
  137. },
  138. expectedStatusCode: http.StatusForbidden,
  139. checkResponse: func(t *testing.T, body string) {
  140. assert.Contains(t, body, "cannot send message to blocked user")
  141. },
  142. },
  143. {
  144. name: "Error_BlockedByRecipient",
  145. queryParams: "t=blocker&message=hello",
  146. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  147. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("blocker")).
  148. Return(state.Relationship{BlocksYou: true}, nil)
  149. },
  150. expectedStatusCode: http.StatusNotFound,
  151. checkResponse: func(t *testing.T, body string) {
  152. assert.Contains(t, body, "recipient is not online")
  153. },
  154. },
  155. {
  156. name: "Error_OfflineInboxFull",
  157. queryParams: "t=offlineuser&message=hello&offlineIM=1",
  158. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  159. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
  160. Return(state.Relationship{}, nil)
  161. sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
  162. Return(nil)
  163. om.On("SaveMessage", mock.Anything, mock.AnythingOfType("state.OfflineMessage")).
  164. Return(0, state.ErrOfflineInboxFull)
  165. },
  166. expectedStatusCode: http.StatusConflict,
  167. checkResponse: func(t *testing.T, body string) {
  168. assert.Contains(t, body, "recipient inbox full")
  169. },
  170. },
  171. {
  172. name: "Error_OfflineRecipient_NoOfflineIM",
  173. queryParams: "t=offlineuser&message=hello&offlineIM=0",
  174. setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  175. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
  176. Return(state.Relationship{}, nil)
  177. sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
  178. Return(nil)
  179. },
  180. expectedStatusCode: http.StatusNotFound,
  181. checkResponse: func(t *testing.T, body string) {
  182. assert.Contains(t, body, "recipient is not online")
  183. },
  184. },
  185. }
  186. for _, tt := range tests {
  187. t.Run(tt.name, func(t *testing.T) {
  188. messageRelayer := &MockMessageRelayer{}
  189. offlineMsgMgr := &MockOfflineMessageManager{}
  190. sessionRetriever := &MockSessionRetriever{}
  191. relFetcher := &MockRelationshipFetcher{}
  192. sessionMgr, aimsid := createTestSessionManager("testuser")
  193. handler := &MessagingHandler{
  194. SessionManager: sessionMgr,
  195. MessageRelayer: messageRelayer,
  196. OfflineMessageManager: offlineMsgMgr,
  197. SessionRetriever: sessionRetriever,
  198. RelationshipFetcher: relFetcher,
  199. Logger: slog.Default(),
  200. }
  201. tt.setupMocks(messageRelayer, offlineMsgMgr, sessionRetriever, relFetcher)
  202. reqURL := "/im/sendIM?aimsid=" + aimsid + "&" + tt.queryParams
  203. req, err := http.NewRequest("GET", reqURL, nil)
  204. assert.NoError(t, err)
  205. rr := httptest.NewRecorder()
  206. handler.SendIM(rr, req)
  207. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  208. responseBody := strings.TrimSpace(rr.Body.String())
  209. if tt.checkResponse != nil {
  210. tt.checkResponse(t, responseBody)
  211. }
  212. messageRelayer.AssertExpectations(t)
  213. offlineMsgMgr.AssertExpectations(t)
  214. sessionRetriever.AssertExpectations(t)
  215. relFetcher.AssertExpectations(t)
  216. })
  217. }
  218. }
  219. func TestMessagingHandler_SendIM_MissingAimsid(t *testing.T) {
  220. handler := &MessagingHandler{
  221. SessionManager: state.NewWebAPISessionManager(),
  222. Logger: slog.Default(),
  223. }
  224. req, err := http.NewRequest("GET", "/im/sendIM", nil)
  225. assert.NoError(t, err)
  226. rr := httptest.NewRecorder()
  227. handler.SendIM(rr, req)
  228. assert.Equal(t, http.StatusBadRequest, rr.Code)
  229. assert.Contains(t, rr.Body.String(), "missing required parameter: aimsid")
  230. }
  231. func TestMessagingHandler_SendIM_InvalidSession(t *testing.T) {
  232. handler := &MessagingHandler{
  233. SessionManager: state.NewWebAPISessionManager(),
  234. Logger: slog.Default(),
  235. }
  236. req, err := http.NewRequest("GET", "/im/sendIM?aimsid=nonexistent&t=someone&message=hi", nil)
  237. assert.NoError(t, err)
  238. rr := httptest.NewRecorder()
  239. handler.SendIM(rr, req)
  240. assert.Equal(t, http.StatusUnauthorized, rr.Code)
  241. assert.Contains(t, rr.Body.String(), "invalid or expired session")
  242. }
  243. func TestMessagingHandler_SetTyping(t *testing.T) {
  244. tests := []struct {
  245. name string
  246. queryParams string
  247. setupMocks func(*MockMessageRelayer, *MockSessionRetriever, *MockRelationshipFetcher)
  248. expectedStatusCode int
  249. checkResponse func(*testing.T, string)
  250. }{
  251. {
  252. name: "Success_TypingStarted",
  253. queryParams: "t=recipient&typing=true",
  254. setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  255. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("recipient")).
  256. Return(state.Relationship{}, nil)
  257. sr.On("RetrieveSession", state.NewIdentScreenName("recipient")).
  258. Return(&state.Session{})
  259. mr.On("RelayToScreenName", mock.Anything, state.NewIdentScreenName("recipient"), mock.AnythingOfType("wire.SNACMessage")).
  260. Return()
  261. },
  262. expectedStatusCode: http.StatusOK,
  263. checkResponse: func(t *testing.T, body string) {
  264. assert.Contains(t, body, `"statusCode":200`)
  265. },
  266. },
  267. {
  268. name: "Success_TypingStopped",
  269. queryParams: "t=recipient&typing=false",
  270. setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  271. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("recipient")).
  272. Return(state.Relationship{}, nil)
  273. sr.On("RetrieveSession", state.NewIdentScreenName("recipient")).
  274. Return(&state.Session{})
  275. mr.On("RelayToScreenName", mock.Anything, state.NewIdentScreenName("recipient"), mock.AnythingOfType("wire.SNACMessage")).
  276. Return()
  277. },
  278. expectedStatusCode: http.StatusOK,
  279. },
  280. {
  281. name: "Success_BlockedSilent",
  282. queryParams: "t=blockeduser&typing=true",
  283. setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  284. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("blockeduser")).
  285. Return(state.Relationship{YouBlock: true}, nil)
  286. // No relay should happen
  287. },
  288. expectedStatusCode: http.StatusOK,
  289. checkResponse: func(t *testing.T, body string) {
  290. assert.Contains(t, body, `"statusCode":200`)
  291. },
  292. },
  293. {
  294. name: "Success_OfflineRecipient",
  295. queryParams: "t=offlineuser&typing=true",
  296. setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
  297. rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
  298. Return(state.Relationship{}, nil)
  299. sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
  300. Return(nil)
  301. // No relay should happen for offline users
  302. },
  303. expectedStatusCode: http.StatusOK,
  304. checkResponse: func(t *testing.T, body string) {
  305. assert.Contains(t, body, `"statusCode":200`)
  306. },
  307. },
  308. {
  309. name: "Error_MissingRecipient",
  310. queryParams: "typing=true",
  311. setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {},
  312. expectedStatusCode: http.StatusBadRequest,
  313. checkResponse: func(t *testing.T, body string) {
  314. assert.Contains(t, body, "missing required parameter: t")
  315. },
  316. },
  317. }
  318. for _, tt := range tests {
  319. t.Run(tt.name, func(t *testing.T) {
  320. messageRelayer := &MockMessageRelayer{}
  321. sessionRetriever := &MockSessionRetriever{}
  322. relFetcher := &MockRelationshipFetcher{}
  323. sessionMgr, aimsid := createTestSessionManager("testuser")
  324. handler := &MessagingHandler{
  325. SessionManager: sessionMgr,
  326. MessageRelayer: messageRelayer,
  327. SessionRetriever: sessionRetriever,
  328. RelationshipFetcher: relFetcher,
  329. Logger: slog.Default(),
  330. }
  331. tt.setupMocks(messageRelayer, sessionRetriever, relFetcher)
  332. reqURL := "/im/setTyping?aimsid=" + aimsid + "&" + tt.queryParams
  333. req, err := http.NewRequest("GET", reqURL, nil)
  334. assert.NoError(t, err)
  335. rr := httptest.NewRecorder()
  336. handler.SetTyping(rr, req)
  337. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  338. if tt.checkResponse != nil {
  339. responseBody := strings.TrimSpace(rr.Body.String())
  340. tt.checkResponse(t, responseBody)
  341. }
  342. messageRelayer.AssertExpectations(t)
  343. sessionRetriever.AssertExpectations(t)
  344. relFetcher.AssertExpectations(t)
  345. })
  346. }
  347. }
  348. func TestMessagingHandler_SetTyping_MissingAimsid(t *testing.T) {
  349. handler := &MessagingHandler{
  350. SessionManager: state.NewWebAPISessionManager(),
  351. Logger: slog.Default(),
  352. }
  353. req, err := http.NewRequest("GET", "/im/setTyping", nil)
  354. assert.NoError(t, err)
  355. rr := httptest.NewRecorder()
  356. handler.SetTyping(rr, req)
  357. assert.Equal(t, http.StatusBadRequest, rr.Code)
  358. assert.Contains(t, rr.Body.String(), "missing required parameter: aimsid")
  359. }