diff --git a/api4/cloud.go b/api4/cloud.go index 2499164fac..d517291d48 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -40,6 +40,7 @@ func (api *API) InitCloud() { api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[A-Za-z0-9]+}/pdf", api.APISessionRequired(getSubscriptionInvoicePDF)).Methods("GET") + api.BaseRoutes.Cloud.Handle("/subscription/expand", api.APISessionRequired(GetLicenseExpandStatus)).Methods("GET") api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(changeSubscription)).Methods("PUT") // GET /api/v4/cloud/request-trial @@ -413,6 +414,34 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } +func GetLicenseExpandStatus(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) { + c.SetPermissionError(model.PermissionManageLicenseInformation) + return + } + + _, token, err := c.App.Srv().GenerateLicenseRenewalLink() + + if err != nil { + c.Err = err + return + } + + res, cloudErr := c.App.Cloud().GetLicenseExpandStatus(c.AppContext.Session().UserId, token) + if cloudErr != nil { + c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(cloudErr) + return + } + + json, jsonErr := json.Marshal(res) + if jsonErr != nil { + c.Err = model.NewAppError("Api4.GetLicenseExpandStatusForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr) + return + } + + w.Write(json) +} + func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.Channels().License().IsCloud() { c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 75bf89f631..399ee67f45 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -649,6 +649,58 @@ func TestGetCloudProducts(t *testing.T) { }) } +func Test_GetExpandStatsForSubscription(t *testing.T) { + isExpandable := &model.SubscriptionExpandStatus{ + IsExpandable: true, + } + + licenseId := "licenseID" + + t.Run("NON Admin users are UNABLE to request expand stats for the subscription", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionExpandable, r, err := th.Client.GetExpandStats(licenseId) + require.Error(t, err) + require.Nil(t, subscriptionExpandable) + require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden") + }) + + t.Run("Admin users are UNABLE to request licenses is expendable due missing the id", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.SystemAdminUser.Email, th.SystemAdminUser.Password) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetLicenseExpandStatus", mock.Anything).Return(isExpandable, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionExpandable, r, err := th.Client.GetExpandStats("") + require.Error(t, err) + require.Nil(t, subscriptionExpandable) + require.Equal(t, http.StatusBadRequest, r.StatusCode, "400 Bad Request") + }) +} + func TestGetSelfHostedProducts(t *testing.T) { products := []*model.Product{ { diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index d58e8c92dd..0c70c02cf3 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -17,6 +17,7 @@ type CloudInterface interface { ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error GetCloudCustomer(userID string) (*model.CloudCustomer, error) + GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) UpdateCloudCustomerAddress(userID string, address *model.Address) (*model.CloudCustomer, error) diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 4b89135dc0..a447399cd8 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -325,6 +325,29 @@ func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.In return r0, r1 } +// GetLicenseExpandStatus provides a mock function with given fields: userID, token +func (_m *CloudInterface) GetLicenseExpandStatus(userID string, token string) (*model.SubscriptionExpandStatus, error) { + ret := _m.Called(userID, token) + + var r0 *model.SubscriptionExpandStatus + if rf, ok := ret.Get(0).(func(string, string) *model.SubscriptionExpandStatus); ok { + r0 = rf(userID, token) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.SubscriptionExpandStatus) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(userID, token) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetLicenseRenewalStatus provides a mock function with given fields: userID, token func (_m *CloudInterface) GetLicenseRenewalStatus(userID string, token string) error { ret := _m.Called(userID, token) diff --git a/model/client4.go b/model/client4.go index 48d63e959c..298334d71a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -8213,6 +8213,19 @@ func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) { return cloudCustomer, BuildResponse(r), nil } +func (c *Client4) GetExpandStats(licenseId string) (*SubscriptionExpandStatus, *Response, error) { + r, err := c.DoAPIGet(fmt.Sprintf("%s%s?licenseID=%s", c.cloudRoute(), "/subscription/expand", licenseId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var subscriptionExpandable *SubscriptionExpandStatus + json.NewDecoder(r.Body).Decode(&subscriptionExpandable) + + return subscriptionExpandable, BuildResponse(r), nil +} + func (c *Client4) GetSubscription() (*Subscription, *Response, error) { r, err := c.DoAPIGet(c.cloudRoute()+"/subscription", "") if err != nil { diff --git a/model/cloud.go b/model/cloud.go index 7feae13e3d..36c639d66c 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -124,6 +124,10 @@ type ValidateBusinessEmailResponse struct { IsValid bool `json:"is_valid"` } +type SubscriptionExpandStatus struct { + IsExpandable bool `json:"is_expandable"` +} + // CloudCustomerInfo represents editable info of a customer. type CloudCustomerInfo struct { Name string `json:"name"`