4
0

imserv_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. package handlers
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "log/slog"
  7. "net/http"
  8. "net/http/httptest"
  9. "net/url"
  10. "testing"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/mock"
  13. "github.com/stretchr/testify/require"
  14. "github.com/google/uuid"
  15. "github.com/mk6i/open-oscar-server/config"
  16. "github.com/mk6i/open-oscar-server/server/webapi/types"
  17. "github.com/mk6i/open-oscar-server/state"
  18. "github.com/mk6i/open-oscar-server/wire"
  19. )
  20. // MockChatService is a mock implementation of ChatService.
  21. type MockChatService struct {
  22. mock.Mock
  23. }
  24. func (m *MockChatService) ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*wire.SNACMessage, error) {
  25. args := m.Called(ctx, instance, inFrame, inBody)
  26. msg, _ := args.Get(0).(*wire.SNACMessage)
  27. return msg, args.Error(1)
  28. }
  29. // MockChatNavService is a mock implementation of ChatNavService.
  30. type MockChatNavService struct {
  31. mock.Mock
  32. }
  33. func (m *MockChatNavService) CreateRoom(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error) {
  34. args := m.Called(ctx, instance, inFrame, inBody)
  35. return args.Get(0).(wire.SNACMessage), args.Error(1)
  36. }
  37. func (m *MockChatNavService) RequestRoomInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (wire.SNACMessage, error) {
  38. args := m.Called(ctx, inFrame, inBody)
  39. return args.Get(0).(wire.SNACMessage), args.Error(1)
  40. }
  41. // MockChatBridgeOServiceService is a mock implementation of ChatBridgeOServiceService.
  42. type MockChatBridgeOServiceService struct {
  43. mock.Mock
  44. }
  45. func (m *MockChatBridgeOServiceService) ServiceRequest(ctx context.Context, service uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x01_0x04_OServiceServiceRequest, listener config.Listener) (wire.SNACMessage, error) {
  46. args := m.Called(ctx, service, instance, inFrame, inBody, listener)
  47. return args.Get(0).(wire.SNACMessage), args.Error(1)
  48. }
  49. func (m *MockChatBridgeOServiceService) ClientOnline(ctx context.Context, service uint16, inBody wire.SNAC_0x01_0x02_OServiceClientOnline, instance *state.SessionInstance) error {
  50. args := m.Called(ctx, service, inBody, instance)
  51. return args.Error(0)
  52. }
  53. // MockChatAuthService is a mock implementation of ChatAuthService.
  54. type MockChatAuthService struct {
  55. mock.Mock
  56. }
  57. func (m *MockChatAuthService) CrackCookie(authCookie []byte) (state.ServerCookie, error) {
  58. args := m.Called(authCookie)
  59. return args.Get(0).(state.ServerCookie), args.Error(1)
  60. }
  61. func (m *MockChatAuthService) RegisterChatSession(ctx context.Context, authCookie state.ServerCookie, cfg func(sess *state.Session)) (*state.SessionInstance, error) {
  62. args := m.Called(ctx, authCookie, cfg)
  63. inst, _ := args.Get(0).(*state.SessionInstance)
  64. return inst, args.Error(1)
  65. }
  66. func (m *MockChatAuthService) SignoutChat(ctx context.Context, sess *state.Session) {
  67. m.Called(ctx, sess)
  68. }
  69. // navInfoReply builds a ChatNav NavInfo reply carrying a room's cookie and name,
  70. // matching what CreateRoom/RequestRoomInfo return.
  71. func navInfoReply(cookie, name string) wire.SNACMessage {
  72. return wire.SNACMessage{
  73. Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  74. TLVRestBlock: wire.TLVRestBlock{
  75. TLVList: wire.TLVList{
  76. wire.NewTLVBE(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  77. Cookie: cookie,
  78. TLVBlock: wire.TLVBlock{
  79. TLVList: wire.TLVList{
  80. wire.NewTLVBE(wire.ChatRoomTLVRoomName, name),
  81. },
  82. },
  83. }),
  84. },
  85. },
  86. },
  87. }
  88. }
  89. // A room is addressed like a buddy: im/sendIM to a joined room id must relay
  90. // through ChatService (not ICBM) and echo the sender's own line as a room `im`
  91. // event keyed by the room id.
  92. func TestMessagingHandler_SendIM_RoutesRoomToChatService(t *testing.T) {
  93. const roomID = "room-cookie-123"
  94. oscarInstance := state.NewSession().AddInstance()
  95. mgr := state.NewWebAPISessionManager()
  96. session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
  97. []string{"im", "sentIM"}, oscarInstance, "", slog.Default())
  98. require.NoError(t, err)
  99. // The session has joined the room; its chat instance is what ChatService is
  100. // invoked against.
  101. chatInstance := state.NewSession().AddInstance()
  102. require.True(t, session.AddChatRoom(roomID, chatInstance))
  103. chatService := &MockChatService{}
  104. chatService.On("ChannelMsgToHost", mock.Anything, chatInstance, mock.Anything, mock.Anything).
  105. Return(nil, nil)
  106. icbmService := &MockICBMService{} // must NOT be called for a room target
  107. handler := &MessagingHandler{
  108. SessionManager: mgr,
  109. ICBMService: icbmService,
  110. ChatService: chatService,
  111. LocateService: stubLocateService("Ann Dupree"),
  112. FeedbagService: &MockFeedbagService{},
  113. Logger: slog.Default(),
  114. }
  115. req, err := http.NewRequest("GET",
  116. "/im/sendIM?aimsid="+session.AimSID+"&t="+url.QueryEscape(roomID)+"&message=hi", nil)
  117. require.NoError(t, err)
  118. rr := httptest.NewRecorder()
  119. requireSession(mgr, handler.SendIM).ServeHTTP(rr, req)
  120. require.Equal(t, http.StatusOK, rr.Code)
  121. chatService.AssertCalled(t, "ChannelMsgToHost", mock.Anything, chatInstance, mock.Anything, mock.Anything)
  122. icbmService.AssertNotCalled(t, "ChannelMsgToHost", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
  123. // The sender's own line is echoed as a room `im` event.
  124. var room types.RoomIMEvent
  125. var found bool
  126. for _, e := range session.EventQueue.GetAllEvents() {
  127. if e.Type == types.EventTypeIM {
  128. room, found = e.Data.(types.RoomIMEvent)
  129. }
  130. }
  131. require.True(t, found, "expected a room im event")
  132. assert.Equal(t, roomID, room.Imserv)
  133. assert.Equal(t, roomID, room.Source.AimID)
  134. assert.Equal(t, "imservMsg", room.SpecialIM)
  135. assert.Equal(t, "anndupree", room.SpecialData.ImFromImserv.OrigSender)
  136. assert.Equal(t, "hi", room.SpecialData.ImFromImserv.Text)
  137. }
  138. // stubChatJoinDeps wires the ServiceRequest/CrackCookie/RegisterChatSession/
  139. // ClientOnline handoff mocks for the room-join path, returning the chat instance
  140. // RegisterChatSession hands back so tests can assert it was registered.
  141. func stubChatJoinDeps(oscarInstance *state.SessionInstance) (*MockChatBridgeOServiceService, *MockChatAuthService, *state.SessionInstance) {
  142. oservice := &MockChatBridgeOServiceService{}
  143. oservice.On("ServiceRequest", mock.Anything, wire.BOS, oscarInstance, mock.Anything, mock.Anything, mock.Anything).
  144. Return(wire.SNACMessage{
  145. Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
  146. TLVRestBlock: wire.TLVRestBlock{
  147. TLVList: wire.TLVList{
  148. wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("login-cookie")),
  149. },
  150. },
  151. },
  152. }, nil)
  153. chatInstance := state.NewSession().AddInstance()
  154. auth := &MockChatAuthService{}
  155. auth.On("CrackCookie", []byte("login-cookie")).Return(state.ServerCookie{}, nil)
  156. auth.On("RegisterChatSession", mock.Anything, mock.Anything, mock.Anything).Return(chatInstance, nil)
  157. oservice.On("ClientOnline", mock.Anything, wire.Chat, mock.Anything, chatInstance).Return(nil)
  158. return oservice, auth, chatInstance
  159. }
  160. // imserv/create makes an exchange-4 room, auto-joins the creator (so im/sendIM to
  161. // the room works immediately), and returns the room id as imserv.
  162. func TestImservHandler_Create(t *testing.T) {
  163. const roomID = "created-cookie"
  164. oscarInstance := state.NewSession().AddInstance()
  165. mgr := state.NewWebAPISessionManager()
  166. session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
  167. []string{"im"}, oscarInstance, "", slog.Default())
  168. require.NoError(t, err)
  169. chatNav := &MockChatNavService{}
  170. chatNav.On("CreateRoom", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
  171. Return(navInfoReply(roomID, "My Room"), nil)
  172. oservice, auth, chatInstance := stubChatJoinDeps(oscarInstance)
  173. handler := &ImservHandler{
  174. SessionManager: mgr,
  175. ChatNavService: chatNav,
  176. OServiceService: oservice,
  177. AuthService: auth,
  178. Logger: slog.Default(),
  179. }
  180. req, err := http.NewRequest("GET",
  181. "/imserv/create?aimsid="+session.AimSID+"&friendly="+url.QueryEscape("My Room"), nil)
  182. require.NoError(t, err)
  183. rr := httptest.NewRecorder()
  184. requireSession(mgr, handler.Create).ServeHTTP(rr, req)
  185. require.Equal(t, http.StatusOK, rr.Code)
  186. chatNav.AssertCalled(t, "CreateRoom", mock.Anything, oscarInstance, mock.Anything, mock.Anything)
  187. assert.Contains(t, rr.Body.String(), roomID)
  188. // The creator is auto-joined so subsequent im/sendIM routes to the room.
  189. got, joined := session.ChatRoom(roomID)
  190. require.True(t, joined)
  191. assert.Equal(t, chatInstance, got)
  192. }
  193. // imserv/join runs the OSCAR chat handoff and registers the room's chat session
  194. // on the WebAPISession so subsequent im/sendIM routes to it.
  195. func TestImservHandler_Join(t *testing.T) {
  196. const roomID = "join-cookie"
  197. oscarInstance := state.NewSession().AddInstance()
  198. mgr := state.NewWebAPISessionManager()
  199. session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
  200. []string{"im"}, oscarInstance, "", slog.Default())
  201. require.NoError(t, err)
  202. chatNav := &MockChatNavService{}
  203. chatNav.On("RequestRoomInfo", mock.Anything, mock.Anything, mock.Anything).
  204. Return(navInfoReply(roomID, "My Room"), nil)
  205. oservice := &MockChatBridgeOServiceService{}
  206. oservice.On("ServiceRequest", mock.Anything, wire.BOS, oscarInstance, mock.Anything, mock.Anything, mock.Anything).
  207. Return(wire.SNACMessage{
  208. Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
  209. TLVRestBlock: wire.TLVRestBlock{
  210. TLVList: wire.TLVList{
  211. wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("login-cookie")),
  212. },
  213. },
  214. },
  215. }, nil)
  216. chatInstance := state.NewSession().AddInstance()
  217. auth := &MockChatAuthService{}
  218. auth.On("CrackCookie", []byte("login-cookie")).Return(state.ServerCookie{}, nil)
  219. auth.On("RegisterChatSession", mock.Anything, mock.Anything, mock.Anything).Return(chatInstance, nil)
  220. oservice.On("ClientOnline", mock.Anything, wire.Chat, mock.Anything, chatInstance).Return(nil)
  221. handler := &ImservHandler{
  222. SessionManager: mgr,
  223. ChatNavService: chatNav,
  224. OServiceService: oservice,
  225. AuthService: auth,
  226. Logger: slog.Default(),
  227. }
  228. req, err := http.NewRequest("GET",
  229. "/imserv/join?aimsid="+session.AimSID+"&imserv="+url.QueryEscape(roomID), nil)
  230. require.NoError(t, err)
  231. rr := httptest.NewRecorder()
  232. requireSession(mgr, handler.Join).ServeHTTP(rr, req)
  233. require.Equal(t, http.StatusOK, rr.Code)
  234. // The room's chat session is registered so im/sendIM can find it.
  235. got, joined := session.ChatRoom(roomID)
  236. require.True(t, joined)
  237. assert.Equal(t, chatInstance, got)
  238. oservice.AssertCalled(t, "ClientOnline", mock.Anything, wire.Chat, mock.Anything, chatInstance)
  239. }
  240. // imserv/invite sends a chat-room invitation to the buddy named by t as an ICBM
  241. // channel-2 CapChat rendezvous carrying the room info.
  242. func TestImservHandler_Invite(t *testing.T) {
  243. const roomID = "4-0-Test Room"
  244. oscarInstance := state.NewSession().AddInstance()
  245. mgr := state.NewWebAPISessionManager()
  246. session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
  247. []string{"im"}, oscarInstance, "", slog.Default())
  248. require.NoError(t, err)
  249. var captured wire.SNAC_0x04_0x06_ICBMChannelMsgToHost
  250. icbm := &MockICBMService{}
  251. icbm.On("ChannelMsgToHost", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
  252. Run(func(args mock.Arguments) {
  253. captured = args.Get(3).(wire.SNAC_0x04_0x06_ICBMChannelMsgToHost)
  254. }).Return(nil, nil)
  255. handler := &ImservHandler{SessionManager: mgr, ICBMService: icbm, Logger: slog.Default()}
  256. req, err := http.NewRequest("GET",
  257. "/imserv/invite?aimsid="+session.AimSID+"&imserv="+url.QueryEscape(roomID)+"&t=bobsmith", nil)
  258. require.NoError(t, err)
  259. rr := httptest.NewRecorder()
  260. requireSession(mgr, handler.Invite).ServeHTTP(rr, req)
  261. require.Equal(t, http.StatusOK, rr.Code)
  262. assert.Equal(t, wire.ICBMChannelRendezvous, captured.ChannelID)
  263. assert.Equal(t, "bobsmith", captured.ScreenName)
  264. // The rendezvous carries a CapChat propose fragment with the room info.
  265. rd, ok := captured.Bytes(wire.ICBMTLVData)
  266. require.True(t, ok)
  267. var frag wire.ICBMCh2Fragment
  268. require.NoError(t, wire.UnmarshalBE(&frag, bytes.NewReader(rd)))
  269. assert.Equal(t, wire.CapChat, uuid.UUID(frag.Capability))
  270. svc, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData)
  271. require.True(t, ok)
  272. var room wire.ICBMRoomInfo
  273. require.NoError(t, wire.UnmarshalBE(&room, bytes.NewReader(svc)))
  274. assert.Equal(t, roomID, room.Cookie)
  275. assert.Equal(t, uint16(4), room.Exchange)
  276. }
  277. // MockChatParticipants is a mock implementation of ChatParticipants.
  278. type MockChatParticipants struct {
  279. mock.Mock
  280. }
  281. func (m *MockChatParticipants) AllSessions(chatCookie string) []*state.Session {
  282. args := m.Called(chatCookie)
  283. s, _ := args.Get(0).([]*state.Session)
  284. return s
  285. }
  286. // imserv/getSettings must return 200 with the room name (friendly) and member
  287. // count — the client's group-chat entry stays in a permanent loading spinner
  288. // until this succeeds.
  289. func TestImservHandler_GetSettings(t *testing.T) {
  290. const roomID = "4-0-Ida and Jon chat"
  291. oscarInstance := state.NewSession().AddInstance()
  292. mgr := state.NewWebAPISessionManager()
  293. session, err := mgr.CreateSession(state.DisplayScreenName("Ida Pruitt"), "test-dev",
  294. []string{"im", "conversation"}, oscarInstance, "", slog.Default())
  295. require.NoError(t, err)
  296. participants := &MockChatParticipants{}
  297. participants.On("AllSessions", roomID).Return([]*state.Session{state.NewSession()})
  298. handler := &ImservHandler{SessionManager: mgr, Participants: participants, Logger: slog.Default()}
  299. req, err := http.NewRequest("GET",
  300. "/imserv/getSettings?aimsid="+session.AimSID+"&imserv="+url.QueryEscape(roomID), nil)
  301. require.NoError(t, err)
  302. rr := httptest.NewRecorder()
  303. requireSession(mgr, handler.GetSettings).ServeHTTP(rr, req)
  304. require.Equal(t, http.StatusOK, rr.Code)
  305. var body struct {
  306. Response struct {
  307. StatusCode int `json:"statusCode"`
  308. Data struct {
  309. Imserv string `json:"imserv"`
  310. Friendly string `json:"friendly"`
  311. MemberCounts int `json:"memberCounts"`
  312. } `json:"data"`
  313. } `json:"response"`
  314. }
  315. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
  316. assert.Equal(t, 200, body.Response.StatusCode)
  317. assert.Equal(t, roomID, body.Response.Data.Imserv)
  318. assert.Equal(t, "Ida and Jon chat", body.Response.Data.Friendly)
  319. assert.Equal(t, 1, body.Response.Data.MemberCounts)
  320. }