Path: blob/main/components/gitpod-db/go/cost_center_test.go
2497 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package db_test56import (7"context"8"sync"9"testing"10"time"1112db "github.com/gitpod-io/gitpod/components/gitpod-db/go"1314"github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest"15"github.com/google/uuid"16"github.com/stretchr/testify/require"17"google.golang.org/grpc/codes"18"google.golang.org/grpc/status"19"gorm.io/gorm"20)2122func TestCostCenter_WriteRead(t *testing.T) {23conn := dbtest.ConnectForTests(t)2425costCenter := &db.CostCenter{26ID: db.NewTeamAttributionID(uuid.New().String()),27SpendingLimit: 100,28}29cleanUp(t, conn, costCenter.ID)3031tx := conn.Create(costCenter)32require.NoError(t, tx.Error)3334read := &db.CostCenter{ID: costCenter.ID}35tx = conn.First(read)36require.NoError(t, tx.Error)37require.Equal(t, costCenter.ID, read.ID)38require.Equal(t, costCenter.SpendingLimit, read.SpendingLimit)39}4041func TestCostCenterManager_GetOrCreateCostCenter_concurrent(t *testing.T) {42conn := dbtest.ConnectForTests(t)43mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{44ForTeams: 0,45ForUsers: 500,46})47id := db.NewTeamAttributionID(uuid.New().String())48cleanUp(t, conn, id)4950waitgroup := &sync.WaitGroup{}51save := func() {52_, err := mnr.GetOrCreateCostCenter(context.Background(), id)53require.NoError(t, err)54waitgroup.Done()55}56waitgroup.Add(10)57for i := 0; i < 10; i++ {58go save()59}60waitgroup.Wait()61}6263func TestCostCenterManager_GetOrCreateCostCenter(t *testing.T) {64conn := dbtest.ConnectForTests(t)65mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{66ForTeams: 0,67ForUsers: 500,68})69team := db.NewTeamAttributionID(uuid.New().String())70cleanUp(t, conn, team)7172teamCC, err := mnr.GetOrCreateCostCenter(context.Background(), team)73require.NoError(t, err)74t.Cleanup(func() {75conn.Model(&db.CostCenter{}).Delete(teamCC)76})77require.Equal(t, int32(0), teamCC.SpendingLimit)78}7980func TestCostCenterManager_GetOrCreateCostCenter_ResetsExpired(t *testing.T) {81conn := dbtest.ConnectForTests(t)82mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{83ForTeams: 0,84ForUsers: 500,85})8687now := time.Now().UTC()88ts := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), 0, time.UTC)89expired := ts.Add(-1 * time.Minute)90unexpired := ts.Add(1 * time.Minute)9192expiredCC := db.CostCenter{93ID: db.NewTeamAttributionID(uuid.New().String()),94CreationTime: db.NewVarCharTime(now),95SpendingLimit: 0,96BillingStrategy: db.CostCenter_Other,97NextBillingTime: db.NewVarCharTime(expired),98BillingCycleStart: db.NewVarCharTime(now),99}100unexpiredCC := db.CostCenter{101ID: db.NewTeamAttributionID(uuid.New().String()),102CreationTime: db.NewVarCharTime(now),103SpendingLimit: 500,104BillingStrategy: db.CostCenter_Other,105NextBillingTime: db.NewVarCharTime(unexpired),106BillingCycleStart: db.NewVarCharTime(now),107}108// Stripe billing strategy should not be reset109stripeCC := db.CostCenter{110ID: db.NewTeamAttributionID(uuid.New().String()),111CreationTime: db.NewVarCharTime(now),112SpendingLimit: 0,113BillingStrategy: db.CostCenter_Stripe,114NextBillingTime: db.VarcharTime{},115BillingCycleStart: db.NewVarCharTime(now),116}117118dbtest.CreateCostCenters(t, conn,119dbtest.NewCostCenter(t, expiredCC),120dbtest.NewCostCenter(t, unexpiredCC),121dbtest.NewCostCenter(t, stripeCC),122)123124// expired db.CostCenter should be reset, so we get a new CreationTime125retrievedExpiredCC, err := mnr.GetOrCreateCostCenter(context.Background(), expiredCC.ID)126require.NoError(t, err)127t.Cleanup(func() {128conn.Model(&db.CostCenter{}).Delete(retrievedExpiredCC.ID)129})130require.Equal(t, db.NewVarCharTime(expired).Time().AddDate(0, 1, 0), retrievedExpiredCC.NextBillingTime.Time())131require.Equal(t, expiredCC.ID, retrievedExpiredCC.ID)132require.Equal(t, expiredCC.BillingStrategy, retrievedExpiredCC.BillingStrategy)133require.WithinDuration(t, now, expiredCC.CreationTime.Time(), 3*time.Second, "new cost center creation time must be within 3 seconds of now")134135// unexpired cost center must not be reset136retrievedUnexpiredCC, err := mnr.GetOrCreateCostCenter(context.Background(), unexpiredCC.ID)137require.NoError(t, err)138require.Equal(t, db.NewVarCharTime(unexpired).Time(), retrievedUnexpiredCC.NextBillingTime.Time())139require.Equal(t, unexpiredCC.ID, retrievedUnexpiredCC.ID)140require.Equal(t, unexpiredCC.BillingStrategy, retrievedUnexpiredCC.BillingStrategy)141require.WithinDuration(t, unexpiredCC.CreationTime.Time(), retrievedUnexpiredCC.CreationTime.Time(), 100*time.Millisecond)142143// stripe cost center must not be reset144retrievedStripeCC, err := mnr.GetOrCreateCostCenter(context.Background(), stripeCC.ID)145require.NoError(t, err)146require.False(t, retrievedStripeCC.NextBillingTime.IsSet())147require.Equal(t, stripeCC.ID, retrievedStripeCC.ID)148require.Equal(t, stripeCC.BillingStrategy, retrievedStripeCC.BillingStrategy)149require.WithinDuration(t, stripeCC.CreationTime.Time(), retrievedStripeCC.CreationTime.Time(), 100*time.Millisecond)150}151152func TestCostCenterManager_UpdateCostCenter(t *testing.T) {153conn := dbtest.ConnectForTests(t)154limits := db.DefaultSpendingLimit{155ForTeams: 0,156ForUsers: 500,157MinForUsersOnStripe: 1000,158}159160t.Run("prevents updates to negative spending limit", func(t *testing.T) {161mnr := db.NewCostCenterManager(conn, limits)162teamAttributionID := db.NewTeamAttributionID(uuid.New().String())163cleanUp(t, conn, teamAttributionID)164165_, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{166ID: teamAttributionID,167BillingStrategy: db.CostCenter_Stripe,168SpendingLimit: -1,169})170require.Error(t, err)171require.Equal(t, codes.InvalidArgument, status.Code(err))172})173174t.Run("team on Stripe billing strategy can set arbitrary positive spending limit", func(t *testing.T) {175mnr := db.NewCostCenterManager(conn, limits)176teamAttributionID := db.NewTeamAttributionID(uuid.New().String())177cleanUp(t, conn, teamAttributionID)178179// Allows udpating cost center as long as spending limit remains as configured180res, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{181ID: teamAttributionID,182BillingStrategy: db.CostCenter_Stripe,183SpendingLimit: limits.ForTeams,184})185require.NoError(t, err)186requireCostCenterEqual(t, db.CostCenter{187ID: teamAttributionID,188BillingStrategy: db.CostCenter_Stripe,189SpendingLimit: limits.ForTeams,190}, res)191192// Allows updating cost center to any positive value193_, err = mnr.UpdateCostCenter(context.Background(), db.CostCenter{194ID: teamAttributionID,195BillingStrategy: db.CostCenter_Stripe,196SpendingLimit: 10,197})198require.NoError(t, err)199})200201t.Run("increment billing cycle should always increment to now", func(t *testing.T) {202mnr := db.NewCostCenterManager(conn, limits)203teamAttributionID := db.NewTeamAttributionID(uuid.New().String())204cleanUp(t, conn, teamAttributionID)205206res, err := mnr.GetOrCreateCostCenter(context.Background(), teamAttributionID)207require.NoError(t, err)208209// set res.nextBillingTime to two months ago210res.NextBillingTime = db.NewVarCharTime(time.Now().AddDate(0, -2, 0))211conn.Save(res)212213cc, err := mnr.IncrementBillingCycle(context.Background(), teamAttributionID)214require.NoError(t, err)215216require.True(t, cc.NextBillingTime.Time().After(time.Now()), "The next billing time should be in the future")217require.True(t, cc.BillingCycleStart.Time().Before(time.Now()), "The next billing time should be in the future")218})219}220221func TestSaveCostCenterMovedToStripe(t *testing.T) {222conn := dbtest.ConnectForTests(t)223mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{224ForTeams: 20,225ForUsers: 500,226})227team := db.NewTeamAttributionID(uuid.New().String())228cleanUp(t, conn, team)229teamCC, err := mnr.GetOrCreateCostCenter(context.Background(), team)230require.NoError(t, err)231require.Equal(t, int32(20), teamCC.SpendingLimit)232233teamCC.BillingStrategy = db.CostCenter_Stripe234teamCC.SpendingLimit = 400050235teamCC, err = mnr.UpdateCostCenter(context.Background(), teamCC)236require.NoError(t, err)237require.Equal(t, db.CostCenter_Stripe, teamCC.BillingStrategy)238require.Equal(t, teamCC.CreationTime.Time().AddDate(0, 1, 0), teamCC.NextBillingTime.Time())239require.Equal(t, int32(400050), teamCC.SpendingLimit)240241teamCC.BillingStrategy = db.CostCenter_Other242teamCC, err = mnr.UpdateCostCenter(context.Background(), teamCC)243require.NoError(t, err)244require.Equal(t, teamCC.CreationTime.Time().AddDate(0, 1, 0).Truncate(time.Second), teamCC.NextBillingTime.Time().Truncate(time.Second))245require.Equal(t, int32(20), teamCC.SpendingLimit)246}247248func cleanUp(t *testing.T, conn *gorm.DB, attributionIds ...db.AttributionID) {249t.Helper()250t.Cleanup(func() {251for _, attributionId := range attributionIds {252conn.Where("id = ?", string(attributionId)).Delete(&db.CostCenter{})253conn.Where("attributionId = ?", string(attributionId)).Delete(&db.Usage{})254}255})256}257258func requireCostCenterEqual(t *testing.T, expected, actual db.CostCenter) {259t.Helper()260261// ignore timestamps in comparsion262require.Equal(t, expected.ID, actual.ID)263require.EqualValues(t, expected.SpendingLimit, actual.SpendingLimit)264require.Equal(t, expected.BillingStrategy, actual.BillingStrategy)265}266267func TestCostCenter_ListLatestCostCentersWithBillingTimeBefore(t *testing.T) {268269t.Run("no cost centers found when no data exists", func(t *testing.T) {270conn := dbtest.ConnectForTests(t)271mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{272ForTeams: 0,273ForUsers: 500,274})275276ts := time.Date(2022, 10, 10, 10, 10, 10, 10, time.UTC)277278retrieved, err := mnr.ListManagedCostCentersWithBillingTimeBefore(context.Background(), ts.Add(7*24*time.Hour))279require.NoError(t, err)280require.Len(t, retrieved, 0)281})282283t.Run("returns the most recent cost center (by creation time)", func(t *testing.T) {284conn := dbtest.ConnectForTests(t)285mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{286ForTeams: 0,287ForUsers: 500,288})289290attributionID := uuid.New().String()291firstCreation := time.Date(2022, 10, 10, 10, 10, 10, 10, time.UTC)292secondCreation := firstCreation.Add(24 * time.Hour)293294costCenters := []db.CostCenter{295dbtest.NewCostCenter(t, db.CostCenter{296ID: db.NewTeamAttributionID(attributionID),297SpendingLimit: 100,298CreationTime: db.NewVarCharTime(firstCreation),299BillingStrategy: db.CostCenter_Other,300NextBillingTime: db.NewVarCharTime(firstCreation),301}),302dbtest.NewCostCenter(t, db.CostCenter{303ID: db.NewTeamAttributionID(attributionID),304SpendingLimit: 100,305CreationTime: db.NewVarCharTime(secondCreation),306BillingStrategy: db.CostCenter_Other,307NextBillingTime: db.NewVarCharTime(secondCreation),308}),309}310311dbtest.CreateCostCenters(t, conn, costCenters...)312313retrieved, err := mnr.ListManagedCostCentersWithBillingTimeBefore(context.Background(), secondCreation.Add(7*24*time.Hour))314require.NoError(t, err)315require.Len(t, retrieved, 1)316317requireCostCenterEqual(t, costCenters[1], retrieved[0])318})319320t.Run("returns results only when most recent cost center matches billing strategy", func(t *testing.T) {321conn := dbtest.ConnectForTests(t)322mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{323ForTeams: 0,324ForUsers: 500,325})326327attributionID := uuid.New().String()328firstCreation := time.Date(2022, 10, 10, 10, 10, 10, 10, time.UTC)329secondCreation := firstCreation.Add(24 * time.Hour)330331costCenters := []db.CostCenter{332dbtest.NewCostCenter(t, db.CostCenter{333ID: db.NewTeamAttributionID(attributionID),334SpendingLimit: 100,335CreationTime: db.NewVarCharTime(firstCreation),336BillingStrategy: db.CostCenter_Other,337NextBillingTime: db.NewVarCharTime(firstCreation),338}),339dbtest.NewCostCenter(t, db.CostCenter{340ID: db.NewTeamAttributionID(attributionID),341SpendingLimit: 100,342CreationTime: db.NewVarCharTime(secondCreation),343BillingStrategy: db.CostCenter_Stripe,344}),345}346347dbtest.CreateCostCenters(t, conn, costCenters...)348349retrieved, err := mnr.ListManagedCostCentersWithBillingTimeBefore(context.Background(), secondCreation.Add(7*24*time.Hour))350require.NoError(t, err)351require.Len(t, retrieved, 0)352})353}354355func TestCostCenterManager_ResetUsage(t *testing.T) {356now := time.Now().UTC()357ts := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), 0, time.UTC)358359t.Run("errors when cost center is not Other", func(t *testing.T) {360conn := dbtest.ConnectForTests(t)361mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{362ForTeams: 0,363ForUsers: 500,364})365cc := dbtest.CreateCostCenters(t, conn, db.CostCenter{366ID: db.NewTeamAttributionID(uuid.New().String()),367CreationTime: db.NewVarCharTime(time.Now()),368SpendingLimit: 500,369BillingStrategy: db.CostCenter_Stripe,370})[0]371372_, err := mnr.ResetUsage(context.Background(), cc.ID)373require.Error(t, err)374})375376t.Run("resets for teams", func(t *testing.T) {377conn := dbtest.ConnectForTests(t)378mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{379ForTeams: 0,380ForUsers: 500,381})382oldCC := dbtest.CreateCostCenters(t, conn, db.CostCenter{383ID: db.NewTeamAttributionID(uuid.New().String()),384CreationTime: db.NewVarCharTime(time.Now()),385SpendingLimit: 10,386BillingStrategy: db.CostCenter_Other,387NextBillingTime: db.NewVarCharTime(ts),388BillingCycleStart: db.NewVarCharTime(ts.AddDate(0, -1, 0)),389})[0]390newCC, err := mnr.ResetUsage(context.Background(), oldCC.ID)391require.NoError(t, err)392require.Equal(t, oldCC.ID, newCC.ID)393require.EqualValues(t, 10, newCC.SpendingLimit)394require.Equal(t, db.CostCenter_Other, newCC.BillingStrategy)395require.Equal(t, db.NewVarCharTime(ts.AddDate(0, 1, 0)).Time(), newCC.NextBillingTime.Time())396397})398399t.Run("transitioning from stripe back to other restores previous limit", func(t *testing.T) {400conn := dbtest.ConnectForTests(t)401mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{402ForTeams: 0,403ForUsers: 500,404})405var originalSpendingLimit int32 = 10406cc := dbtest.CreateCostCenters(t, conn, db.CostCenter{407ID: db.NewTeamAttributionID(uuid.New().String()),408CreationTime: db.NewVarCharTime(time.Now()),409SpendingLimit: originalSpendingLimit,410BillingStrategy: db.CostCenter_Other,411NextBillingTime: db.NewVarCharTime(ts),412BillingCycleStart: db.NewVarCharTime(ts.AddDate(0, -1, 0)),413})[0]414cc.BillingStrategy = db.CostCenter_Stripe415_, err := mnr.UpdateCostCenter(context.Background(), cc)416require.NoError(t, err)417// change spending limit418cc.SpendingLimit = int32(20)419_, err = mnr.UpdateCostCenter(context.Background(), cc)420require.NoError(t, err)421422cc, err = mnr.GetOrCreateCostCenter(context.Background(), cc.ID)423require.NoError(t, err)424require.Equal(t, int32(20), cc.SpendingLimit)425426// change to other strategy again and verify the original limit is restored427cc.BillingStrategy = db.CostCenter_Other428cc, err = mnr.UpdateCostCenter(context.Background(), cc)429require.NoError(t, err)430require.Equal(t, originalSpendingLimit, cc.SpendingLimit)431})432433t.Run("users get free credits only once for the first org", func(t *testing.T) {434conn := dbtest.ConnectForTests(t)435limitForUsers := int32(500)436limitForTeams := int32(0)437mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{438ForTeams: limitForTeams,439ForUsers: limitForUsers,440})441createMembership := func(t *testing.T, anOrgID string, aUserID string) {442memberShipID := uuid.New().String()443db := conn.Exec(`INSERT INTO d_b_team_membership (id, teamid, userid, role, creationtime) VALUES (?, ?, ?, ?, ?)`,444memberShipID,445anOrgID,446aUserID,447"owner",448db.TimeToISO8601(time.Now()))449require.NoError(t, db.Error)450t.Cleanup(func() {451conn.Exec(`DELETE FROM d_b_team_membership WHERE id = ?`, memberShipID)452})453}454455createIdentity := func(t *testing.T, aUserID string, email string) {456identityID := uuid.New().String()457db := conn.Exec(`INSERT INTO d_b_identity (authProviderID, authId, authName, userid, primaryemail) VALUES (?, ?, ?, ?, ?)`,458"gitpod", identityID, "gitpod", aUserID, email)459require.NoError(t, db.Error)460t.Cleanup(func() {461conn.Exec(`DELETE FROM d_b_identity WHERE authId = ?`, identityID)462})463}464465orgID := uuid.New().String()466orgID2 := uuid.New().String()467userID := uuid.New().String()468t.Cleanup(func() {469conn.Exec(`DELETE FROM d_b_free_credits WHERE userId = ?`, userID)470})471createIdentity(t, userID, "[email protected]")472createMembership(t, orgID, userID)473createMembership(t, orgID2, userID)474cc1, err := mnr.GetOrCreateCostCenter(context.Background(), db.NewTeamAttributionID(orgID))475require.NoError(t, err)476cc2, err := mnr.GetOrCreateCostCenter(context.Background(), db.NewTeamAttributionID(orgID2))477require.NoError(t, err)478require.Equal(t, limitForUsers, cc1.SpendingLimit)479require.Equal(t, limitForTeams, cc2.SpendingLimit)480})481482t.Run("users with same email get free credits only once", func(t *testing.T) {483conn := dbtest.ConnectForTests(t)484limitForUsers := int32(500)485limitForTeams := int32(0)486mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{487ForTeams: limitForTeams,488ForUsers: limitForUsers,489})490createMembership := func(t *testing.T, anOrgID string, aUserID string) {491memberShipID := uuid.New().String()492db := conn.Exec(`INSERT INTO d_b_team_membership (id, teamid, userid, role, creationtime) VALUES (?, ?, ?, ?, ?)`,493memberShipID,494anOrgID,495aUserID,496"owner",497db.TimeToISO8601(time.Now()))498require.NoError(t, db.Error)499t.Cleanup(func() {500conn.Exec(`DELETE FROM d_b_team_membership WHERE id = ?`, memberShipID)501})502}503504createIdentity := func(t *testing.T, aUserID string, email string) {505identityID := uuid.New().String()506db := conn.Exec(`INSERT INTO d_b_identity (authProviderID, authId, authName, userid, primaryemail) VALUES (?, ?, ?, ?, ?)`,507"gitpod", identityID, "gitpod", aUserID, email)508require.NoError(t, db.Error)509t.Cleanup(func() {510conn.Exec(`DELETE FROM d_b_identity WHERE authId = ?`, identityID)511})512}513514orgID := uuid.New().String()515orgID2 := uuid.New().String()516userID := uuid.New().String()517userID2 := uuid.New().String()518t.Cleanup(func() {519conn.Exec(`DELETE FROM d_b_free_credits WHERE userId = ?`, userID)520})521createIdentity(t, userID, "[email protected]")522createIdentity(t, userID2, "[email protected]")523createMembership(t, orgID, userID)524createMembership(t, orgID2, userID2)525cc1, err := mnr.GetOrCreateCostCenter(context.Background(), db.NewTeamAttributionID(orgID))526require.NoError(t, err)527cc2, err := mnr.GetOrCreateCostCenter(context.Background(), db.NewTeamAttributionID(orgID2))528require.NoError(t, err)529require.Equal(t, limitForUsers, cc1.SpendingLimit)530require.Equal(t, limitForTeams, cc2.SpendingLimit)531})532}533534535