messaging_test.go 15 KB

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