Merge pull request #21914 from mattermost/MM-47853-true-up-review-telemetry-off-non-air-gapped

MM-47853 - True Up Review Backend
Этот коммит содержится в:
Conor Macpherson
2023-01-17 11:17:07 -05:00
коммит произвёл GitHub
родитель 240304ad07 357428dfb0
Коммит c99f9b1f57
32 изменённых файлов: 1228 добавлений и 2 удалений

Просмотреть файл

@@ -52,6 +52,9 @@ func (api *API) InitCloud() {
// POST /api/v4/cloud/webhook
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
// GET /api/v4/cloud/cws-health-check
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
}
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -746,3 +749,12 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request) {
if err := c.App.Cloud().CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.health_check.app_error", nil, "CWS Server is not available.", http.StatusInternalServerError)
return
}
ReturnStatusOK(w)
}

Просмотреть файл

@@ -5,6 +5,7 @@ package api4
import (
"bytes"
b64 "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -24,6 +25,8 @@ func (api *API) InitLicense() {
api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
api.BaseRoutes.APIRoot.Handle("/license/review", api.APISessionRequired(requestTrueUpReview)).Methods("POST")
api.BaseRoutes.APIRoot.Handle("/license/review/status", api.APISessionRequired(trueUpReviewStatus)).Methods("GET")
}
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -296,3 +299,100 @@ func getPrevTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJSON(clientLicense)))
}
func requestTrueUpReview(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.not_allowed_for_cloud", nil, "", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
return
}
// If a true up review has already been submitted for the current due date, complete the request
// with no errors.
if status.Completed {
ReturnStatusOK(w)
}
profileMap, err := c.App.GetTrueUpProfile()
if err != nil {
c.Err = model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "", http.StatusInternalServerError)
return
}
profileMapJson, err := json.Marshal(profileMap)
if err != nil {
c.SetJSONEncodingError(err)
return
}
// Do not send true-up review data if the user has already requested one for the quarter.
// And only send a true-up review via as a one-time telemetry request if telemetry is disabled.
telemetryEnabled := c.App.Config().LogSettings.EnableDiagnostics
if telemetryEnabled != nil && !*telemetryEnabled {
// Send telemetry data
c.App.Srv().GetTelemetryService().SendTelemetry(model.TrueUpReviewTelemetryName, profileMap)
// Update the review status to reflect the completion.
status.Completed = true
c.App.Srv().Store().TrueUpReview().Update(status)
}
// Encode to string rather than byte[] otherwise json.Marshal will encode it further.
encodedData := b64.StdEncoding.EncodeToString(profileMapJson)
responseContent := struct {
Content string `json:"content"`
}{Content: encodedData}
response, _ := json.Marshal(responseContent)
w.Write(response)
}
func trueUpReviewStatus(c *Context, w http.ResponseWriter, r *http.Request) {
// Only admins can request a true up review.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
}
// Check for license
license := c.App.Channels().License()
if license == nil {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.license_required", nil, "True up review requires a license", http.StatusNotImplemented)
return
}
if license.IsCloud() {
c.Err = model.NewAppError("cloudTrueUpReviewNotAllowed", "api.license.true_up_review.not_allowed_for_cloud", nil, "True up review is not allowed for cloud instances", http.StatusNotImplemented)
return
}
status, appErr := c.App.GetOrCreateTrueUpReviewStatus()
if appErr != nil {
c.Err = appErr
}
json, err := json.Marshal(status)
if err != nil {
c.Err = model.NewAppError("trueUpReviewStatus", "api.marshal_error", nil, "", http.StatusInternalServerError)
return
}
w.Write(json)
}

Просмотреть файл

@@ -328,3 +328,77 @@ func TestRequestRenewalLink(t *testing.T) {
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
func TestRequestTrueUpReview(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense())
t.Run("returns status 200 when telemetry data sent", func(t *testing.T) {
resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("returns 501 when ran by cloud user", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
th.App.Srv().SetLicense(model.NewTestLicense())
})
t.Run("returns 403 when user does not have permissions", func(t *testing.T) {
resp, err := th.Client.DoAPIPost("/license/review", "")
require.Error(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("returns 400 when license is nil", func(t *testing.T) {
th.App.Srv().SetLicense(nil)
resp, err := th.SystemAdminClient.DoAPIPost("/license/review", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}
func TestTrueUpReviewStatus(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense())
t.Run("returns 200 when status retrieved", func(t *testing.T) {
resp, err := th.SystemAdminClient.DoAPIGet("/license/review/status", "")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("returns 501 when ran by cloud user", func(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
resp, err := th.SystemAdminClient.DoAPIGet("/license/review/status", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
th.App.Srv().SetLicense(model.NewTestLicense())
})
t.Run("returns 403 when user does not have permissions", func(t *testing.T) {
resp, err := th.Client.DoAPIGet("/license/review/status", "")
require.Error(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("returns 400 when license is nil", func(t *testing.T) {
th.App.Srv().SetLicense(nil)
resp, err := th.SystemAdminClient.DoAPIGet("/license/review/status", "")
require.Error(t, err)
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}

Просмотреть файл

@@ -699,6 +699,7 @@ type AppIface interface {
GetOnboarding() (*model.System, *model.AppError)
GetOpenGraphMetadata(requestURL string) ([]byte, error)
GetOrCreateDirectChannel(c request.CTX, userID, otherUserID string, channelOptions ...model.ChannelOption) (*model.Channel, *model.AppError)
GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError)
GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError)
GetOutgoingWebhooksForChannelPageByUser(channelID string, userID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError)
GetOutgoingWebhooksForTeamPage(teamID string, page, perPage int) ([]*model.OutgoingWebhook, *model.AppError)
@@ -811,6 +812,7 @@ type AppIface interface {
GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError)
GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError)
GetTopThreadsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError)
GetTrueUpProfile() (map[string]any, error)
GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError)
GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError)
GetUser(userID string) (*model.User, *model.AppError)

Просмотреть файл

@@ -7584,6 +7584,28 @@ func (a *OpenTracingAppLayer) GetOrCreateDirectChannel(c request.CTX, userID str
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOrCreateTrueUpReviewStatus")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetOrCreateTrueUpReviewStatus()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetOutgoingWebhook(hookID string) (*model.OutgoingWebhook, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetOutgoingWebhook")
@@ -10328,6 +10350,28 @@ func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUse
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTrueUpProfile() (map[string]any, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTrueUpProfile")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTrueUpProfile()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUploadSession")

168
app/true_up.go Обычный файл
Просмотреть файл

@@ -0,0 +1,168 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"os"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/services/telemetry"
"github.com/mattermost/mattermost-server/v6/store"
"github.com/mattermost/mattermost-server/v6/utils"
)
func pluginActivated(pluginStates map[string]*model.PluginState, pluginId string) bool {
state, ok := pluginStates[pluginId]
if !ok {
return false
}
return state.Enable
}
func (a *App) getMarketplacePlugins() ([]string, error) {
ts := a.Srv().telemetryService
config := a.Srv().Config()
marketplacePlugins, err := ts.GetAllMarketplacePlugins(model.PluginSettingsDefaultMarketplaceURL)
if err != nil {
return nil, err
}
activePlugins := []string{}
for _, p := range marketplacePlugins {
id := p.Manifest.Id
if pluginActivated(config.PluginSettings.PluginStates, id) {
activePlugins = append(activePlugins, id)
}
}
return activePlugins, nil
}
func (a *App) getTrueUpProfile() (*model.TrueUpReviewProfile, error) {
license := a.Channels().License()
if license == nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.license_required", nil, "Could not get the total active users count", http.StatusInternalServerError)
}
// Customer Info & Usage Analytics
activeUserCount, err := a.Srv().Store().Status().GetTotalActiveUsersCount()
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.user_count_fail", nil, "Could not get the total active users count", http.StatusInternalServerError)
}
// Webhook, calls, boards, and playbook counts
incomingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsIncomingCount("")
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_in_count_fail", nil, "Could not get the total incoming webhook count", http.StatusInternalServerError)
}
outgoingWebhookCount, err := a.Srv().Store().Webhook().AnalyticsOutgoingCount("")
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.webhook_out_count_fail", nil, "Could not get the total outgoing webhook count", http.StatusInternalServerError)
}
// Plugin Data
trueUpReviewPlugins := model.TrueUpReviewPlugins{
PluginNames: []string{},
}
if plugins, err := a.getMarketplacePlugins(); err == nil {
trueUpReviewPlugins.PluginNames = plugins
trueUpReviewPlugins.TotalPlugins = len(plugins)
}
// Authentication Features
config := a.Config()
mfaUsed := config.ServiceSettings.EnforceMultifactorAuthentication
ldapUsed := config.LdapSettings.Enable
samlUsed := config.SamlSettings.Enable
openIdUsed := config.OpenIdSettings.Enable
guestAccessAllowed := config.GuestAccountsSettings.Enable
authFeatures := map[string]*bool{
model.TrueUpReviewAuthFeaturesMfa: mfaUsed,
model.TrueUpReviewAuthFeaturesADLdap: ldapUsed,
model.TrueUpReviewAuthFeaturesSaml: samlUsed,
model.TrueUpReviewAuthFeatureOpenId: openIdUsed,
model.TrueUpReviewAuthFeatureGuestAccess: guestAccessAllowed,
}
authFeatureList := []string{}
for feature, used := range authFeatures {
if used != nil && *used {
authFeatureList = append(authFeatureList, feature)
}
}
reviewProfile := model.TrueUpReviewProfile{
ServerId: a.TelemetryId(),
ServerVersion: model.CurrentVersion,
ServerInstallationType: os.Getenv(telemetry.EnvVarInstallType),
LicenseId: license.Id,
LicensedSeats: *license.Features.Users,
LicensePlan: license.SkuName,
CustomerName: license.Customer.Name,
ActiveUsers: activeUserCount,
TotalIncomingWebhooks: incomingWebhookCount,
TotalOutgoingWebhooks: outgoingWebhookCount,
Plugins: trueUpReviewPlugins,
AuthenticationFeatures: authFeatureList,
}
return &reviewProfile, nil
}
func (a *App) GetTrueUpProfile() (map[string]any, error) {
profile, err := a.getTrueUpProfile()
if err != nil {
return nil, err
}
profileJson, err := json.Marshal(profile)
if err != nil {
return nil, err
}
telemetryProperties := map[string]any{}
json.Unmarshal(profileJson, &telemetryProperties)
delete(telemetryProperties, "plugins")
plugins := profile.Plugins.ToMap()
for key, pluginValue := range plugins {
telemetryProperties[key] = pluginValue
}
delete(telemetryProperties, "authentication_features")
telemetryProperties["authentication_features"] = strings.Join(profile.AuthenticationFeatures, ",")
return telemetryProperties, nil
}
func (a *App) GetOrCreateTrueUpReviewStatus() (*model.TrueUpReviewStatus, *model.AppError) {
nextDueDate := utils.GetNextTrueUpReviewDueDate(time.Now())
status, err := a.Srv().Store().TrueUpReview().GetTrueUpReviewStatus(nextDueDate.UnixMilli())
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
a.Log().Warn("Could not find true up review status")
default:
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.get_status_error", nil, "Could not get true up status records", http.StatusInternalServerError).Wrap(err)
}
status, err = a.Srv().Store().TrueUpReview().CreateTrueUpReviewStatusRecord(&model.TrueUpReviewStatus{DueDate: nextDueDate.UnixMilli(), Completed: false})
if err != nil {
return nil, model.NewAppError("requestTrueUpReview", "api.license.true_up_review.create_error", nil, "Could not create true up status record", http.StatusInternalServerError)
}
}
return status, nil
}

Просмотреть файл

@@ -200,6 +200,8 @@ db/migrations/mysql/000099_create_drafts.down.sql
db/migrations/mysql/000099_create_drafts.up.sql
db/migrations/mysql/000100_add_draft_priority_column.down.sql
db/migrations/mysql/000100_add_draft_priority_column.up.sql
db/migrations/mysql/000101_create_true_up_review_history.down.sql
db/migrations/mysql/000101_create_true_up_review_history.up.sql
db/migrations/postgres/000001_create_teams.down.sql
db/migrations/postgres/000001_create_teams.up.sql
db/migrations/postgres/000002_create_team_members.down.sql
@@ -400,3 +402,5 @@ db/migrations/postgres/000099_create_drafts.down.sql
db/migrations/postgres/000099_create_drafts.up.sql
db/migrations/postgres/000100_add_draft_priority_column.down.sql
db/migrations/postgres/000100_add_draft_priority_column.up.sql
db/migrations/postgres/000101_create_true_up_review_history.down.sql
db/migrations/postgres/000101_create_true_up_review_history.up.sql

Просмотреть файл

@@ -0,0 +1 @@
DROP TABLE IF EXISTS TrueUpReviewHistory;

Просмотреть файл

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS TrueUpReviewHistory (
DueDate bigint(20),
Completed boolean,
PRIMARY KEY (DueDate)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Просмотреть файл

@@ -0,0 +1 @@
DROP TABLE IF EXISTS trueupreviewhistory;

Просмотреть файл

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS trueupreviewhistory (
duedate bigint,
completed boolean,
PRIMARY KEY (duedate)
);

Просмотреть файл

@@ -45,4 +45,6 @@ type CloudInterface interface {
CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error)
HandleLicenseChange() error
CheckCWSConnection(userId string) error
}

Просмотреть файл

@@ -60,6 +60,20 @@ func (_m *CloudInterface) ChangeSubscription(userID string, subscriptionID strin
return r0, r1
}
// CheckCWSConnection provides a mock function with given fields: userId
func (_m *CloudInterface) CheckCWSConnection(userId string) error {
ret := _m.Called(userId)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(userId)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfirmCustomerPayment provides a mock function with given fields: userID, confirmRequest
func (_m *CloudInterface) ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error {
ret := _m.Called(userID, confirmRequest)

Просмотреть файл

@@ -2077,6 +2077,34 @@
"id": "api.license.request_trial_license.fail_get_user_count.app_error",
"translation": "Unable to get a trial license, please try again or contact with support@mattermost.com. Cannot obtain the number of registered users."
},
{
"id": "api.license.true_up_review.create_error",
"translation": "Could not create true up status record"
},
{
"id": "api.license.true_up_review.get_status_error",
"translation": "Could not get true up status records"
},
{
"id": "api.license.true_up_review.license_required",
"translation": "True up review requires a license"
},
{
"id": "api.license.true_up_review.not_allowed_for_cloud",
"translation": "True up review is not allowed for cloud instances"
},
{
"id": "api.license.true_up_review.user_count_fail",
"translation": "Could not get the total active users count"
},
{
"id": "api.license.true_up_review.webhook_in_count_fail",
"translation": "Could not get the total incoming webhook count"
},
{
"id": "api.license.true_up_review.webhook_out_count_fail",
"translation": "Could not get the total outgoing webhook count"
},
{
"id": "api.license.upgrade_needed.app_error",
"translation": "Feature requires an upgrade to Enterprise Edition."
@@ -2543,6 +2571,10 @@
"id": "api.scheme.patch_scheme.license.error",
"translation": "Your license does not support update permissions schemes"
},
{
"id": "api.server.cws.health_check.app_error",
"translation": "CWS Server is not available."
},
{
"id": "api.server.hosted_signup_unavailable.error",
"translation": "Portal unavailable for self-hosted signup."

Просмотреть файл

@@ -8700,6 +8700,17 @@ func (c *Client4) AddUserToGroupSyncables(userID string) (*Response, error) {
return BuildResponse(r), nil
}
func (c *Client4) CheckCWSConnection(userId string) (*Response, error) {
r, err := c.DoAPIGet(c.cloudRoute()+"/healthz", "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// Worktemplates sections
func (c *Client4) worktemplatesRoute() string {

Просмотреть файл

@@ -39,6 +39,15 @@ var (
sanctionedTrialDurationUpperBound = 29*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 696 hours (29 days) + 23 hours, 59 mins and 59 seconds
)
const (
TrueUpReviewTelemetryName = "true_up_review_sent"
TrueUpReviewAuthFeaturesMfa = "multi_factor_authentication"
TrueUpReviewAuthFeaturesADLdap = "ad_ldap_sign_in"
TrueUpReviewAuthFeaturesSaml = "saml_sign_in"
TrueUpReviewAuthFeatureOpenId = "openid_connect"
TrueUpReviewAuthFeatureGuestAccess = "guest_access"
)
type LicenseRecord struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`

45
model/true_up_review_profile.go Обычный файл
Просмотреть файл

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "strings"
type TrueUpReviewProfile struct {
ServerId string `json:"server_id"`
ServerVersion string `json:"server_version"`
ServerInstallationType string `json:"server_installation_type"`
LicenseId string `json:"license_id"`
LicensedSeats int `json:"licensed_seats"`
LicensePlan string `json:"license_plan"`
CustomerName string `json:"customer_name"`
ActiveUsers int64 `json:"active_users"`
AuthenticationFeatures []string `json:"authentication_features"`
Plugins TrueUpReviewPlugins `json:"plugins"`
TotalIncomingWebhooks int64 `json:"incoming_webhooks_count"`
TotalOutgoingWebhooks int64 `json:"outgoing_webhooks_count"`
}
type TrueUpReviewPlugins struct {
TotalPlugins int `json:"total_plugins"`
PluginNames []string `json:"plugin_names"`
}
func (t *TrueUpReviewPlugins) ToMap() map[string]interface{} {
return map[string]interface{}{
"total_plugins": t.TotalPlugins,
"plugin_names": strings.Join(t.PluginNames, ","),
}
}
type TrueUpReviewStatus struct {
Completed bool `json:"complete"`
DueDate int64 `json:"due_date"`
}
func (t *TrueUpReviewStatus) ToSlice() []interface{} {
return []interface{}{
t.DueDate,
t.Completed,
}
}

Просмотреть файл

@@ -1421,7 +1421,7 @@ func (ts *TelemetryService) trackPluginConfig(cfg *model.Config, marketplaceURL
"focalboard",
}
marketplacePlugins, err := ts.getAllMarketplaceplugins(marketplaceURL)
marketplacePlugins, err := ts.GetAllMarketplacePlugins(marketplaceURL)
if err != nil {
mlog.Info("Failed to fetch marketplace plugins for telemetry. Using predefined list.", mlog.Err(err))
@@ -1459,7 +1459,7 @@ func (ts *TelemetryService) trackPluginConfig(cfg *model.Config, marketplaceURL
ts.SendTelemetry(TrackConfigPlugin, pluginConfigData)
}
func (ts *TelemetryService) getAllMarketplaceplugins(marketplaceURL string) ([]*model.BaseMarketplacePlugin, error) {
func (ts *TelemetryService) GetAllMarketplacePlugins(marketplaceURL string) ([]*model.BaseMarketplacePlugin, error) {
marketplaceClient, err := marketplace.NewClient(
marketplaceURL,
ts.srv.HTTPService(),

Просмотреть файл

@@ -55,6 +55,7 @@ type OpenTracingLayer struct {
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
@@ -206,6 +207,10 @@ func (s *OpenTracingLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *OpenTracingLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *OpenTracingLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
@@ -406,6 +411,11 @@ type OpenTracingLayerTokenStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *OpenTracingLayer
}
type OpenTracingLayerUploadSessionStore struct {
store.UploadSessionStore
Root *OpenTracingLayer
@@ -10612,6 +10622,60 @@ func (s *OpenTracingLayerTokenStore) Save(recovery *model.Token) error {
return err
}
func (s *OpenTracingLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.CreateTrueUpReviewStatusRecord")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.GetTrueUpReviewStatus")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TrueUpReviewStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.TrueUpReviewStore.Update(reviewStatus)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUploadSessionStore) Delete(id string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UploadSessionStore.Delete")
@@ -12840,6 +12904,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.TermsOfServiceStore = &OpenTracingLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &OpenTracingLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &OpenTracingLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &OpenTracingLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &OpenTracingLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &OpenTracingLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &OpenTracingLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}

Просмотреть файл

@@ -58,6 +58,7 @@ type RetryLayer struct {
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
@@ -209,6 +210,10 @@ func (s *RetryLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *RetryLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *RetryLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
@@ -409,6 +414,11 @@ type RetryLayerTokenStore struct {
Root *RetryLayer
}
type RetryLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *RetryLayer
}
type RetryLayerUploadSessionStore struct {
store.UploadSessionStore
Root *RetryLayer
@@ -12126,6 +12136,69 @@ func (s *RetryLayerTokenStore) Save(recovery *model.Token) error {
}
func (s *RetryLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
tries := 0
for {
result, err := s.TrueUpReviewStore.Update(reviewStatus)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUploadSessionStore) Delete(id string) error {
tries := 0
@@ -14630,6 +14703,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.TermsOfServiceStore = &RetryLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &RetryLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &RetryLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &RetryLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &RetryLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &RetryLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &RetryLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}

Просмотреть файл

@@ -57,6 +57,7 @@ func genStore() *mocks.Store {
mock.On("Draft").Return(&mocks.DraftStore{})
mock.On("PostPriority").Return(&mocks.PostPriorityStore{})
mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{})
mock.On("TrueUpReview").Return(&mocks.TrueUpReviewStore{})
return mock
}

Просмотреть файл

@@ -112,6 +112,7 @@ type SqlStoreStores struct {
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
trueUpReview store.TrueUpReviewStore
}
type SqlStore struct {
@@ -220,6 +221,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
store.stores.postPriority = newSqlPostPriorityStore(store)
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
store.stores.trueUpReview = newSqlTrueUpReviewStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -984,6 +986,10 @@ func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore {
return ss.stores.postAcknowledgement
}
func (ss *SqlStore) TrueUpReview() store.TrueUpReviewStore {
return ss.stores.trueUpReview
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO

80
store/sqlstore/true_up_review_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"strconv"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/store"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
// SqlLicenseStore encapsulates the database writes and reads for
// model.LicenseRecord objects.
type SqlTrueUpReviewStore struct {
*SqlStore
}
func newSqlTrueUpReviewStore(sqlStore *SqlStore) store.TrueUpReviewStore {
return &SqlTrueUpReviewStore{sqlStore}
}
func trueUpReviewStatusColumns() []string {
return []string{
"DueDate",
"Completed",
}
}
func (s *SqlTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
query := s.getQueryBuilder().
Select("*").
From("TrueUpReviewHistory").
Where(sq.Eq{"DueDate": dueDate})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_trueUpReviewStatusRecord_tosql")
}
var trueUpReviewStatus model.TrueUpReviewStatus
if err := s.GetReplicaX().Get(&trueUpReviewStatus, queryString, args...); err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("TrueUpReviewStatus", strconv.FormatInt(dueDate, 10))
}
return nil, err
}
return &trueUpReviewStatus, nil
}
func (s *SqlTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
builder := s.getQueryBuilder().Insert("TrueUpReviewHistory").Columns(trueUpReviewStatusColumns()...).Values(reviewStatus.ToSlice()...)
query, args, err := builder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "create_trueUpReviewStatusRecord_tosql")
}
if _, err = s.GetMasterX().Exec(query, args...); err != nil {
return nil, errors.Wrap(err, "fail to create true up review status record")
}
return reviewStatus, nil
}
func (s *SqlTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
query := s.getQueryBuilder().
Update("TrueUpReviewHistory").
Set("Completed", reviewStatus.Completed).
Where(sq.Eq{"DueDate": reviewStatus.DueDate})
if _, err := s.GetMasterX().ExecBuilder(query); err != nil {
return nil, errors.Wrapf(err, "failed to update true up review status with DueDate=%d", reviewStatus.DueDate)
}
return reviewStatus, nil
}

14
store/sqlstore/true_up_review_store_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost-server/v6/store/storetest"
)
func TestTrueUpReviewStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestTrueUpReviewStatusStore)
}

Просмотреть файл

@@ -87,6 +87,7 @@ type Store interface {
NotifyAdmin() NotifyAdminStore
PostPriority() PostPriorityStore
PostAcknowledgement() PostAcknowledgementStore
TrueUpReview() TrueUpReviewStore
}
type RetentionPolicyStore interface {
@@ -998,6 +999,12 @@ type PostAcknowledgementStore interface {
Delete(acknowledgement *model.PostAcknowledgement) error
}
type TrueUpReviewStore interface {
GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error)
CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error)
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.

Просмотреть файл

@@ -843,6 +843,22 @@ func (_m *Store) TotalSearchDbConnections() int {
return r0
}
// TrueUpReview provides a mock function with given fields:
func (_m *Store) TrueUpReview() store.TrueUpReviewStore {
ret := _m.Called()
var r0 store.TrueUpReviewStore
if rf, ok := ret.Get(0).(func() store.TrueUpReviewStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.TrueUpReviewStore)
}
}
return r0
}
// UnlockFromMaster provides a mock function with given fields:
func (_m *Store) UnlockFromMaster() {
_m.Called()

84
store/storetest/mocks/TrueUpReviewStore.go Обычный файл
Просмотреть файл

@@ -0,0 +1,84 @@
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// TrueUpReviewStore is an autogenerated mock type for the TrueUpReviewStore type
type TrueUpReviewStore struct {
mock.Mock
}
// CreateTrueUpReviewStatusRecord provides a mock function with given fields: reviewStatus
func (_m *TrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(reviewStatus)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok {
r0 = rf(reviewStatus)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok {
r1 = rf(reviewStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTrueUpReviewStatus provides a mock function with given fields: dueDate
func (_m *TrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(dueDate)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(int64) *model.TrueUpReviewStatus); ok {
r0 = rf(dueDate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(dueDate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: reviewStatus
func (_m *TrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
ret := _m.Called(reviewStatus)
var r0 *model.TrueUpReviewStatus
if rf, ok := ret.Get(0).(func(*model.TrueUpReviewStatus) *model.TrueUpReviewStatus); ok {
r0 = rf(reviewStatus)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TrueUpReviewStatus)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*model.TrueUpReviewStatus) error); ok {
r1 = rf(reviewStatus)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

Просмотреть файл

@@ -59,6 +59,7 @@ type Store struct {
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
PostAcknowledgementStore mocks.PostAcknowledgementStore
TrueUpReviewStore mocks.TrueUpReviewStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@@ -100,6 +101,7 @@ func (s *Store) Draft() store.DraftStore { return &s.D
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
return &s.ChannelMemberHistoryStore
}
func (s *Store) TrueUpReview() store.TrueUpReviewStore { return &s.TrueUpReviewStore }
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }

82
store/storetest/true_up_review_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/store"
"github.com/mattermost/mattermost-server/v6/utils"
"github.com/stretchr/testify/assert"
)
func TestTrueUpReviewStatusStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("CreateTrueUpReviewStatusRecord", func(t *testing.T) { testCreateTrueUpReviewStatus(t, ss) })
t.Run("GetTrueUpReviewStatus", func(t *testing.T) { testGetTrueUpReviewStatus(t, ss) })
t.Run("Update", func(t *testing.T) { testUpdateTrueUpReviewStatus(t, ss) })
}
func testCreateTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.Local)
reviewStatus := model.TrueUpReviewStatus{
Completed: true,
DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(),
}
t.Run("create true up review status", func(t *testing.T) {
resp, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
assert.Equal(t, reviewStatus.Completed, resp.Completed)
assert.Equal(t, reviewStatus.DueDate, resp.DueDate)
})
}
func testGetTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.August, 1, 0, 0, 0, 0, time.Local)
dueDate := utils.GetNextTrueUpReviewDueDate(now).UnixMilli()
reviewStatus := model.TrueUpReviewStatus{
Completed: true,
DueDate: dueDate,
}
_, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
t.Run("get true up review status", func(t *testing.T) {
resp, err := ss.TrueUpReview().GetTrueUpReviewStatus(dueDate)
assert.NoError(t, err)
assert.Equal(t, resp.Completed, resp.Completed)
assert.Equal(t, resp.DueDate, resp.DueDate)
})
}
func testUpdateTrueUpReviewStatus(t *testing.T, ss store.Store) {
now := time.Date(time.Now().Year(), time.April, 1, 0, 0, 0, 0, time.Local)
reviewStatus := model.TrueUpReviewStatus{
Completed: false,
DueDate: utils.GetNextTrueUpReviewDueDate(now).UnixMilli(),
}
_, err := ss.TrueUpReview().CreateTrueUpReviewStatusRecord(&reviewStatus)
assert.NoError(t, err)
t.Run("save ", func(t *testing.T) {
reviewStatus.Completed = true
resp, err := ss.TrueUpReview().Update(&reviewStatus)
assert.NoError(t, err)
assert.Equal(t, resp.Completed, resp.Completed)
assert.Equal(t, resp.DueDate, resp.DueDate)
})
}

Просмотреть файл

@@ -54,6 +54,7 @@ type TimerLayer struct {
TermsOfServiceStore store.TermsOfServiceStore
ThreadStore store.ThreadStore
TokenStore store.TokenStore
TrueUpReviewStore store.TrueUpReviewStore
UploadSessionStore store.UploadSessionStore
UserStore store.UserStore
UserAccessTokenStore store.UserAccessTokenStore
@@ -205,6 +206,10 @@ func (s *TimerLayer) Token() store.TokenStore {
return s.TokenStore
}
func (s *TimerLayer) TrueUpReview() store.TrueUpReviewStore {
return s.TrueUpReviewStore
}
func (s *TimerLayer) UploadSession() store.UploadSessionStore {
return s.UploadSessionStore
}
@@ -405,6 +410,11 @@ type TimerLayerTokenStore struct {
Root *TimerLayer
}
type TimerLayerTrueUpReviewStore struct {
store.TrueUpReviewStore
Root *TimerLayer
}
type TimerLayerUploadSessionStore struct {
store.UploadSessionStore
Root *TimerLayer
@@ -9549,6 +9559,54 @@ func (s *TimerLayerTokenStore) Save(recovery *model.Token) error {
return err
}
func (s *TimerLayerTrueUpReviewStore) CreateTrueUpReviewStatusRecord(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.CreateTrueUpReviewStatusRecord(reviewStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.CreateTrueUpReviewStatusRecord", success, elapsed)
}
return result, err
}
func (s *TimerLayerTrueUpReviewStore) GetTrueUpReviewStatus(dueDate int64) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.GetTrueUpReviewStatus(dueDate)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.GetTrueUpReviewStatus", success, elapsed)
}
return result, err
}
func (s *TimerLayerTrueUpReviewStore) Update(reviewStatus *model.TrueUpReviewStatus) (*model.TrueUpReviewStatus, error) {
start := time.Now()
result, err := s.TrueUpReviewStore.Update(reviewStatus)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TrueUpReviewStore.Update", success, elapsed)
}
return result, err
}
func (s *TimerLayerUploadSessionStore) Delete(id string) error {
start := time.Now()
@@ -11573,6 +11631,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.TermsOfServiceStore = &TimerLayerTermsOfServiceStore{TermsOfServiceStore: childStore.TermsOfService(), Root: &newStore}
newStore.ThreadStore = &TimerLayerThreadStore{ThreadStore: childStore.Thread(), Root: &newStore}
newStore.TokenStore = &TimerLayerTokenStore{TokenStore: childStore.Token(), Root: &newStore}
newStore.TrueUpReviewStore = &TimerLayerTrueUpReviewStore{TrueUpReviewStore: childStore.TrueUpReview(), Root: &newStore}
newStore.UploadSessionStore = &TimerLayerUploadSessionStore{UploadSessionStore: childStore.UploadSession(), Root: &newStore}
newStore.UserStore = &TimerLayerUserStore{UserStore: childStore.User(), Root: &newStore}
newStore.UserAccessTokenStore = &TimerLayerUserAccessTokenStore{UserAccessTokenStore: childStore.UserAccessToken(), Root: &newStore}

82
utils/true_up.go Обычный файл
Просмотреть файл

@@ -0,0 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"time"
)
const trueUpReviewDueDay = 15
const day = time.Hour * 24
type DueDateWindow struct {
Start time.Time
End time.Time
}
func GetNextTrueUpReviewDueDate(now time.Time) time.Time {
nowYear := now.Year()
nowMonth := now.Month()
nowDay := now.Day()
finalQuarterYear := nowYear
if nowMonth >= time.October && nowMonth <= time.December {
finalQuarterYear = nowYear + 1
}
trueUpSubmissionWindows := []DueDateWindow{
{
Start: time.Date(now.Year(), time.January, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.April, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.April, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.July, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.July, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(now.Year(), time.October, 15, 0, 0, 0, 0, now.Location()),
},
{
Start: time.Date(now.Year(), time.October, 16, 0, 0, 0, 0, now.Location()),
End: time.Date(finalQuarterYear, time.January, 15, 0, 0, 0, 0, now.Location()),
},
}
for _, window := range trueUpSubmissionWindows {
withinWindow := false
// Our due dates "wrap" around (i.e. can go into the next year), so we'll need to check the months different. Since January = 1 and December = 12, the checks
// for the current month being greater or equal to the start month and less than or equal to the end month will not work.
if window.End.Month() == time.January {
withinWindow = (nowMonth != time.January && nowMonth >= window.Start.Month()) || nowMonth == window.End.Month()
} else {
withinWindow = nowMonth >= window.Start.Month() && nowMonth <= window.End.Month()
}
// Only check the days if the current month is equal to the start or end months.
// The dates of the middle month(s) don't matter so much.
isFirstMonth := nowMonth == window.Start.Month()
if isFirstMonth {
withinWindow = withinWindow && nowDay >= window.Start.Day()
}
isFinalMonth := nowMonth == window.End.Month()
if isFinalMonth {
withinWindow = withinWindow && nowDay <= window.End.Day()
}
if withinWindow {
return window.End
}
}
return trueUpSubmissionWindows[0].End
}
func IsTrueUpReviewDueDateWithinTheNext30Days(now time.Time, dueDate time.Time) bool {
dueDateWindow := dueDate.Add(-day * 30)
if now.Before(dueDateWindow) || now.After(dueDate) {
return false
}
return true
}

125
utils/true_up_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package utils
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGetNextTrueUpReviewDueDate(t *testing.T) {
t.Run("Due date always falls on the 15th", func(t *testing.T) {
// Before the 15th
now := time.Date(2022, time.March, 14, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, trueUpReviewDueDay, due.Day())
// On the 15th
now = time.Date(2022, time.December, 15, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, trueUpReviewDueDay, due.Day())
// After the 15th
now = time.Date(2022, time.September, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, trueUpReviewDueDay, due.Day())
})
t.Run("Due date will always be in next quarter if the current date is past the 15th", func(t *testing.T) {
now := time.Date(2022, time.March, 16, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.April, due.Month())
now = time.Date(2022, time.June, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.July, due.Month())
now = time.Date(2022, time.September, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.October, due.Month())
now = time.Date(2022, time.December, 16, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.January, due.Month())
})
t.Run("Due date will always be in the current quarter if the current date is before or on the 15th", func(t *testing.T) {
now := time.Date(2022, time.April, 15, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.April, due.Month())
now = time.Date(2022, time.July, 15, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.July, due.Month())
now = time.Date(2022, time.October, 14, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.October, due.Month())
now = time.Date(2022, time.January, 14, 0, 0, 0, 0, time.Local)
due = GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.January, due.Month())
})
t.Run("Due date will be in the next year if the next quarter is not within the current year", func(t *testing.T) {
now := time.Date(2022, time.October, 21, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
assert.Equal(t, time.January, due.Month())
assert.Equal(t, 2023, due.Year())
})
}
func TestIsTrueUpReviewDueDateWithinTheNext15Days(t *testing.T) {
t.Run("Ensure a date within 30 days before the due date returns true", func(t *testing.T) {
// 1 Day before the due date
now := time.Date(2022, time.March, 16, 0, 0, 0, 0, time.Local)
// Due date is December 15th, 2022
due := GetNextTrueUpReviewDueDate(now)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.True(t, res)
})
t.Run("Ensure a date that is more than two weeks before the due date returns false", func(t *testing.T) {
// 15 Days before the due date
now := time.Date(2022, time.October, 16, 0, 0, 0, 0, time.Local)
// Due date is December 15th, 2022
due := GetNextTrueUpReviewDueDate(now)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.False(t, res)
})
t.Run("Ensure a date that is past the due date returns false", func(t *testing.T) {
now := time.Date(2022, time.April, 15, 0, 0, 0, 0, time.Local)
// Due date is April 16th, 2022
dueNow := time.Date(2022, time.April, 16, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(dueNow)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.False(t, res)
})
t.Run("Ensure a date that is on the due date returns true", func(t *testing.T) {
now := time.Date(2022, time.January, 15, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
fmt.Printf("\n\ndue date: %s\n\n", due.Format("2006-Jan-02"))
fmt.Printf("\n\nnow: %s\n\n", now.Format("2006-Jan-02"))
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.True(t, res)
})
t.Run("Ensure a date that is on the first day of the due date window returns true", func(t *testing.T) {
now := time.Date(2022, time.December, 16, 0, 0, 0, 0, time.Local)
due := GetNextTrueUpReviewDueDate(now)
res := IsTrueUpReviewDueDateWithinTheNext30Days(now, due)
assert.True(t, res)
})
}