Skip to content

Commit

Permalink
Merge branch 'epic-ui' of s.github.com:Brightscout/mattermost-plugin-…
Browse files Browse the repository at this point in the history
…msteams-sync into MI-3832
  • Loading branch information
SaurabhSharma-884 committed Dec 15, 2023
2 parents 7d082b6 + ee3ad52 commit 30447cc
Show file tree
Hide file tree
Showing 95 changed files with 5,974 additions and 1,650 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ bin/

node_modules

coverage
6 changes: 6 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@
"value": "UserPrincipalName"
}
]
},{
"key": "enableRhs",
"display_name": "Enable RHS",
"type": "bool",
"help_text": "When true, users will be able to view RHS (right hand sidebar).",
"default": false
},{
"key": "appManifestDownload",
"display_name": "Download Manifest",
Expand Down
60 changes: 34 additions & 26 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ const (
QueryParamSearchTerm = "search"

// Path params
PathParamTeamID = "team_id"
PathParamChannelID = "channel_id"
PathParamTeamID = "team_id"
PathParamChannelID = "channel_id"
PathParamMSTeamsUserID = "user_id"

// Used for storing the token in the request context to pass from one middleware to another
// #nosec G101 -- This is a false positive. The below line is not a hardcoded credential
Expand Down Expand Up @@ -84,7 +85,7 @@ func NewAPI(p *Plugin, store store.Store) *API {
msTeamsRouter := router.PathPrefix("/msteams").Subrouter()
channelsRouter := router.PathPrefix("/channels").Subrouter()

router.HandleFunc("/avatar/{userId:.*}", api.getAvatar).Methods(http.MethodGet)
router.HandleFunc(fmt.Sprintf("/avatar/{%s:.*}", PathParamMSTeamsUserID), api.getAvatar).Methods(http.MethodGet)
router.HandleFunc("/changes", api.processActivity).Methods(http.MethodPost)
router.HandleFunc("/lifecycle", api.processLifecycle).Methods(http.MethodPost)
router.HandleFunc("/needsConnect", api.handleAuthRequired(api.needsConnect)).Methods(http.MethodGet)
Expand All @@ -94,7 +95,7 @@ func NewAPI(p *Plugin, store store.Store) *API {
router.HandleFunc("/oauth-redirect", api.oauthRedirectHandler).Methods(http.MethodGet)
router.HandleFunc("/connected-users", api.getConnectedUsers).Methods(http.MethodGet)
router.HandleFunc("/connected-users/download", api.getConnectedUsersFile).Methods(http.MethodGet)
router.HandleFunc("/whitelist-user", api.handleAuthRequired(api.whitelistUser)).Methods(http.MethodGet)
router.HandleFunc("/config", api.handleAuthRequired(api.getConfig)).Methods(http.MethodGet)

channelsRouter.HandleFunc("/link", api.handleAuthRequired(api.checkUserConnected(api.linkChannels))).Methods(http.MethodPost)
channelsRouter.HandleFunc(fmt.Sprintf("/{%s}/unlink", PathParamChannelID), api.handleAuthRequired(api.unlinkChannels)).Methods(http.MethodDelete)
Expand All @@ -117,7 +118,7 @@ func NewAPI(p *Plugin, store store.Store) *API {
// getAvatar returns the microsoft teams avatar
func (a *API) getAvatar(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
userID := params["userId"]
userID := params[PathParamMSTeamsUserID]
photo, appErr := a.store.GetAvatarCache(userID)
if appErr != nil || len(photo) == 0 {
var err error
Expand Down Expand Up @@ -490,8 +491,13 @@ func (a *API) getLinkedChannels(w http.ResponseWriter, r *http.Request) {

paginatedLinks := []*storemodels.ChannelLink{}
if len(links) > 0 {
mmChannelSortNames := map[string]string{}
for _, link := range links {
mmChannelSortNames[link.MattermostChannelID] = fmt.Sprintf("%s_%s", link.MattermostChannelName, link.MattermostChannelID)
}

sort.Slice(links, func(i, j int) bool {
return fmt.Sprintf("%s_%s", links[i].MattermostChannelName, links[i].MattermostChannelID) < fmt.Sprintf("%s_%s", links[j].MattermostChannelName, links[j].MattermostChannelID)
return mmChannelSortNames[links[i].MattermostChannelID] < mmChannelSortNames[links[j].MattermostChannelID]
})

msTeamsTeamIDsVsNames, msTeamsChannelIDsVsNames, errorsFound := a.p.GetMSTeamsTeamAndChannelDetailsFromChannelLinks(links, userID, true)
Expand All @@ -500,7 +506,7 @@ func (a *API) getLinkedChannels(w http.ResponseWriter, r *http.Request) {
return
}

searchTerm := r.URL.Query().Get(QueryParamSearchTerm)
searchTerm := strings.ToLower(strings.TrimSpace(r.URL.Query().Get(QueryParamSearchTerm)))
offset, limit := a.p.GetOffsetAndLimit(r.URL.Query())
matchCount := 0
for _, link := range links {
Expand All @@ -512,7 +518,7 @@ func (a *API) getLinkedChannels(w http.ResponseWriter, r *http.Request) {
break
}

if strings.HasPrefix(strings.ToLower(link.MattermostChannelName), strings.ToLower(searchTerm)) {
if strings.HasPrefix(strings.ToLower(link.MattermostChannelName), searchTerm) {
if matchCount < offset {
matchCount++
continue
Expand Down Expand Up @@ -550,11 +556,16 @@ func (a *API) getMSTeamsTeamList(w http.ResponseWriter, r *http.Request) {
return
}

msTeamsTeamSortNames := map[string]string{}
for _, team := range teams {
msTeamsTeamSortNames[team.ID] = fmt.Sprintf("%s_%s", team.DisplayName, team.ID)
}

sort.Slice(teams, func(i, j int) bool {
return fmt.Sprintf("%s_%s", teams[i].DisplayName, teams[i].ID) < fmt.Sprintf("%s_%s", teams[j].DisplayName, teams[j].ID)
return msTeamsTeamSortNames[teams[i].ID] < msTeamsTeamSortNames[teams[j].ID]
})

searchTerm := r.URL.Query().Get(QueryParamSearchTerm)
searchTerm := strings.ToLower(r.URL.Query().Get(QueryParamSearchTerm))
offset, limit := a.p.GetOffsetAndLimit(r.URL.Query())
paginatedTeams := []*clientmodels.Team{}
matchCount := 0
Expand All @@ -563,7 +574,7 @@ func (a *API) getMSTeamsTeamList(w http.ResponseWriter, r *http.Request) {
break
}

if strings.HasPrefix(strings.ToLower(team.DisplayName), strings.ToLower(searchTerm)) {
if strings.HasPrefix(strings.ToLower(team.DisplayName), searchTerm) {
if matchCount >= offset {
paginatedTeams = append(paginatedTeams, team)
} else {
Expand All @@ -584,11 +595,16 @@ func (a *API) getMSTeamsTeamChannels(w http.ResponseWriter, r *http.Request) {
return
}

msTeamsChannelSortNames := map[string]string{}
for _, channel := range channels {
msTeamsChannelSortNames[channel.ID] = fmt.Sprintf("%s_%s", channel.DisplayName, channel.ID)
}

sort.Slice(channels, func(i, j int) bool {
return fmt.Sprintf("%s_%s", channels[i].DisplayName, channels[i].ID) < fmt.Sprintf("%s_%s", channels[j].DisplayName, channels[j].ID)
return msTeamsChannelSortNames[channels[i].ID] < msTeamsChannelSortNames[channels[j].ID]
})

searchTerm := r.URL.Query().Get(QueryParamSearchTerm)
searchTerm := strings.ToLower(r.URL.Query().Get(QueryParamSearchTerm))
offset, limit := a.p.GetOffsetAndLimit(r.URL.Query())
paginatedChannels := []*clientmodels.Channel{}
matchCount := 0
Expand All @@ -597,7 +613,7 @@ func (a *API) getMSTeamsTeamChannels(w http.ResponseWriter, r *http.Request) {
break
}

if strings.HasPrefix(strings.ToLower(channel.DisplayName), strings.ToLower(searchTerm)) {
if strings.HasPrefix(strings.ToLower(channel.DisplayName), searchTerm) {
if matchCount >= offset {
paginatedChannels = append(paginatedChannels, channel)
} else {
Expand All @@ -619,7 +635,7 @@ func (a *API) linkChannels(w http.ResponseWriter, r *http.Request) {
return
}

if err := storemodels.IsChannelLinkPayloadValid(body); err != nil {
if err := body.IsValid(); err != nil {
a.p.API.LogError("Invalid channel link payload.", "Error", err.Error())
http.Error(w, "Invalid channel link payload.", http.StatusBadRequest)
return
Expand Down Expand Up @@ -891,17 +907,9 @@ func (a *API) getConnectedUsersFile(w http.ResponseWriter, r *http.Request) {
}
}

func (a *API) whitelistUser(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get(HeaderMattermostUserID)
presentInWhitelist, err := a.p.store.IsUserPresentInWhitelist(userID)
if err != nil {
a.p.API.LogError("Error in checking if a user is present in whitelist", "UserID", userID, "Error", err.Error())
http.Error(w, "error in checking if a user is present in whitelist", http.StatusInternalServerError)
return
}

func (a *API) getConfig(w http.ResponseWriter, _ *http.Request) {
response := map[string]bool{
"presentInWhitelist": presentInWhitelist,
"rhsEnabled": a.p.getConfiguration().EnableRHS,
}

w.Header().Set("Content-Type", "application/json")
Expand Down Expand Up @@ -981,7 +989,7 @@ func (a *API) checkUserConnected(handleFunc http.HandlerFunc) http.HandlerFunc {
token, err := a.p.store.GetTokenForMattermostUser(mattermostUserID)
if err != nil {
a.p.API.LogError("Unable to get the oauth token for the user.", "UserID", mattermostUserID, "Error", err.Error())
http.Error(w, "The account is not connected.", http.StatusBadRequest)
http.Error(w, "The account is not connected.", http.StatusUnauthorized)
return
}

Expand Down
52 changes: 12 additions & 40 deletions server/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ func TestDisconnect(t *testing.T) {
mockmetrics.On("IncrementHTTPErrors").Times(1)
},
ExpectedResult: "The account is not connected.\n",
ExpectedStatusCode: http.StatusBadRequest,
ExpectedStatusCode: http.StatusUnauthorized,
},
{
Name: "Disconnect: error occurred while setting the user info",
Expand Down Expand Up @@ -1813,64 +1813,36 @@ func TestUnlinkChannels(t *testing.T) {
}
}

func TestWhitelistUser(t *testing.T) {
func TestGetConfig(t *testing.T) {
for _, test := range []struct {
Name string
SetupPlugin func(*plugintest.API)
SetupStore func(*storemocks.Store)
SetupMetrics func(*metricsmocks.Metrics)
SetupPlugin func(*Plugin)
ExpectedResult string
ExpectedStatusCode int
}{
{
Name: "WhitelistUser: unable to check user in the whitelist",
SetupPlugin: func(api *plugintest.API) {
api.On("LogError", "Error in checking if a user is present in whitelist", "UserID", testutils.GetUserID(), "Error", "unable to check user in the whitelist").Times(1)
},
SetupStore: func(store *storemocks.Store) {
store.On("IsUserPresentInWhitelist", testutils.GetUserID()).Return(false, errors.New("unable to check user in the whitelist")).Times(1)
},
SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {
mockmetrics.On("IncrementHTTPErrors").Times(1)
},
ExpectedResult: "error in checking if a user is present in whitelist\n",
ExpectedStatusCode: http.StatusInternalServerError,
},
{
Name: "WhitelistUser: user is not present in whitelist",
SetupPlugin: func(api *plugintest.API) {},
SetupStore: func(store *storemocks.Store) {
store.On("IsUserPresentInWhitelist", testutils.GetUserID()).Return(false, nil).Times(1)
Name: "RhsEnabled: rhs is enabled",
SetupPlugin: func(p *Plugin) {
p.configuration.EnableRHS = true
},
SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {},
ExpectedResult: `{"presentInWhitelist":false}`,
ExpectedResult: `{"rhsEnabled":true}`,
ExpectedStatusCode: http.StatusOK,
},
{
Name: "WhitelistUser: user present in whitelist",
SetupPlugin: func(api *plugintest.API) {},
SetupStore: func(store *storemocks.Store) {
store.On("IsUserPresentInWhitelist", testutils.GetUserID()).Return(true, nil).Times(1)
},
SetupMetrics: func(mockmetrics *metricsmocks.Metrics) {},
ExpectedResult: `{"presentInWhitelist":true}`,
Name: "RhsEnabled: rhs is not enabled",
SetupPlugin: func(p *Plugin) {},
ExpectedResult: `{"rhsEnabled":false}`,
ExpectedStatusCode: http.StatusOK,
},
} {
t.Run(test.Name, func(t *testing.T) {
assert := assert.New(t)
plugin := newTestPlugin(t)
mockAPI := &plugintest.API{}

plugin.SetAPI(mockAPI)
defer mockAPI.AssertExpectations(t)

test.SetupPlugin(mockAPI)
test.SetupStore(plugin.store.(*storemocks.Store))
test.SetupMetrics(plugin.metricsService.(*metricsmocks.Metrics))
test.SetupPlugin(plugin)

w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/whitelist-user", nil)
r := httptest.NewRequest(http.MethodGet, "/config", nil)
r.Header.Add(HeaderMattermostUserID, testutils.GetUserID())
plugin.ServeHTTP(nil, w, r)

Expand Down
1 change: 1 addition & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type configuration struct {
SyntheticUserAuthService string `json:"syntheticUserAuthService"`
SyntheticUserAuthData string `json:"syntheticUserAuthData"`
AutomaticallyPromoteSyntheticUsers bool `json:"automaticallyPromoteSyntheticUsers"`
EnableRHS bool `json:"enableRhs"`
}

func (c *configuration) ProcessConfiguration() {
Expand Down
2 changes: 1 addition & 1 deletion server/store/storemodels/storemodels.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ type ConnectedUser struct {
Email string
}

func IsChannelLinkPayloadValid(body *ChannelLink) error {
func (body *ChannelLink) IsValid() error {
if body == nil {
return errors.New("invalid body")
}
Expand Down
3 changes: 3 additions & 0 deletions webapp/__mocks__/svg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// https://react-svgr.com/docs/jest/
export default 'SvgrURL';
export const ReactComponent = 'div';
44 changes: 44 additions & 0 deletions webapp/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

const config = {
presets: [
['@babel/preset-env', {
targets: {
chrome: 66,
firefox: 60,
},
modules: false,
corejs: 3,
debug: false,
useBuiltIns: 'usage',
shippedProposals: true,
}],
['@babel/preset-react', {
useBuiltIns: true,
}],
['@babel/typescript', {
allExtensions: true,
isTSX: true,
}],
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-optional-chaining',
'babel-plugin-typescript-to-proptypes',
],
};

// Jest needs module transformation
config.env = {
test: {
presets: config.presets,
plugins: config.plugins,
},
};

// Refer for more information: https://babeljs.io/docs/babel-preset-env#modules
config.env.test.presets[0][1].modules = 'auto';

module.exports = config;
Loading

0 comments on commit 30447cc

Please sign in to comment.