| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- package handlers
- import (
- "bytes"
- "context"
- "encoding/json"
- "log/slog"
- "net/http"
- "net/http/httptest"
- "net/url"
- "testing"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
- "github.com/google/uuid"
- "github.com/mk6i/open-oscar-server/config"
- "github.com/mk6i/open-oscar-server/server/webapi/types"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // MockChatService is a mock implementation of ChatService.
- type MockChatService struct {
- mock.Mock
- }
- func (m *MockChatService) ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*wire.SNACMessage, error) {
- args := m.Called(ctx, instance, inFrame, inBody)
- msg, _ := args.Get(0).(*wire.SNACMessage)
- return msg, args.Error(1)
- }
- // MockChatNavService is a mock implementation of ChatNavService.
- type MockChatNavService struct {
- mock.Mock
- }
- func (m *MockChatNavService) CreateRoom(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error) {
- args := m.Called(ctx, instance, inFrame, inBody)
- return args.Get(0).(wire.SNACMessage), args.Error(1)
- }
- func (m *MockChatNavService) RequestRoomInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (wire.SNACMessage, error) {
- args := m.Called(ctx, inFrame, inBody)
- return args.Get(0).(wire.SNACMessage), args.Error(1)
- }
- // MockChatBridgeOServiceService is a mock implementation of ChatBridgeOServiceService.
- type MockChatBridgeOServiceService struct {
- mock.Mock
- }
- 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) {
- args := m.Called(ctx, service, instance, inFrame, inBody, listener)
- return args.Get(0).(wire.SNACMessage), args.Error(1)
- }
- func (m *MockChatBridgeOServiceService) ClientOnline(ctx context.Context, service uint16, inBody wire.SNAC_0x01_0x02_OServiceClientOnline, instance *state.SessionInstance) error {
- args := m.Called(ctx, service, inBody, instance)
- return args.Error(0)
- }
- // MockChatAuthService is a mock implementation of ChatAuthService.
- type MockChatAuthService struct {
- mock.Mock
- }
- func (m *MockChatAuthService) CrackCookie(authCookie []byte) (state.ServerCookie, error) {
- args := m.Called(authCookie)
- return args.Get(0).(state.ServerCookie), args.Error(1)
- }
- func (m *MockChatAuthService) RegisterChatSession(ctx context.Context, authCookie state.ServerCookie, cfg func(sess *state.Session)) (*state.SessionInstance, error) {
- args := m.Called(ctx, authCookie, cfg)
- inst, _ := args.Get(0).(*state.SessionInstance)
- return inst, args.Error(1)
- }
- func (m *MockChatAuthService) SignoutChat(ctx context.Context, sess *state.Session) {
- m.Called(ctx, sess)
- }
- // navInfoReply builds a ChatNav NavInfo reply carrying a room's cookie and name,
- // matching what CreateRoom/RequestRoomInfo return.
- func navInfoReply(cookie, name string) wire.SNACMessage {
- return wire.SNACMessage{
- Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
- Cookie: cookie,
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.ChatRoomTLVRoomName, name),
- },
- },
- }),
- },
- },
- },
- }
- }
- // A room is addressed like a buddy: im/sendIM to a joined room id must relay
- // through ChatService (not ICBM) and echo the sender's own line as a room `im`
- // event keyed by the room id.
- func TestMessagingHandler_SendIM_RoutesRoomToChatService(t *testing.T) {
- const roomID = "room-cookie-123"
- oscarInstance := state.NewSession().AddInstance()
- mgr := state.NewWebAPISessionManager()
- session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
- []string{"im", "sentIM"}, oscarInstance, "", slog.Default())
- require.NoError(t, err)
- // The session has joined the room; its chat instance is what ChatService is
- // invoked against.
- chatInstance := state.NewSession().AddInstance()
- require.True(t, session.AddChatRoom(roomID, chatInstance))
- chatService := &MockChatService{}
- chatService.On("ChannelMsgToHost", mock.Anything, chatInstance, mock.Anything, mock.Anything).
- Return(nil, nil)
- icbmService := &MockICBMService{} // must NOT be called for a room target
- handler := &MessagingHandler{
- SessionManager: mgr,
- ICBMService: icbmService,
- ChatService: chatService,
- LocateService: stubLocateService("Ann Dupree"),
- FeedbagService: &MockFeedbagService{},
- Logger: slog.Default(),
- }
- req, err := http.NewRequest("GET",
- "/im/sendIM?aimsid="+session.AimSID+"&t="+url.QueryEscape(roomID)+"&message=hi", nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- requireSession(mgr, handler.SendIM).ServeHTTP(rr, req)
- require.Equal(t, http.StatusOK, rr.Code)
- chatService.AssertCalled(t, "ChannelMsgToHost", mock.Anything, chatInstance, mock.Anything, mock.Anything)
- icbmService.AssertNotCalled(t, "ChannelMsgToHost", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
- // The sender's own line is echoed as a room `im` event.
- var room types.RoomIMEvent
- var found bool
- for _, e := range session.EventQueue.GetAllEvents() {
- if e.Type == types.EventTypeIM {
- room, found = e.Data.(types.RoomIMEvent)
- }
- }
- require.True(t, found, "expected a room im event")
- assert.Equal(t, roomID, room.Imserv)
- assert.Equal(t, roomID, room.Source.AimID)
- assert.Equal(t, "imservMsg", room.SpecialIM)
- assert.Equal(t, "anndupree", room.SpecialData.ImFromImserv.OrigSender)
- assert.Equal(t, "hi", room.SpecialData.ImFromImserv.Text)
- }
- // stubChatJoinDeps wires the ServiceRequest/CrackCookie/RegisterChatSession/
- // ClientOnline handoff mocks for the room-join path, returning the chat instance
- // RegisterChatSession hands back so tests can assert it was registered.
- func stubChatJoinDeps(oscarInstance *state.SessionInstance) (*MockChatBridgeOServiceService, *MockChatAuthService, *state.SessionInstance) {
- oservice := &MockChatBridgeOServiceService{}
- oservice.On("ServiceRequest", mock.Anything, wire.BOS, oscarInstance, mock.Anything, mock.Anything, mock.Anything).
- Return(wire.SNACMessage{
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("login-cookie")),
- },
- },
- },
- }, nil)
- chatInstance := state.NewSession().AddInstance()
- auth := &MockChatAuthService{}
- auth.On("CrackCookie", []byte("login-cookie")).Return(state.ServerCookie{}, nil)
- auth.On("RegisterChatSession", mock.Anything, mock.Anything, mock.Anything).Return(chatInstance, nil)
- oservice.On("ClientOnline", mock.Anything, wire.Chat, mock.Anything, chatInstance).Return(nil)
- return oservice, auth, chatInstance
- }
- // imserv/create makes an exchange-4 room, auto-joins the creator (so im/sendIM to
- // the room works immediately), and returns the room id as imserv.
- func TestImservHandler_Create(t *testing.T) {
- const roomID = "created-cookie"
- oscarInstance := state.NewSession().AddInstance()
- mgr := state.NewWebAPISessionManager()
- session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
- []string{"im"}, oscarInstance, "", slog.Default())
- require.NoError(t, err)
- chatNav := &MockChatNavService{}
- chatNav.On("CreateRoom", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
- Return(navInfoReply(roomID, "My Room"), nil)
- oservice, auth, chatInstance := stubChatJoinDeps(oscarInstance)
- handler := &ImservHandler{
- SessionManager: mgr,
- ChatNavService: chatNav,
- OServiceService: oservice,
- AuthService: auth,
- Logger: slog.Default(),
- }
- req, err := http.NewRequest("GET",
- "/imserv/create?aimsid="+session.AimSID+"&friendly="+url.QueryEscape("My Room"), nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- requireSession(mgr, handler.Create).ServeHTTP(rr, req)
- require.Equal(t, http.StatusOK, rr.Code)
- chatNav.AssertCalled(t, "CreateRoom", mock.Anything, oscarInstance, mock.Anything, mock.Anything)
- assert.Contains(t, rr.Body.String(), roomID)
- // The creator is auto-joined so subsequent im/sendIM routes to the room.
- got, joined := session.ChatRoom(roomID)
- require.True(t, joined)
- assert.Equal(t, chatInstance, got)
- }
- // imserv/join runs the OSCAR chat handoff and registers the room's chat session
- // on the WebAPISession so subsequent im/sendIM routes to it.
- func TestImservHandler_Join(t *testing.T) {
- const roomID = "join-cookie"
- oscarInstance := state.NewSession().AddInstance()
- mgr := state.NewWebAPISessionManager()
- session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
- []string{"im"}, oscarInstance, "", slog.Default())
- require.NoError(t, err)
- chatNav := &MockChatNavService{}
- chatNav.On("RequestRoomInfo", mock.Anything, mock.Anything, mock.Anything).
- Return(navInfoReply(roomID, "My Room"), nil)
- oservice := &MockChatBridgeOServiceService{}
- oservice.On("ServiceRequest", mock.Anything, wire.BOS, oscarInstance, mock.Anything, mock.Anything, mock.Anything).
- Return(wire.SNACMessage{
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("login-cookie")),
- },
- },
- },
- }, nil)
- chatInstance := state.NewSession().AddInstance()
- auth := &MockChatAuthService{}
- auth.On("CrackCookie", []byte("login-cookie")).Return(state.ServerCookie{}, nil)
- auth.On("RegisterChatSession", mock.Anything, mock.Anything, mock.Anything).Return(chatInstance, nil)
- oservice.On("ClientOnline", mock.Anything, wire.Chat, mock.Anything, chatInstance).Return(nil)
- handler := &ImservHandler{
- SessionManager: mgr,
- ChatNavService: chatNav,
- OServiceService: oservice,
- AuthService: auth,
- Logger: slog.Default(),
- }
- req, err := http.NewRequest("GET",
- "/imserv/join?aimsid="+session.AimSID+"&imserv="+url.QueryEscape(roomID), nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- requireSession(mgr, handler.Join).ServeHTTP(rr, req)
- require.Equal(t, http.StatusOK, rr.Code)
- // The room's chat session is registered so im/sendIM can find it.
- got, joined := session.ChatRoom(roomID)
- require.True(t, joined)
- assert.Equal(t, chatInstance, got)
- oservice.AssertCalled(t, "ClientOnline", mock.Anything, wire.Chat, mock.Anything, chatInstance)
- }
- // imserv/invite sends a chat-room invitation to the buddy named by t as an ICBM
- // channel-2 CapChat rendezvous carrying the room info.
- func TestImservHandler_Invite(t *testing.T) {
- const roomID = "4-0-Test Room"
- oscarInstance := state.NewSession().AddInstance()
- mgr := state.NewWebAPISessionManager()
- session, err := mgr.CreateSession(state.DisplayScreenName("Ann Dupree"), "test-dev",
- []string{"im"}, oscarInstance, "", slog.Default())
- require.NoError(t, err)
- var captured wire.SNAC_0x04_0x06_ICBMChannelMsgToHost
- icbm := &MockICBMService{}
- icbm.On("ChannelMsgToHost", mock.Anything, oscarInstance, mock.Anything, mock.Anything).
- Run(func(args mock.Arguments) {
- captured = args.Get(3).(wire.SNAC_0x04_0x06_ICBMChannelMsgToHost)
- }).Return(nil, nil)
- handler := &ImservHandler{SessionManager: mgr, ICBMService: icbm, Logger: slog.Default()}
- req, err := http.NewRequest("GET",
- "/imserv/invite?aimsid="+session.AimSID+"&imserv="+url.QueryEscape(roomID)+"&t=bobsmith", nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- requireSession(mgr, handler.Invite).ServeHTTP(rr, req)
- require.Equal(t, http.StatusOK, rr.Code)
- assert.Equal(t, wire.ICBMChannelRendezvous, captured.ChannelID)
- assert.Equal(t, "bobsmith", captured.ScreenName)
- // The rendezvous carries a CapChat propose fragment with the room info.
- rd, ok := captured.Bytes(wire.ICBMTLVData)
- require.True(t, ok)
- var frag wire.ICBMCh2Fragment
- require.NoError(t, wire.UnmarshalBE(&frag, bytes.NewReader(rd)))
- assert.Equal(t, wire.CapChat, uuid.UUID(frag.Capability))
- svc, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData)
- require.True(t, ok)
- var room wire.ICBMRoomInfo
- require.NoError(t, wire.UnmarshalBE(&room, bytes.NewReader(svc)))
- assert.Equal(t, roomID, room.Cookie)
- assert.Equal(t, uint16(4), room.Exchange)
- }
- // MockChatParticipants is a mock implementation of ChatParticipants.
- type MockChatParticipants struct {
- mock.Mock
- }
- func (m *MockChatParticipants) AllSessions(chatCookie string) []*state.Session {
- args := m.Called(chatCookie)
- s, _ := args.Get(0).([]*state.Session)
- return s
- }
- // imserv/getSettings must return 200 with the room name (friendly) and member
- // count — the client's group-chat entry stays in a permanent loading spinner
- // until this succeeds.
- func TestImservHandler_GetSettings(t *testing.T) {
- const roomID = "4-0-Ida and Jon chat"
- oscarInstance := state.NewSession().AddInstance()
- mgr := state.NewWebAPISessionManager()
- session, err := mgr.CreateSession(state.DisplayScreenName("Ida Pruitt"), "test-dev",
- []string{"im", "conversation"}, oscarInstance, "", slog.Default())
- require.NoError(t, err)
- participants := &MockChatParticipants{}
- participants.On("AllSessions", roomID).Return([]*state.Session{state.NewSession()})
- handler := &ImservHandler{SessionManager: mgr, Participants: participants, Logger: slog.Default()}
- req, err := http.NewRequest("GET",
- "/imserv/getSettings?aimsid="+session.AimSID+"&imserv="+url.QueryEscape(roomID), nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- requireSession(mgr, handler.GetSettings).ServeHTTP(rr, req)
- require.Equal(t, http.StatusOK, rr.Code)
- var body struct {
- Response struct {
- StatusCode int `json:"statusCode"`
- Data struct {
- Imserv string `json:"imserv"`
- Friendly string `json:"friendly"`
- MemberCounts int `json:"memberCounts"`
- } `json:"data"`
- } `json:"response"`
- }
- require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &body))
- assert.Equal(t, 200, body.Response.StatusCode)
- assert.Equal(t, roomID, body.Response.Data.Imserv)
- assert.Equal(t, "Ida and Jon chat", body.Response.Data.Friendly)
- assert.Equal(t, 1, body.Response.Data.MemberCounts)
- }
|