diff --git a/server/command_test.go b/server/command_test.go index d602873..d0ef735 100644 --- a/server/command_test.go +++ b/server/command_test.go @@ -68,7 +68,6 @@ func TestHandleConnect(t *testing.T) { mockSetup: func(_ *plugintest.API, _ []byte, _ *MockClient) { }, expectedOutput: tooManyParametersText, - expectError: false, }, { name: "Error connecting user", @@ -80,9 +79,8 @@ func TestHandleConnect(t *testing.T) { api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: model.NewString("https://example.com")}}) mockClient.On("GetMe").Return(&msgraph.User{}, errors.New("error getting user details")) }, - expectedOutput: "", - expectError: true, - expectedError: "error getting user details", + expectError: true, + expectedError: "error getting user details", }, { name: "Successful connection", @@ -93,23 +91,16 @@ func TestHandleConnect(t *testing.T) { api.On("GetConfig").Return(&model.Config{ServiceSettings: model.ServiceSettings{SiteURL: model.NewString("https://example.com")}}) mockClient.On("GetMe").Return(&msgraph.User{}, nil) }, - expectedOutput: "", - expectError: false, + expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - api := &plugintest.API{} + mockAPI := &plugintest.API{} mockClient := &MockClient{} - p := &Plugin{ - MattermostPlugin: plugin.MattermostPlugin{ - API: api, - }, - client: mockClient, - } - + p := SetupMockPlugin(mockAPI, nil, mockClient) p.setConfiguration(&configuration{ EncryptionKey: "demo_encrypt_key", OAuth2ClientID: "demo_oauth2_client_id", @@ -127,7 +118,7 @@ func TestHandleConnect(t *testing.T) { encryptedUserInfo, err := userInfo.EncryptedJSON([]byte("demo_encrypt_key")) require.NoError(t, err) - tt.mockSetup(api, encryptedUserInfo, mockClient) + tt.mockSetup(mockAPI, encryptedUserInfo, mockClient) resp, err := p.handleConnect(tt.args, tt.commandArgs) if tt.expectError { @@ -137,7 +128,7 @@ func TestHandleConnect(t *testing.T) { require.Contains(t, resp, tt.expectedOutput) } - api.AssertExpectations(t) + mockAPI.AssertExpectations(t) }) } } @@ -184,16 +175,10 @@ func TestHandleDisconnect(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - api := &plugintest.API{} + mockAPI := &plugintest.API{} mockTracker := &MockTracker{} - p := &Plugin{ - MattermostPlugin: plugin.MattermostPlugin{ - API: api, - }, - tracker: mockTracker, - } - + p := SetupMockPlugin(mockAPI, mockTracker, nil) p.setConfiguration(&configuration{ EncryptionKey: "demo_encrypt_key", }) @@ -208,13 +193,13 @@ func TestHandleDisconnect(t *testing.T) { encryptedUserInfo, err := userInfo.EncryptedJSON([]byte("demo_encrypt_key")) require.NoError(t, err) - tt.mockSetup(api, encryptedUserInfo, mockTracker) + tt.mockSetup(mockAPI, encryptedUserInfo, mockTracker) resp, err := p.handleDisconnect(tt.args, tt.commandArgs) require.NoError(t, err) require.Contains(t, resp, tt.expectedOutput) - api.AssertExpectations(t) + mockAPI.AssertExpectations(t) mockTracker.AssertExpectations(t) }) } @@ -290,8 +275,6 @@ func TestHandleStart(t *testing.T) { api.On("SendEphemeralPost", "demoUserID", mock.Anything).Return(&model.Post{}) mockTracker.On("TrackUserEvent", mock.Anything, "demoUserID", mock.Anything).Return(nil) }, - expectError: false, - expectedOutput: "", }, { name: "Authentication error", @@ -351,24 +334,16 @@ func TestHandleStart(t *testing.T) { mockClient.On("CreateMeeting", mock.Anything, mock.Anything, mock.Anything).Return(&msgraph.OnlineMeeting{JoinURL: &joinURL}, nil) mockTracker.On("TrackUserEvent", "meeting_started", "demoUserID", mock.Anything).Return(nil) }, - expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - api := &plugintest.API{} + mockAPI := &plugintest.API{} mockTracker := &MockTracker{} mockClient := &MockClient{} - p := &Plugin{ - MattermostPlugin: plugin.MattermostPlugin{ - API: api, - }, - tracker: mockTracker, - client: mockClient, - } - + p := SetupMockPlugin(mockAPI, mockTracker, mockClient) p.setConfiguration(&configuration{ EncryptionKey: "demo_encrypt_key", }) @@ -383,7 +358,7 @@ func TestHandleStart(t *testing.T) { encryptedUserInfo, err := userInfo.EncryptedJSON([]byte("demo_encrypt_key")) require.NoError(t, err) - tt.mockSetup(api, encryptedUserInfo, mockTracker, mockClient) + tt.mockSetup(mockAPI, encryptedUserInfo, mockTracker, mockClient) resp, err := p.handleStart(tt.args, tt.commandArgs) if tt.expectError { @@ -393,7 +368,7 @@ func TestHandleStart(t *testing.T) { require.Contains(t, resp, tt.expectedOutput) } - api.AssertExpectations(t) + mockAPI.AssertExpectations(t) mockTracker.AssertExpectations(t) }) } @@ -410,3 +385,52 @@ func TestGetHelpText(t *testing.T) { actual := p.getHelpText() require.Equal(t, expected, actual) } + +func TestExecuteCommand(t *testing.T) { + mockAPI := &plugintest.API{} + p := SetupMockPlugin(mockAPI, nil, nil) + + var dummyPluginContext plugin.Context + + tests := []struct { + name string + commandArgs model.CommandArgs + expectedMsg string + }{ + { + name: "Invalid Command", + commandArgs: model.CommandArgs{ + Command: "/dummyCommand start", + ChannelId: "dummyChannelID", + UserId: "dummyUserID", + }, + expectedMsg: "Command '/dummyCommand' is not /mstmeetings. Please try again.", + }, + { + name: "Successful execution of Help Command", + commandArgs: model.CommandArgs{ + Command: "/mstmeetings help", + ChannelId: "dummyChannelID", + UserId: "dummyUserID", + }, + expectedMsg: "###### Mattermost MS Teams Meetings Plugin - Slash Command Help\n* `/mstmeetings start` - Start an MS Teams meeting. \n* `/mstmeetings connect` - Connect to MS Teams meeting. \n* `/mstmeetings disconnect` - Disconnect your Mattermost account from MS Teams. \n* `/mstmeetings help` - Display this help text.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + post := &model.Post{ + UserId: p.botUserID, + ChannelId: tt.commandArgs.ChannelId, + Message: tt.expectedMsg, + } + + mockAPI.On("SendEphemeralPost", tt.commandArgs.UserId, post).Return(&model.Post{}).Once() + + response, _ := p.ExecuteCommand(&dummyPluginContext, &tt.commandArgs) // nolint:gosec + + require.Equal(t, &model.CommandResponse{}, response) + mockAPI.AssertExpectations(t) + }) + } +} diff --git a/server/state_test.go b/server/state_test.go new file mode 100644 index 0000000..be839b5 --- /dev/null +++ b/server/state_test.go @@ -0,0 +1,216 @@ +package main + +import ( + "testing" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestStoreState(t *testing.T) { + testCases := []struct { + name string + userID string + channelID string + justConnect bool + returnError error + expectError bool + expectedState string + expectedErrMsg string + }{ + { + name: "Error occurred while storing state", + userID: "mockUserID1", + channelID: "mockChannelID1", + returnError: &model.AppError{Message: "error occurred while storing state"}, + expectError: true, + expectedErrMsg: "error occurred while storing state", + }, + { + name: "Store state successful", + userID: "mockUserID2", + channelID: "mockChannelID2", + justConnect: true, + expectedState: "msteamsmeetinguserstate_mockUserID2_mockChannelID2_true", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockAPI := &plugintest.API{} + p := SetupMockPlugin(mockAPI, nil, nil) + + mockAPI.On("KVSet", mock.Anything, mock.Anything).Return(tc.returnError) + + state, err := p.StoreState(tc.userID, tc.channelID, tc.justConnect) + + if tc.expectError { + require.Error(t, err) + require.Equal(t, tc.expectedErrMsg, err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedState, state) + } + mockAPI.AssertExpectations(t) + }) + } +} + +func TestGetState(t *testing.T) { + testCases := []struct { + name string + key string + getStateValue []byte + getStateError error + expectedState string + expectError bool + expectedErrMsg string + }{ + { + name: "Error occurred while getting stored state", + key: "mockKey", + getStateValue: []byte(""), + getStateError: &model.AppError{Message: "error occurred while getting stored state"}, + expectError: true, + expectedErrMsg: "error occurred while getting stored state", + }, + { + name: "Valid state retrieved", + key: "mockKey", + getStateValue: []byte("mockState"), + expectedState: "mockState", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + api := &plugintest.API{} + p := SetupMockPlugin(api, nil, nil) + + api.On("KVGet", tc.key).Return(tc.getStateValue, tc.getStateError) + + state, err := p.GetState(tc.key) + if tc.expectError { + require.Error(t, err) + require.Equal(t, tc.expectedState, state) + require.Equal(t, tc.expectedErrMsg, err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedState, state) + } + api.AssertExpectations(t) + }) + } +} + +func TestDeleteState(t *testing.T) { + testCases := []struct { + name string + key string + returnError error + expectError bool + expectedErrMsg string + }{ + { + name: "Error occurred while deleting state", + key: "mockKey", + returnError: &model.AppError{Message: "error occurred while deleting state"}, + expectError: true, + expectedErrMsg: "error occurred while deleting state", + }, + { + name: "Delete state successful", + key: "mockKey", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockAPI := &plugintest.API{} + p := SetupMockPlugin(mockAPI, nil, nil) + + mockAPI.On("KVDelete", tc.key).Return(tc.returnError) + + err := p.DeleteState(tc.key) + if tc.expectError { + require.Error(t, err) + require.Equal(t, tc.expectedErrMsg, err.Error()) + } else { + require.NoError(t, err) + } + mockAPI.AssertExpectations(t) + }) + } +} + +func TestParseState(t *testing.T) { + testCases := []struct { + name string + state string + expectedKey string + expectedUserID string + expectedChannelID string + expectedJustConnect bool + expectError bool + expectedErrMsg string + }{ + { + name: "State length mismatch", + state: "key1_userID1_channelID1", + expectError: true, + expectedErrMsg: "status mismatch", + }, + { + name: "Invalid state format", + state: "key1_userID1", + expectError: true, + expectedErrMsg: "status mismatch", + }, + { + name: "Parse state successful", + state: "key1_userID1_channelID1_true", + expectedKey: "key1_userID1", + expectedUserID: "userID1", + expectedChannelID: "channelID1", + expectedJustConnect: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockAPI := &plugintest.API{} + p := SetupMockPlugin(mockAPI, nil, nil) + + if tc.expectError { + mockAPI.On("LogDebug", "complete oauth, state mismatch", "stateComponents", mock.Anything, "state", tc.state).Return() + } + + key, userID, channelID, justConnect, err := p.ParseState(tc.state) + + if tc.expectError { + require.Error(t, err) + require.Equal(t, tc.expectedErrMsg, err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedKey, key) + require.Equal(t, tc.expectedUserID, userID) + require.Equal(t, tc.expectedChannelID, channelID) + require.Equal(t, tc.expectedJustConnect, justConnect) + } + mockAPI.AssertExpectations(t) + }) + } +} + +func SetupMockPlugin(mockAPI *plugintest.API, mockTracker *MockTracker, mockClient *MockClient) *Plugin { + return &Plugin{ + MattermostPlugin: plugin.MattermostPlugin{ + API: mockAPI, + }, + tracker: mockTracker, + client: mockClient, + } +} diff --git a/server/user.go b/server/user.go index c69c0dc..646ad8b 100644 --- a/server/user.go +++ b/server/user.go @@ -227,7 +227,7 @@ func (p *Plugin) resetAllOAuthTokens() { p.API.LogInfo("OAuth2 configuration changed. Resetting all users' tokens, everyone will need to reconnect to MS Teams") appErr := p.API.KVDeleteAll() if appErr != nil { - p.API.LogError("failed to reset users' OAuth2 tokens", "error", appErr.Error()) + p.API.LogError("failed to reset user's OAuth2 tokens", "error", appErr.Error()) return } } diff --git a/server/user_test.go b/server/user_test.go index 5b52af1..c428fb4 100644 --- a/server/user_test.go +++ b/server/user_test.go @@ -4,6 +4,10 @@ import ( "testing" "time" + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin/plugintest" + + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "golang.org/x/oauth2" ) @@ -38,3 +42,89 @@ func TestEncryptUserData(t *testing.T) { require.NoError(t, err) require.EqualValues(t, &expected, decrypted) } + +func TestStoreUserInfo(t *testing.T) { + tests := []struct { + name string + kvSetUserErr error + kvSetRemoteErr error + expectedErr string + }{ + { + name: "Error Saving UserID", + kvSetUserErr: &model.AppError{Message: "some error occurred while saving the user id"}, + expectedErr: "some error occurred while saving the user id", + }, + { + name: "Error Saving RemoteID", + kvSetRemoteErr: &model.AppError{Message: "some error occurred while saving the remote id"}, + expectedErr: "some error occurred while saving the remote id", + }, + { + name: "User Info stored successfully", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAPI := &plugintest.API{} + p := SetupMockPlugin(mockAPI, nil, nil) + p.setConfiguration(&configuration{ + EncryptionKey: "demo_encrypt_key", + }) + + dummyInfo := &UserInfo{ + UserID: "mockUserID", + RemoteID: "mockRemoteID", + } + + mockAPI.On("KVSet", "token_"+dummyInfo.UserID, mock.Anything).Return(tt.kvSetUserErr) + if tt.kvSetUserErr == nil { + mockAPI.On("KVSet", "tbyrid_"+dummyInfo.RemoteID, mock.Anything).Return(tt.kvSetRemoteErr) + } + + responseErr := p.StoreUserInfo(dummyInfo) + if tt.expectedErr == "" { + require.NoError(t, responseErr) + } else { + require.Error(t, responseErr) + require.Equal(t, tt.expectedErr, responseErr.Error()) + } + mockAPI.AssertExpectations(t) + }) + } +} + +func TestResetAllOAuthTokens(t *testing.T) { + tests := []struct { + name string + kvDeleteAllErr error + expectLogError bool + }{ + { + name: "Error Deleting Tokens", + kvDeleteAllErr: &model.AppError{Message: "error in deleting all oauth token"}, + expectLogError: true, + }, + { + name: "No Error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAPI := &plugintest.API{} + p := SetupMockPlugin(mockAPI, nil, nil) + + mockAPI.On("LogInfo", "OAuth2 configuration changed. Resetting all users' tokens, everyone will need to reconnect to MS Teams").Return(nil) + mockAPI.On("KVDeleteAll").Return(tt.kvDeleteAllErr) + + if tt.expectLogError { + mockAPI.On("LogError", "failed to reset user's OAuth2 tokens", "error", tt.kvDeleteAllErr.Error()).Return(nil) + } + + p.resetAllOAuthTokens() + mockAPI.AssertExpectations(t) + }) + } +}