From d4db43c95e91fe94a7b2574b378437d2b7f65764 Mon Sep 17 00:00:00 2001 From: Nathaniel Allred Date: Fri, 22 Apr 2022 14:23:19 -0500 Subject: [PATCH] Mm 43084 add cloud workspace limits endpoint (#20041) * add cloud workspace limits endpoint --- api4/cloud.go | 34 +++++++ api4/cloud_test.go | 143 ++++++++++++++++++++++++++++ einterfaces/cloud.go | 1 + einterfaces/mocks/CloudInterface.go | 23 +++++ model/client4.go | 13 +++ model/cloud.go | 29 ++++++ model/feature_flags.go | 3 + 7 files changed, 246 insertions(+) create mode 100644 api4/cloud_test.go diff --git a/api4/cloud.go b/api4/cloud.go index ba9f56f5d3..18861c086b 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -19,6 +19,8 @@ import ( func (api *API) InitCloud() { // GET /api/v4/cloud/products api.BaseRoutes.Cloud.Handle("/products", api.APISessionRequired(getCloudProducts)).Methods("GET") + // GET /api/v4/cloud/limits + api.BaseRoutes.Cloud.Handle("/limits", api.APISessionRequired(getCloudLimits)).Methods("GET") // POST /api/v4/cloud/payment // POST /api/v4/cloud/payment/confirm @@ -141,6 +143,38 @@ func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } +func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.Cloud { + c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusNotImplemented) + return + } + + if !c.App.Config().FeatureFlags.CloudFree { + emptyLimits := &model.ProductLimits{} + json, err := json.Marshal(emptyLimits) + if err != nil { + c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + w.Write(json) + return + } + + limits, err := c.App.Cloud().GetCloudLimits(c.AppContext.Session().UserId) + if err != nil { + c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + json, err := json.Marshal(limits) + if err != nil { + c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + w.Write(json) +} + func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.Cloud { c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusNotImplemented) diff --git a/api4/cloud_test.go b/api4/cloud_test.go new file mode 100644 index 0000000000..628230bbc8 --- /dev/null +++ b/api4/cloud_test.go @@ -0,0 +1,143 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "errors" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/v6/einterfaces/mocks" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" +) + +func Test_getCloudLimits(t *testing.T) { + t.Run("feature flag off returns empty limits", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE") + th.App.ReloadConfig() + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + limits, r, err := th.Client.GetProductLimits() + require.NoError(t, err) + require.Equal(t, limits, &model.ProductLimits{}) + require.Equal(t, http.StatusOK, r.StatusCode, "Expected 200 OK") + }) + + t.Run("no license returns not implemented", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE") + th.App.ReloadConfig() + + th.App.Srv().RemoveLicense() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + limits, r, err := th.Client.GetProductLimits() + require.Error(t, err) + require.Nil(t, limits) + require.Equal(t, http.StatusNotImplemented, r.StatusCode, "Expected 501 Not Implemented") + }) + + t.Run("non cloud license returns not implemented", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE") + th.App.ReloadConfig() + + th.App.Srv().SetLicense(model.NewTestLicense()) + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + limits, r, err := th.Client.GetProductLimits() + require.Error(t, err) + require.Nil(t, limits) + require.Equal(t, http.StatusNotImplemented, r.StatusCode, "Expected 501 Not Implemented") + }) + + t.Run("error fetching limits returns internal server error", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE") + th.App.ReloadConfig() + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := &mocks.CloudInterface{} + cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits")) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = cloud + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + limits, r, err := th.Client.GetProductLimits() + require.Error(t, err) + require.Nil(t, limits) + require.Equal(t, http.StatusInternalServerError, r.StatusCode, "Expected 500 Internal Server Error") + }) + + t.Run("unauthenticated users can not access", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Logout() + + limits, r, err := th.Client.GetProductLimits() + require.Error(t, err) + require.Nil(t, limits) + require.Equal(t, http.StatusUnauthorized, r.StatusCode, "Expected 401 Unauthorized") + }) + + t.Run("good request with cloud server and feature flag returns response", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE") + th.App.ReloadConfig() + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := &mocks.CloudInterface{} + ten := 10 + mockLimits := &model.ProductLimits{ + Messages: &model.MessagesLimits{ + History: &ten, + }, + } + cloud.Mock.On("GetCloudLimits", mock.Anything).Return(mockLimits, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = cloud + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + limits, r, err := th.Client.GetProductLimits() + require.NoError(t, err) + require.Equal(t, http.StatusOK, r.StatusCode, "Expected 200 OK") + require.Equal(t, mockLimits, limits) + require.Equal(t, *mockLimits.Messages.History, *limits.Messages.History) + }) +} diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index bbc3a76beb..48c43e6f43 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -9,6 +9,7 @@ import ( type CloudInterface interface { GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error) + GetCloudLimits(userID string) (*model.ProductLimits, error) CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 0d7c59916f..64b33b3dbb 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -97,6 +97,29 @@ func (_m *CloudInterface) GetCloudCustomer(userID string) (*model.CloudCustomer, return r0, r1 } +// GetCloudLimits provides a mock function with given fields: userID +func (_m *CloudInterface) GetCloudLimits(userID string) (*model.ProductLimits, error) { + ret := _m.Called(userID) + + var r0 *model.ProductLimits + if rf, ok := ret.Get(0).(func(string) *model.ProductLimits); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.ProductLimits) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetCloudProducts provides a mock function with given fields: userID, includeLegacyProducts func (_m *CloudInterface) GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error) { ret := _m.Called(userID, includeLegacyProducts) diff --git a/model/client4.go b/model/client4.go index c6e7d887f1..e46112528a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -7707,6 +7707,19 @@ func (c *Client4) GetCloudProducts() ([]*Product, *Response, error) { return cloudProducts, BuildResponse(r), nil } +func (c *Client4) GetProductLimits() (*ProductLimits, *Response, error) { + r, err := c.DoAPIGet(c.cloudRoute()+"/limits", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var productLimits *ProductLimits + json.NewDecoder(r.Body).Decode(&productLimits) + + return productLimits, BuildResponse(r), nil +} + func (c *Client4) CreateCustomerPayment() (*StripeSetupIntent, *Response, error) { r, err := c.DoAPIPost(c.cloudRoute()+"/payment", "") if err != nil { diff --git a/model/cloud.go b/model/cloud.go index 7bab4d86fb..a05e67d355 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -181,3 +181,32 @@ type CloudWorkspaceOwner struct { type SubscriptionChange struct { ProductID string `json:"product_id"` } + +type BoardsLimits struct { + Cards *int `json:"cards"` + Views *int `json:"views"` +} + +type FilesLimits struct { + TotalStorage *int64 `json:"total_storage"` +} + +type IntegrationsLimits struct { + Enabled *int `json:"enabled"` +} + +type MessagesLimits struct { + History *int `json:"history"` +} + +type TeamsLimits struct { + Active *int `json:"active"` +} + +type ProductLimits struct { + Boards *BoardsLimits `json:"boards,omitempty"` + Files *FilesLimits `json:"files,omitempty"` + Integrations *IntegrationsLimits `json:"integrations,omitempty"` + Messages *MessagesLimits `json:"messages,omitempty"` + Teams *TeamsLimits `json:"teams,omitempty"` +} diff --git a/model/feature_flags.go b/model/feature_flags.go index 3fc7294ff2..64e85f347e 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -64,6 +64,8 @@ type FeatureFlags struct { InsightsEnabled bool + CloudFree bool + CommandPalette bool } @@ -88,6 +90,7 @@ func (f *FeatureFlags) SetDefaults() { f.UseCaseOnboarding = true f.GraphQL = false f.InsightsEnabled = false + f.CloudFree = false f.CommandPalette = false } func (f *FeatureFlags) Plugins() map[string]string {