From f08d65909f37de8332c20b6019a6d0fc4ffa0a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Andr=C3=A9s=20V=C3=A9lez=20Vidal?= Date: Wed, 18 May 2022 10:19:38 +0200 Subject: [PATCH] MM-43529 - start freemium trial (#20163) * MM-43529 - start freemium trial * change the method to put * Add unit testing for the request-trial-endpoint * use correct require * add the translation texts * fix texts * remove unnecessary log * change response status code to forbidden when non cloud Co-authored-by: Pablo Velez Vidal --- api4/cloud.go | 40 +++++++++++ api4/cloud_test.go | 105 ++++++++++++++++++++++++++++ einterfaces/cloud.go | 2 + einterfaces/mocks/CloudInterface.go | 23 ++++++ i18n/en.json | 4 ++ model/client4.go | 13 ++++ 6 files changed, 187 insertions(+) diff --git a/api4/cloud.go b/api4/cloud.go index 10b5d3e434..0f7ea1e1d4 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -41,6 +41,9 @@ func (api *API) InitCloud() { api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT") + // GET /api/v4/cloud/request-trial + api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT") + // POST /api/v4/cloud/webhook api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST") } @@ -121,6 +124,43 @@ func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } +func requestCloudTrial(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.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden) + return + } + + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) { + c.SetPermissionError(model.PermissionSysconsoleWriteBilling) + return + } + + if !c.App.Config().FeatureFlags.CloudFree { + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.cloud_free_feature_flag_off_error", nil, "", http.StatusInternalServerError) + return + } + + currentSubscription, appErr := c.App.Cloud().GetSubscription(c.AppContext.Session().UserId) + if appErr != nil { + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, appErr.Error(), http.StatusInternalServerError) + return + } + + changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, currentSubscription.ID) + if err != nil { + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + json, err := json.Marshal(changedSub) + if err != nil { + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + w.Write(json) +} + func getCloudProducts(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.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusNotImplemented) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 753a0e08ec..5a9594bf51 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -141,3 +141,108 @@ func Test_getCloudLimits(t *testing.T) { require.Equal(t, *mockLimits.Messages.History, *limits.Messages.History) }) } + +func Test_requestTrial(t *testing.T) { + subscription := &model.Subscription{ + ID: "MySubscriptionID", + CustomerID: "MyCustomer", + ProductID: "SomeProductId", + AddOns: []string{}, + StartAt: 1000000000, + EndAt: 2000000000, + CreateAt: 1000000000, + Seats: 10, + DNS: "some.dns.server", + IsPaidTier: "false", + } + + t.Run("NON Admin users are UNABLE to request the trial", 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.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything).Return(subscription, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionChanged, r, err := th.Client.RequestCloudTrial() + t.Logf("\n\nresp %#v, \n\n r: %v\n\n, err: %v\n\n", subscriptionChanged, r, err) + require.Error(t, err) + require.Nil(t, subscriptionChanged) + require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden") + }) + + t.Run("cloudFree feature flag FALSE and Admin user are UNABLE to request the trial", 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.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything).Return(subscription, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial() + + require.Error(t, err) + require.Nil(t, subscriptionChanged) + require.Equal(t, http.StatusInternalServerError, r.StatusCode, "Expected 500 Internal Server Error") + }) + + t.Run("cloudFree feature flag TRUE and ADMIN user are ABLE to request the trial", 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.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything).Return(subscription, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial() + + require.NoError(t, err) + require.Equal(t, subscriptionChanged, subscription) + require.Equal(t, http.StatusOK, r.StatusCode, "Status OK") + }) +} diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index 750023965f..a403e9ba2c 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -25,6 +25,8 @@ type CloudInterface interface { ChangeSubscription(userID, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) + RequestCloudTrial(userID, subscriptionID string) (*model.Subscription, error) + // GetLicenseRenewalStatus checks on the portal whether it is possible to use token to renew a license GetLicenseRenewalStatus(userID, token string) error InvalidateCaches() error diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 8ab5cf0c8f..301918b613 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -247,6 +247,29 @@ func (_m *CloudInterface) InvalidateCaches() error { return r0 } +// RequestCloudTrial provides a mock function with given fields: userID, subscriptionID +func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string) (*model.Subscription, error) { + ret := _m.Called(userID, subscriptionID) + + var r0 *model.Subscription + if rf, ok := ret.Get(0).(func(string, string) *model.Subscription); ok { + r0 = rf(userID, subscriptionID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Subscription) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(userID, subscriptionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) { ret := _m.Called(userID, customerInfo) diff --git a/i18n/en.json b/i18n/en.json index c68a18a9a5..834219bc96 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -463,6 +463,10 @@ "id": "api.cloud.app_error", "translation": "Internal error during cloud api request." }, + { + "id": "api.cloud.cloud_free_feature_flag_off_error", + "translation": "CloudFree feature flag is off." + }, { "id": "api.cloud.cws_webhook_event_missing_error", "translation": "Webhook event was not handled. Either it is missing or it is not valid." diff --git a/model/client4.go b/model/client4.go index 04f499172b..e1fed3d4bb 100644 --- a/model/client4.go +++ b/model/client4.go @@ -7784,6 +7784,19 @@ func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodReq return BuildResponse(r), nil } +func (c *Client4) RequestCloudTrial() (*Subscription, *Response, error) { + r, err := c.DoAPIPut(c.cloudRoute()+"/request-trial", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var subscription *Subscription + json.NewDecoder(r.Body).Decode(&subscription) + + return subscription, BuildResponse(r), nil +} + func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) { r, err := c.DoAPIGet(c.cloudRoute()+"/customer", "") if err != nil {