| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- package handlers
- import (
- "context"
- "log/slog"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // MockMessageRelayer is a mock implementation of MessageRelayer
- type MockMessageRelayer struct {
- mock.Mock
- }
- func (m *MockMessageRelayer) RelayToScreenName(ctx context.Context, recipient state.IdentScreenName, msg wire.SNACMessage) {
- m.Called(ctx, recipient, msg)
- }
- // MockOfflineMessageManager is a mock implementation of OfflineMessageManager
- type MockOfflineMessageManager struct {
- mock.Mock
- }
- func (m *MockOfflineMessageManager) SaveMessage(ctx context.Context, msg state.OfflineMessage) (int, error) {
- args := m.Called(ctx, msg)
- return args.Int(0), args.Error(1)
- }
- // MockSessionRetriever is a mock implementation of SessionRetriever
- type MockSessionRetriever struct {
- mock.Mock
- }
- func (m *MockSessionRetriever) AllSessions() []*state.Session {
- args := m.Called()
- if sessions := args.Get(0); sessions != nil {
- return sessions.([]*state.Session)
- }
- return nil
- }
- func (m *MockSessionRetriever) RetrieveSession(screenName state.IdentScreenName) *state.Session {
- args := m.Called(screenName)
- if session := args.Get(0); session != nil {
- return session.(*state.Session)
- }
- return nil
- }
- // MockRelationshipFetcher is a mock implementation of RelationshipFetcher
- type MockRelationshipFetcher struct {
- mock.Mock
- }
- func (m *MockRelationshipFetcher) Relationship(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) (state.Relationship, error) {
- args := m.Called(ctx, me, them)
- return args.Get(0).(state.Relationship), args.Error(1)
- }
- // createTestSessionManager creates a WebAPISessionManager with a pre-populated session.
- func createTestSessionManager(screenName string) (*state.WebAPISessionManager, string) {
- mgr := state.NewWebAPISessionManager()
- session, _ := mgr.CreateSession(
- context.Background(),
- state.DisplayScreenName(screenName),
- "test-dev",
- []string{"im", "presence", "buddylist", "sentIM"},
- nil,
- slog.Default(),
- )
- return mgr, session.AimSID
- }
- func TestMessagingHandler_SendIM(t *testing.T) {
- tests := []struct {
- name string
- queryParams string
- setupMocks func(*MockMessageRelayer, *MockOfflineMessageManager, *MockSessionRetriever, *MockRelationshipFetcher)
- expectedStatusCode int
- checkResponse func(*testing.T, string)
- }{
- {
- name: "Success_OnlineRecipient",
- queryParams: "t=recipient&message=hello+world",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("recipient")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("recipient")).
- Return(&state.Session{})
- mr.On("RelayToScreenName", mock.Anything, state.NewIdentScreenName("recipient"), mock.AnythingOfType("wire.SNACMessage")).
- Return()
- },
- expectedStatusCode: http.StatusOK,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, `"statusCode":200`)
- assert.Contains(t, body, `"msgId"`)
- assert.Contains(t, body, `"state":"delivered"`)
- },
- },
- {
- name: "Success_OfflineRecipient_OfflineIM",
- queryParams: "t=offlineuser&message=hello&offlineIM=1",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
- Return(nil)
- om.On("SaveMessage", mock.Anything, mock.AnythingOfType("state.OfflineMessage")).
- Return(1, nil)
- },
- expectedStatusCode: http.StatusOK,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, `"statusCode":200`)
- assert.Contains(t, body, `"msgId"`)
- },
- },
- {
- name: "Error_MissingRecipient",
- queryParams: "message=hello",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- },
- expectedStatusCode: http.StatusBadRequest,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "missing required parameter: t")
- },
- },
- {
- name: "Error_MissingMessage",
- queryParams: "t=recipient",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- },
- expectedStatusCode: http.StatusBadRequest,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "missing required parameter: message")
- },
- },
- {
- name: "Error_BlockedBySender",
- queryParams: "t=blockeduser&message=hello",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("blockeduser")).
- Return(state.Relationship{YouBlock: true}, nil)
- },
- expectedStatusCode: http.StatusForbidden,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "cannot send message to blocked user")
- },
- },
- {
- name: "Error_BlockedByRecipient",
- queryParams: "t=blocker&message=hello",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("blocker")).
- Return(state.Relationship{BlocksYou: true}, nil)
- },
- expectedStatusCode: http.StatusNotFound,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "recipient is not online")
- },
- },
- {
- name: "Error_OfflineInboxFull",
- queryParams: "t=offlineuser&message=hello&offlineIM=1",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
- Return(nil)
- om.On("SaveMessage", mock.Anything, mock.AnythingOfType("state.OfflineMessage")).
- Return(0, state.ErrOfflineInboxFull)
- },
- expectedStatusCode: http.StatusConflict,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "recipient inbox full")
- },
- },
- {
- name: "Error_OfflineRecipient_NoOfflineIM",
- queryParams: "t=offlineuser&message=hello&offlineIM=0",
- setupMocks: func(mr *MockMessageRelayer, om *MockOfflineMessageManager, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
- Return(nil)
- },
- expectedStatusCode: http.StatusNotFound,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "recipient is not online")
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- messageRelayer := &MockMessageRelayer{}
- offlineMsgMgr := &MockOfflineMessageManager{}
- sessionRetriever := &MockSessionRetriever{}
- relFetcher := &MockRelationshipFetcher{}
- sessionMgr, aimsid := createTestSessionManager("testuser")
- handler := &MessagingHandler{
- SessionManager: sessionMgr,
- MessageRelayer: messageRelayer,
- OfflineMessageManager: offlineMsgMgr,
- SessionRetriever: sessionRetriever,
- RelationshipFetcher: relFetcher,
- Logger: slog.Default(),
- }
- tt.setupMocks(messageRelayer, offlineMsgMgr, sessionRetriever, relFetcher)
- reqURL := "/im/sendIM?aimsid=" + aimsid + "&" + tt.queryParams
- req, err := http.NewRequest("GET", reqURL, nil)
- assert.NoError(t, err)
- rr := httptest.NewRecorder()
- handler.SendIM(rr, req)
- assert.Equal(t, tt.expectedStatusCode, rr.Code)
- responseBody := strings.TrimSpace(rr.Body.String())
- if tt.checkResponse != nil {
- tt.checkResponse(t, responseBody)
- }
- messageRelayer.AssertExpectations(t)
- offlineMsgMgr.AssertExpectations(t)
- sessionRetriever.AssertExpectations(t)
- relFetcher.AssertExpectations(t)
- })
- }
- }
- func TestMessagingHandler_SendIM_MissingAimsid(t *testing.T) {
- handler := &MessagingHandler{
- SessionManager: state.NewWebAPISessionManager(),
- Logger: slog.Default(),
- }
- req, err := http.NewRequest("GET", "/im/sendIM", nil)
- assert.NoError(t, err)
- rr := httptest.NewRecorder()
- handler.SendIM(rr, req)
- assert.Equal(t, http.StatusBadRequest, rr.Code)
- assert.Contains(t, rr.Body.String(), "missing required parameter: aimsid")
- }
- func TestMessagingHandler_SendIM_InvalidSession(t *testing.T) {
- handler := &MessagingHandler{
- SessionManager: state.NewWebAPISessionManager(),
- Logger: slog.Default(),
- }
- req, err := http.NewRequest("GET", "/im/sendIM?aimsid=nonexistent&t=someone&message=hi", nil)
- assert.NoError(t, err)
- rr := httptest.NewRecorder()
- handler.SendIM(rr, req)
- assert.Equal(t, http.StatusUnauthorized, rr.Code)
- assert.Contains(t, rr.Body.String(), "invalid or expired session")
- }
- func TestMessagingHandler_SetTyping(t *testing.T) {
- tests := []struct {
- name string
- queryParams string
- setupMocks func(*MockMessageRelayer, *MockSessionRetriever, *MockRelationshipFetcher)
- expectedStatusCode int
- checkResponse func(*testing.T, string)
- }{
- {
- name: "Success_TypingStarted",
- queryParams: "t=recipient&typing=true",
- setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("recipient")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("recipient")).
- Return(&state.Session{})
- mr.On("RelayToScreenName", mock.Anything, state.NewIdentScreenName("recipient"), mock.AnythingOfType("wire.SNACMessage")).
- Return()
- },
- expectedStatusCode: http.StatusOK,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, `"statusCode":200`)
- },
- },
- {
- name: "Success_TypingStopped",
- queryParams: "t=recipient&typing=false",
- setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("recipient")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("recipient")).
- Return(&state.Session{})
- mr.On("RelayToScreenName", mock.Anything, state.NewIdentScreenName("recipient"), mock.AnythingOfType("wire.SNACMessage")).
- Return()
- },
- expectedStatusCode: http.StatusOK,
- },
- {
- name: "Success_BlockedSilent",
- queryParams: "t=blockeduser&typing=true",
- setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("blockeduser")).
- Return(state.Relationship{YouBlock: true}, nil)
- // No relay should happen
- },
- expectedStatusCode: http.StatusOK,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, `"statusCode":200`)
- },
- },
- {
- name: "Success_OfflineRecipient",
- queryParams: "t=offlineuser&typing=true",
- setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {
- rf.On("Relationship", mock.Anything, mock.Anything, state.NewIdentScreenName("offlineuser")).
- Return(state.Relationship{}, nil)
- sr.On("RetrieveSession", state.NewIdentScreenName("offlineuser")).
- Return(nil)
- // No relay should happen for offline users
- },
- expectedStatusCode: http.StatusOK,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, `"statusCode":200`)
- },
- },
- {
- name: "Error_MissingRecipient",
- queryParams: "typing=true",
- setupMocks: func(mr *MockMessageRelayer, sr *MockSessionRetriever, rf *MockRelationshipFetcher) {},
- expectedStatusCode: http.StatusBadRequest,
- checkResponse: func(t *testing.T, body string) {
- assert.Contains(t, body, "missing required parameter: t")
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- messageRelayer := &MockMessageRelayer{}
- sessionRetriever := &MockSessionRetriever{}
- relFetcher := &MockRelationshipFetcher{}
- sessionMgr, aimsid := createTestSessionManager("testuser")
- handler := &MessagingHandler{
- SessionManager: sessionMgr,
- MessageRelayer: messageRelayer,
- SessionRetriever: sessionRetriever,
- RelationshipFetcher: relFetcher,
- Logger: slog.Default(),
- }
- tt.setupMocks(messageRelayer, sessionRetriever, relFetcher)
- reqURL := "/im/setTyping?aimsid=" + aimsid + "&" + tt.queryParams
- req, err := http.NewRequest("GET", reqURL, nil)
- assert.NoError(t, err)
- rr := httptest.NewRecorder()
- handler.SetTyping(rr, req)
- assert.Equal(t, tt.expectedStatusCode, rr.Code)
- if tt.checkResponse != nil {
- responseBody := strings.TrimSpace(rr.Body.String())
- tt.checkResponse(t, responseBody)
- }
- messageRelayer.AssertExpectations(t)
- sessionRetriever.AssertExpectations(t)
- relFetcher.AssertExpectations(t)
- })
- }
- }
- func TestMessagingHandler_SetTyping_MissingAimsid(t *testing.T) {
- handler := &MessagingHandler{
- SessionManager: state.NewWebAPISessionManager(),
- Logger: slog.Default(),
- }
- req, err := http.NewRequest("GET", "/im/setTyping", nil)
- assert.NoError(t, err)
- rr := httptest.NewRecorder()
- handler.SetTyping(rr, req)
- assert.Equal(t, http.StatusBadRequest, rr.Code)
- assert.Contains(t, rr.Body.String(), "missing required parameter: aimsid")
- }
|