From c0b0f62429024cbfda4bd5db5f0f58a27c87d243 Mon Sep 17 00:00:00 2001 From: Peter Turi Date: Mon, 1 Jul 2024 11:43:06 +0200 Subject: [PATCH 1/4] feat: add regression tests --- e2e/entitlement/regression/scenario_test.go | 262 ++++++++++++++++++ internal/credit/balance_connector.go | 5 +- internal/credit/grant_connector.go | 5 +- .../postgresadapter/balance_snapshot.go | 3 +- internal/entitlement/connector.go | 3 +- .../entitlement/httpdriver/entitlement.go | 9 +- internal/entitlement/httpdriver/metered.go | 3 +- internal/entitlement/metered/balance.go | 3 +- internal/entitlement/metered/connector.go | 3 +- .../entitlement/metered/entitlement_grant.go | 3 +- .../postgresadapter/entitlement.go | 11 +- .../productcatalog/postgresadapter/feature.go | 3 +- pkg/clock/clock.go | 20 ++ pkg/clock/clock_test.go | 23 ++ pkg/framework/entutils/mixins.go | 9 +- 15 files changed, 340 insertions(+), 25 deletions(-) create mode 100644 e2e/entitlement/regression/scenario_test.go create mode 100644 pkg/clock/clock.go create mode 100644 pkg/clock/clock_test.go diff --git a/e2e/entitlement/regression/scenario_test.go b/e2e/entitlement/regression/scenario_test.go new file mode 100644 index 000000000..7b7593da1 --- /dev/null +++ b/e2e/entitlement/regression/scenario_test.go @@ -0,0 +1,262 @@ +package framework_test + +import ( + "context" + "log/slog" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/openmeterio/openmeter/internal/credit" + grantrepo "github.com/openmeterio/openmeter/internal/credit/postgresadapter" + grantdb "github.com/openmeterio/openmeter/internal/credit/postgresadapter/ent/db" + "github.com/openmeterio/openmeter/internal/entitlement" + booleanentitlement "github.com/openmeterio/openmeter/internal/entitlement/boolean" + meteredentitlement "github.com/openmeterio/openmeter/internal/entitlement/metered" + entitlementrepo "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter" + entitlementdb "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter/ent/db" + + staticentitlement "github.com/openmeterio/openmeter/internal/entitlement/static" + + "github.com/openmeterio/openmeter/internal/meter" + "github.com/openmeterio/openmeter/internal/productcatalog" + productcatalogrepo "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter" + productcatalogdb "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter/ent/db" + streamingtestutils "github.com/openmeterio/openmeter/internal/streaming/testutils" + "github.com/openmeterio/openmeter/internal/testutils" + "github.com/openmeterio/openmeter/pkg/clock" + "github.com/openmeterio/openmeter/pkg/convert" + "github.com/openmeterio/openmeter/pkg/models" + "github.com/openmeterio/openmeter/pkg/recurrence" +) + +func TestScenario(t *testing.T) { + defer clock.ResetTime() + log := slog.Default() + ctx := context.Background() + driver := testutils.InitPostgresDB(t) + + // Init product catalog + productCatalogDB := productcatalogdb.NewClient(productcatalogdb.Driver(driver)) + defer productCatalogDB.Close() + + if err := productCatalogDB.Schema.Create(context.Background()); err != nil { + t.Fatalf("failed to migrate database %s", err) + } + + featureRepo := productcatalogrepo.NewPostgresFeatureRepo(productCatalogDB, log) + + meters := []models.Meter{ + { + Namespace: "namespace-1", + ID: "meter-1", + Slug: "meter-1", + WindowSize: models.WindowSizeMinute, + Aggregation: models.MeterAggregationCount, + }, + } + + meterRepo := meter.NewInMemoryRepository(meters) + + assert := assert.New(t) + featureConnector := productcatalog.NewFeatureConnector(featureRepo, meterRepo) // TODO: meter repo is needed + + // Init grants/credit + grantDB := grantdb.NewClient(grantdb.Driver(driver)) + if err := grantDB.Schema.Create(context.Background()); err != nil { + t.Fatalf("failed to migrate database %s", err) + } + + grantRepo := grantrepo.NewPostgresGrantRepo(grantDB) + balanceSnapshotRepo := grantrepo.NewPostgresBalanceSnapshotRepo(grantDB) + + // Init entitlements + streaming := streamingtestutils.NewMockStreamingConnector(t) + + entitlementDB := entitlementdb.NewClient(entitlementdb.Driver(driver)) + defer entitlementDB.Close() + + if err := entitlementDB.Schema.Create(context.Background()); err != nil { + t.Fatalf("failed to migrate database %s", err) + } + + entitlementRepo := entitlementrepo.NewPostgresEntitlementRepo(entitlementDB) + usageResetRepo := entitlementrepo.NewPostgresUsageResetRepo(entitlementDB) + + owner := meteredentitlement.NewEntitlementGrantOwnerAdapter( + featureRepo, + entitlementRepo, + usageResetRepo, + meterRepo, + log, + ) + + balance := credit.NewBalanceConnector( + grantRepo, + balanceSnapshotRepo, + owner, + streaming, + log, + ) + + grant := credit.NewGrantConnector( + owner, + grantRepo, + balanceSnapshotRepo, + time.Minute, + ) + + meteredEntitlementConnector := meteredentitlement.NewMeteredEntitlementConnector( + streaming, + owner, + balance, + grant, + entitlementRepo) + + entitlementConnector := entitlement.NewEntitlementConnector( + entitlementRepo, + featureConnector, + meterRepo, + meteredEntitlementConnector, + staticentitlement.NewStaticEntitlementConnector(), + booleanentitlement.NewBooleanEntitlementConnector(), + ) + // Let's create a feature + + feature, err := featureConnector.CreateFeature(ctx, productcatalog.CreateFeatureInputs{ + Name: "feature-1", + Key: "feature-1", + Namespace: "namespace-1", + MeterSlug: convert.ToPointer("meter-1"), + }) + assert.NoError(err) + assert.NotNil(feature) + + // Let's create a new entitlement for the feature + + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:35:21Z")) + entitlement, err := entitlementConnector.CreateEntitlement(ctx, entitlement.CreateEntitlementInputs{ + Namespace: "namespace-1", + FeatureID: &feature.ID, + FeatureKey: &feature.Key, + SubjectKey: "subject-1", + EntitlementType: entitlement.EntitlementTypeMetered, + UsagePeriod: &entitlement.UsagePeriod{ + Interval: recurrence.RecurrencePeriodDaily, + Anchor: testutils.GetRFC3339Time(t, "2024-06-28T14:48:00Z"), + }, + }) + assert.NoError(err) + assert.NotNil(entitlement) + + // Let's grant some credit + + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:35:24Z")) + grant1, err := grant.CreateGrant(ctx, + credit.NamespacedGrantOwner{ + Namespace: "namespace-1", + ID: credit.GrantOwner(entitlement.ID), + }, + credit.CreateGrantInput{ + Amount: 10, + Priority: 5, + EffectiveAt: testutils.GetRFC3339Time(t, "2024-06-28T14:35:00Z"), + Expiration: credit.ExpirationPeriod{ + Count: 1, + Duration: credit.ExpirationPeriodDurationYear, + }, + }) + assert.NoError(err) + assert.NotNil(grant1) + + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:36:33Z")) + grant2, err := grant.CreateGrant(ctx, + credit.NamespacedGrantOwner{ + Namespace: "namespace-1", + ID: credit.GrantOwner(entitlement.ID), + }, + credit.CreateGrantInput{ + Amount: 20, + Priority: 3, + EffectiveAt: testutils.GetRFC3339Time(t, "2024-06-28T14:36:00Z"), + Expiration: credit.ExpirationPeriod{ + Count: 1, + Duration: credit.ExpirationPeriodDurationDay, + }, + ResetMaxRollover: 20, + }) + assert.NoError(err) + assert.NotNil(grant2) + + // Hack: this is in the future, but at least it won't return an error + streaming.AddSimpleEvent("meter-1", 1, testutils.GetRFC3339Time(t, "2025-06-28T14:36:00Z")) + + // Let's query the usage + currentBalance, err := meteredEntitlementConnector.GetEntitlementBalance(ctx, + models.NamespacedID{ + Namespace: "namespace-1", + ID: entitlement.ID, + }, + testutils.GetRFC3339Time(t, "2024-06-28T14:36:45Z")) + assert.NoError(err) + assert.NotNil(currentBalance) + assert.Equal(30.0, currentBalance.Balance) + + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:30:41Z")) + // Let's query the usage + currentBalance, err = meteredEntitlementConnector.GetEntitlementBalance(ctx, + models.NamespacedID{ + Namespace: "namespace-1", + ID: entitlement.ID, + }, + testutils.GetRFC3339Time(t, "2024-06-28T14:30:41Z")) + assert.NoError(err) + assert.NotNil(currentBalance) + assert.Equal(10.0, currentBalance.Balance) + + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:35:54Z")) + grant3, err := grant.CreateGrant(ctx, + credit.NamespacedGrantOwner{ + Namespace: "namespace-1", + ID: credit.GrantOwner(entitlement.ID), + }, + credit.CreateGrantInput{ + Amount: 100, + Priority: 1, + EffectiveAt: testutils.GetRFC3339Time(t, "2024-06-28T15:39:00Z"), + Expiration: credit.ExpirationPeriod{ + Count: 1, + Duration: credit.ExpirationPeriodDurationYear, + }, + }) + assert.NoError(err) + assert.NotNil(grant3) + + // There should be a snapshot created + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:37:18Z")) + reset, err := meteredEntitlementConnector.ResetEntitlementUsage(ctx, + models.NamespacedID{ + Namespace: "namespace-1", + ID: entitlement.ID, + }, + meteredentitlement.ResetEntitlementUsageParams{ + At: testutils.GetRFC3339Time(t, "2024-06-29T14:36:00Z"), + RetainAnchor: false, + }, + ) + assert.NoError(err) + assert.NotNil(reset) + + now := clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:42:41Z")) + // Let's query the usage + currentBalance, err = meteredEntitlementConnector.GetEntitlementBalance(ctx, + models.NamespacedID{ + Namespace: "namespace-1", + ID: entitlement.ID, + }, + now) + assert.NoError(err) + assert.NotNil(currentBalance) + assert.Equal(100.0, currentBalance.Balance) +} diff --git a/internal/credit/balance_connector.go b/internal/credit/balance_connector.go index a189c5716..13d59e89a 100644 --- a/internal/credit/balance_connector.go +++ b/internal/credit/balance_connector.go @@ -9,6 +9,7 @@ import ( "time" "github.com/openmeterio/openmeter/internal/streaming" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/recurrence" @@ -218,7 +219,7 @@ func (m *balanceConnector) GetBalanceHistoryOfOwner(ctx context.Context, owner N func (m *balanceConnector) ResetUsageForOwner(ctx context.Context, owner NamespacedGrantOwner, params ResetUsageForOwnerParams) (*GrantBalanceSnapshot, error) { // Cannot reset for the future - if params.At.After(time.Now()) { + if params.At.After(clock.Now()) { return nil, &models.GenericUserError{Message: fmt.Sprintf("cannot reset at %s in the future", params.At)} } @@ -230,7 +231,7 @@ func (m *balanceConnector) ResetUsageForOwner(ctx context.Context, owner Namespa at := params.At.Truncate(ownerMeter.WindowSize.Duration()) // check if reset is possible (after last reset) - periodStart, err := m.ownerConnector.GetUsagePeriodStartAt(ctx, owner, time.Now()) + periodStart, err := m.ownerConnector.GetUsagePeriodStartAt(ctx, owner, clock.Now()) if err != nil { if _, ok := err.(*OwnerNotFoundError); ok { return nil, err diff --git a/internal/credit/grant_connector.go b/internal/credit/grant_connector.go index eb9af12ba..06b2fe69c 100644 --- a/internal/credit/grant_connector.go +++ b/internal/credit/grant_connector.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/recurrence" @@ -109,7 +110,7 @@ func (m *grantConnector) CreateGrant(ctx context.Context, owner NamespacedGrantO if input.Recurrence != nil { input.Recurrence.Anchor = input.Recurrence.Anchor.Truncate(granularity) } - periodStart, err := m.ownerConnector.GetUsagePeriodStartAt(ctx, owner, time.Now()) + periodStart, err := m.ownerConnector.GetUsagePeriodStartAt(ctx, owner, clock.Now()) if err != nil { return nil, err } @@ -176,7 +177,7 @@ func (m *grantConnector) VoidGrant(ctx context.Context, grantID models.Namespace if err != nil { return nil, err } - now := time.Now() + now := clock.Now() err = m.grantRepo.WithTx(ctx, tx).VoidGrant(ctx, grantID, now) if err != nil { return nil, err diff --git a/internal/credit/postgresadapter/balance_snapshot.go b/internal/credit/postgresadapter/balance_snapshot.go index e8c4545f8..24cbb3b91 100644 --- a/internal/credit/postgresadapter/balance_snapshot.go +++ b/internal/credit/postgresadapter/balance_snapshot.go @@ -9,6 +9,7 @@ import ( "github.com/openmeterio/openmeter/internal/credit" "github.com/openmeterio/openmeter/internal/credit/postgresadapter/ent/db" db_balancesnapshot "github.com/openmeterio/openmeter/internal/credit/postgresadapter/ent/db/balancesnapshot" + "github.com/openmeterio/openmeter/pkg/clock" ) // naive implementation of the BalanceSnapshotConnector @@ -25,7 +26,7 @@ func NewPostgresBalanceSnapshotRepo(db *db.Client) credit.BalanceSnapshotConnect func (b *balanceSnapshotAdapter) InvalidateAfter(ctx context.Context, owner credit.NamespacedGrantOwner, at time.Time) error { return b.db.BalanceSnapshot.Update(). Where(db_balancesnapshot.OwnerID(owner.ID), db_balancesnapshot.Namespace(owner.Namespace), db_balancesnapshot.AtGT(at)). - SetDeletedAt(time.Now()). + SetDeletedAt(clock.Now()). Exec(ctx) } diff --git a/internal/entitlement/connector.go b/internal/entitlement/connector.go index 34a1d8bc9..d83f261ad 100644 --- a/internal/entitlement/connector.go +++ b/internal/entitlement/connector.go @@ -7,6 +7,7 @@ import ( "github.com/openmeterio/openmeter/internal/meter" "github.com/openmeterio/openmeter/internal/productcatalog" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/models" ) @@ -81,7 +82,7 @@ func (c *entitlementConnector) CreateEntitlement(ctx context.Context, input Crea if err != nil || feature == nil { return nil, &productcatalog.FeatureNotFoundError{ID: *idOrFeatureKey} } - if feature.ArchivedAt != nil && feature.ArchivedAt.Before(time.Now()) { + if feature.ArchivedAt != nil && feature.ArchivedAt.Before(clock.Now()) { return nil, &models.GenericUserError{Message: "Feature is archived"} } diff --git a/internal/entitlement/httpdriver/entitlement.go b/internal/entitlement/httpdriver/entitlement.go index e63be7cc5..cc4ea1570 100644 --- a/internal/entitlement/httpdriver/entitlement.go +++ b/internal/entitlement/httpdriver/entitlement.go @@ -12,6 +12,7 @@ import ( meteredentitlement "github.com/openmeterio/openmeter/internal/entitlement/metered" staticentitlement "github.com/openmeterio/openmeter/internal/entitlement/static" "github.com/openmeterio/openmeter/internal/namespace/namespacedriver" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" "github.com/openmeterio/openmeter/pkg/defaultx" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" @@ -83,7 +84,7 @@ func (h *entitlementHandler) CreateEntitlement() CreateEntitlementHandler { IsSoftLimit: v.IsSoftLimit, IssueAfterReset: v.IssueAfterReset, UsagePeriod: &entitlement.UsagePeriod{ - Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, time.Now()), // TODO: shouldn't we truncate this? + Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, clock.Now()), // TODO: shouldn't we truncate this? Interval: recurrence.RecurrenceInterval(v.UsagePeriod.Interval), }, } @@ -101,7 +102,7 @@ func (h *entitlementHandler) CreateEntitlement() CreateEntitlementHandler { } if v.UsagePeriod != nil { request.UsagePeriod = &entitlement.UsagePeriod{ - Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, time.Now()), // TODO: shouldn't we truncate this? + Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, clock.Now()), // TODO: shouldn't we truncate this? Interval: recurrence.RecurrenceInterval(v.UsagePeriod.Interval), } } @@ -118,7 +119,7 @@ func (h *entitlementHandler) CreateEntitlement() CreateEntitlementHandler { } if v.UsagePeriod != nil { request.UsagePeriod = &entitlement.UsagePeriod{ - Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, time.Now()), // TODO: shouldn't we truncate this? + Anchor: defaultx.WithDefault(v.UsagePeriod.Anchor, clock.Now()), // TODO: shouldn't we truncate this? Interval: recurrence.RecurrenceInterval(v.UsagePeriod.Interval), } } @@ -173,7 +174,7 @@ func (h *entitlementHandler) GetEntitlementValue() GetEntitlementValueHandler { SubjectKey: params.SubjectKey, EntitlementIdOrFeatureKey: params.EntitlementIdOrFeatureKey, Namespace: ns, - At: defaultx.WithDefault(params.Params.Time, time.Now()), + At: defaultx.WithDefault(params.Params.Time, clock.Now()), }, nil }, func(ctx context.Context, request GetEntitlementValueHandlerRequest) (api.EntitlementValue, error) { diff --git a/internal/entitlement/httpdriver/metered.go b/internal/entitlement/httpdriver/metered.go index 83c455097..218362304 100644 --- a/internal/entitlement/httpdriver/metered.go +++ b/internal/entitlement/httpdriver/metered.go @@ -12,6 +12,7 @@ import ( "github.com/openmeterio/openmeter/internal/entitlement" meteredentitlement "github.com/openmeterio/openmeter/internal/entitlement/metered" "github.com/openmeterio/openmeter/internal/namespace/namespacedriver" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" "github.com/openmeterio/openmeter/pkg/defaultx" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" @@ -216,7 +217,7 @@ func (h *meteredEntitlementHandler) ResetEntitlementUsage() ResetEntitlementUsag EntitlementID: params.EntitlementID, Namespace: ns, SubjectID: params.SubjectKey, - At: defaultx.WithDefault(body.EffectiveAt, time.Now()), + At: defaultx.WithDefault(body.EffectiveAt, clock.Now()), RetainAnchor: defaultx.WithDefault(body.RetainAnchor, false), }, nil }, diff --git a/internal/entitlement/metered/balance.go b/internal/entitlement/metered/balance.go index f86c5df70..d145d9da7 100644 --- a/internal/entitlement/metered/balance.go +++ b/internal/entitlement/metered/balance.go @@ -8,6 +8,7 @@ import ( "github.com/openmeterio/openmeter/internal/credit" "github.com/openmeterio/openmeter/internal/entitlement" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/slicesx" @@ -115,7 +116,7 @@ func (e *connector) GetEntitlementBalanceHistory(ctx context.Context, entitlemen } if params.To == nil { - params.To = convert.ToPointer(time.Now()) + params.To = convert.ToPointer(clock.Now()) } // query period cannot be before start of measuring usage diff --git a/internal/entitlement/metered/connector.go b/internal/entitlement/metered/connector.go index a0c4d5c15..0f1009023 100644 --- a/internal/entitlement/metered/connector.go +++ b/internal/entitlement/metered/connector.go @@ -9,6 +9,7 @@ import ( "github.com/openmeterio/openmeter/internal/entitlement" "github.com/openmeterio/openmeter/internal/productcatalog" "github.com/openmeterio/openmeter/internal/streaming" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" "github.com/openmeterio/openmeter/pkg/defaultx" "github.com/openmeterio/openmeter/pkg/models" @@ -117,7 +118,7 @@ func (c *connector) BeforeCreate(model entitlement.CreateEntitlementInputs, feat return nil, &entitlement.InvalidFeatureError{FeatureID: feature.ID, Message: "Feature has no meter"} } - model.MeasureUsageFrom = convert.ToPointer(defaultx.WithDefault(model.MeasureUsageFrom, time.Now().Truncate(c.granularity))) + model.MeasureUsageFrom = convert.ToPointer(defaultx.WithDefault(model.MeasureUsageFrom, clock.Now().Truncate(c.granularity))) model.IsSoftLimit = convert.ToPointer(defaultx.WithDefault(model.IsSoftLimit, false)) model.IssueAfterReset = convert.ToPointer(defaultx.WithDefault(model.IssueAfterReset, 0.0)) diff --git a/internal/entitlement/metered/entitlement_grant.go b/internal/entitlement/metered/entitlement_grant.go index 2f4e949f7..59dc7802d 100644 --- a/internal/entitlement/metered/entitlement_grant.go +++ b/internal/entitlement/metered/entitlement_grant.go @@ -6,6 +6,7 @@ import ( "github.com/openmeterio/openmeter/internal/credit" "github.com/openmeterio/openmeter/internal/entitlement" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" "github.com/openmeterio/openmeter/pkg/models" ) @@ -86,7 +87,7 @@ type EntitlementGrant struct { func GrantFromCreditGrant(grant credit.Grant) (*EntitlementGrant, error) { g := &EntitlementGrant{} if grant.Recurrence != nil { - next, err := grant.Recurrence.NextAfter(time.Now()) + next, err := grant.Recurrence.NextAfter(clock.Now()) if err != nil { return nil, err } diff --git a/internal/entitlement/postgresadapter/entitlement.go b/internal/entitlement/postgresadapter/entitlement.go index 8618ab36c..e9b04ca4c 100644 --- a/internal/entitlement/postgresadapter/entitlement.go +++ b/internal/entitlement/postgresadapter/entitlement.go @@ -12,6 +12,7 @@ import ( "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter/ent/db" db_entitlement "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter/ent/db/entitlement" "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter/ent/db/usagereset" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/recurrence" @@ -32,7 +33,7 @@ func (a *entitlementDBAdapter) GetEntitlement(ctx context.Context, entitlementID Where( db_entitlement.ID(entitlementID.ID), db_entitlement.Namespace(entitlementID.Namespace), - db_entitlement.Or(db_entitlement.DeletedAtGT(time.Now()), db_entitlement.DeletedAtIsNil()), + db_entitlement.Or(db_entitlement.DeletedAtGT(clock.Now()), db_entitlement.DeletedAtIsNil()), ). First(ctx) @@ -49,7 +50,7 @@ func (a *entitlementDBAdapter) GetEntitlement(ctx context.Context, entitlementID func (a *entitlementDBAdapter) GetEntitlementOfSubject(ctx context.Context, namespace string, subjectKey string, idOrFeatureKey string) (*entitlement.Entitlement, error) { res, err := withLatestUsageReset(a.db.Entitlement.Query()). Where( - db_entitlement.Or(db_entitlement.DeletedAtGT(time.Now()), db_entitlement.DeletedAtIsNil()), + db_entitlement.Or(db_entitlement.DeletedAtGT(clock.Now()), db_entitlement.DeletedAtIsNil()), db_entitlement.SubjectKey(string(subjectKey)), db_entitlement.Namespace(namespace), db_entitlement.Or(db_entitlement.ID(idOrFeatureKey), db_entitlement.FeatureKey(idOrFeatureKey)), @@ -115,7 +116,7 @@ func (a *entitlementDBAdapter) CreateEntitlement(ctx context.Context, entitlemen func (a *entitlementDBAdapter) DeleteEntitlement(ctx context.Context, entitlementID models.NamespacedID) error { affectedCount, err := a.db.Entitlement.Update(). Where(db_entitlement.ID(entitlementID.ID), db_entitlement.Namespace(entitlementID.Namespace)). - SetDeletedAt(time.Now()). + SetDeletedAt(clock.Now()). Save(ctx) if err != nil { return err @@ -129,7 +130,7 @@ func (a *entitlementDBAdapter) DeleteEntitlement(ctx context.Context, entitlemen func (a *entitlementDBAdapter) GetEntitlementsOfSubject(ctx context.Context, namespace string, subjectKey models.SubjectKey) ([]entitlement.Entitlement, error) { res, err := withLatestUsageReset(a.db.Entitlement.Query()). Where( - db_entitlement.Or(db_entitlement.DeletedAtGT(time.Now()), db_entitlement.DeletedAtIsNil()), + db_entitlement.Or(db_entitlement.DeletedAtGT(clock.Now()), db_entitlement.DeletedAtIsNil()), db_entitlement.SubjectKey(string(subjectKey)), db_entitlement.Namespace(namespace), ). @@ -161,7 +162,7 @@ func (a *entitlementDBAdapter) ListEntitlements(ctx context.Context, params enti } if !params.IncludeDeleted { - query = query.Where(db_entitlement.Or(db_entitlement.DeletedAtGT(time.Now()), db_entitlement.DeletedAtIsNil())) + query = query.Where(db_entitlement.Or(db_entitlement.DeletedAtGT(clock.Now()), db_entitlement.DeletedAtIsNil())) } if params.Limit > 0 { diff --git a/internal/productcatalog/postgresadapter/feature.go b/internal/productcatalog/postgresadapter/feature.go index 7120e1706..62d06ec77 100644 --- a/internal/productcatalog/postgresadapter/feature.go +++ b/internal/productcatalog/postgresadapter/feature.go @@ -9,6 +9,7 @@ import ( "github.com/openmeterio/openmeter/internal/productcatalog" "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter/ent/db" db_feature "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter/ent/db/feature" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/models" ) @@ -76,7 +77,7 @@ func (c *featureDBAdapter) ArchiveFeature(ctx context.Context, featureID models. } err = c.db.Feature.Update(). - SetArchivedAt(time.Now()). + SetArchivedAt(clock.Now()). Where(db_feature.ID(featureID.ID)). Where(db_feature.Namespace(featureID.Namespace)). Exec(ctx) diff --git a/pkg/clock/clock.go b/pkg/clock/clock.go new file mode 100644 index 000000000..fe507e5a9 --- /dev/null +++ b/pkg/clock/clock.go @@ -0,0 +1,20 @@ +package clock + +import "time" + +var ( + drift time.Duration +) + +func Now() time.Time { + return time.Now().Add(-drift) +} + +func SetTime(t time.Time) time.Time { + drift = time.Now().Sub(t) + return Now() +} + +func ResetTime() { + drift = 0 +} diff --git a/pkg/clock/clock_test.go b/pkg/clock/clock_test.go new file mode 100644 index 000000000..7728ae949 --- /dev/null +++ b/pkg/clock/clock_test.go @@ -0,0 +1,23 @@ +package clock_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/openmeterio/openmeter/internal/testutils" + "github.com/openmeterio/openmeter/pkg/clock" +) + +func TestClock(t *testing.T) { + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:39:00Z")) + defer clock.ResetTime() + + now := clock.Now() + diff := now.Sub(testutils.GetRFC3339Time(t, "2024-06-30T15:39:00Z")) + if diff < 0 { + diff = -diff + } + assert.True(t, diff < time.Second) +} diff --git a/pkg/framework/entutils/mixins.go b/pkg/framework/entutils/mixins.go index 307bd0109..df3816c2b 100644 --- a/pkg/framework/entutils/mixins.go +++ b/pkg/framework/entutils/mixins.go @@ -1,14 +1,13 @@ package entutils import ( - "time" - "entgo.io/ent" "entgo.io/ent/dialect" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" "entgo.io/ent/schema/mixin" "github.com/oklog/ulid/v2" + "github.com/openmeterio/openmeter/pkg/clock" ) // IDMixin adds the ID field to the schema @@ -76,11 +75,11 @@ type TimeMixin struct { func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time("created_at"). - Default(time.Now). + Default(clock.Now). Immutable(), field.Time("updated_at"). - Default(time.Now). - UpdateDefault(time.Now), + Default(clock.Now). + UpdateDefault(clock.Now), field.Time("deleted_at"). Optional(). Nillable(), From 172ef7a66ca08209f9f2cd98739baf3a514128f0 Mon Sep 17 00:00:00 2001 From: Alex Goth Date: Mon, 1 Jul 2024 12:35:05 +0200 Subject: [PATCH 2/4] fix: check if grants expire at reset time --- e2e/entitlement/regression/scenario_test.go | 2 +- internal/credit/balance_connector.go | 12 ++- internal/credit/grant.go | 3 +- internal/entitlement/metered/balance_test.go | 82 +++++++++++--------- 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/e2e/entitlement/regression/scenario_test.go b/e2e/entitlement/regression/scenario_test.go index 7b7593da1..e3c81fbbe 100644 --- a/e2e/entitlement/regression/scenario_test.go +++ b/e2e/entitlement/regression/scenario_test.go @@ -258,5 +258,5 @@ func TestScenario(t *testing.T) { now) assert.NoError(err) assert.NotNil(currentBalance) - assert.Equal(100.0, currentBalance.Balance) + assert.Equal(0.0, currentBalance.Balance) } diff --git a/internal/credit/balance_connector.go b/internal/credit/balance_connector.go index 13d59e89a..de4a2088c 100644 --- a/internal/credit/balance_connector.go +++ b/internal/credit/balance_connector.go @@ -287,15 +287,21 @@ func (m *balanceConnector) ResetUsageForOwner(ctx context.Context, owner Namespa grantMap[grant.ID] = grant } - // We have to roll over the grants and save the starting balance for the next period - // at the reset time. - startingBalance := endingBalance.Copy() + // We have to roll over the grants and save the starting balance for the next period at the reset time. + // Engine treates the output balance as a period end (exclusive), but we need to treat it as a period start (inclusive). + startingBalance := GrantBalanceMap{} for grantID, grantBalance := range endingBalance { grant, ok := grantMap[grantID] // inconsistency check, shouldn't happen if !ok { return nil, fmt.Errorf("attempting to roll over unknown grant %s", grantID) } + + // grants might become inactive at the reset time, in which case they're irrelevant for the next period + if !grant.ActiveAt(at) { + continue + } + startingBalance.Set(grantID, grant.RolloverBalance(grantBalance)) } diff --git a/internal/credit/grant.go b/internal/credit/grant.go index abebed20c..df90e14e7 100644 --- a/internal/credit/grant.go +++ b/internal/credit/grant.go @@ -52,7 +52,8 @@ type Grant struct { // Expiration The expiration configuration. Expiration ExpirationPeriod `json:"expiration"` - // ExpiresAt contains the exact expiration date calculated from effectiveAt and Expiration for rendering + // ExpiresAt contains the exact expiration date calculated from effectiveAt and Expiration for rendering. + // ExpiresAt is exclusive, meaning that the grant is no longer active after this time, but it is still active at the time. ExpiresAt time.Time `json:"expiresAt"` Metadata map[string]string `json:"metadata,omitempty"` diff --git a/internal/entitlement/metered/balance_test.go b/internal/entitlement/metered/balance_test.go index 7bd63da0b..376aa9115 100644 --- a/internal/entitlement/metered/balance_test.go +++ b/internal/entitlement/metered/balance_test.go @@ -993,43 +993,6 @@ func TestResetEntitlementUsage(t *testing.T) { assert.Equal(t, resetTime.Format(time.RFC3339), ent.LastReset.Format(time.RFC3339)) }, }, - { - name: "Should return proper last reset time after reset", - run: func(t *testing.T, connector meteredentitlement.Connector, deps *testDependencies) { - ctx := context.Background() - startTime := testutils.GetRFC3339Time(t, "2024-03-01T00:00:00Z") - - // create featute in db - feature, err := deps.featureDB.CreateFeature(ctx, exampleFeature) - assert.NoError(t, err) - - // create entitlement in db - inp := getEntitlement(t, feature) - inp.MeasureUsageFrom = &startTime - ent, err := deps.entitlementDB.CreateEntitlement(ctx, inp) - assert.NoError(t, err) - - ent, err = deps.entitlementDB.GetEntitlement(ctx, models.NamespacedID{Namespace: namespace, ID: ent.ID}) - assert.NoError(t, err) - assert.Equal(t, startTime.Format(time.RFC3339), ent.LastReset.Format(time.RFC3339)) - - deps.streaming.AddSimpleEvent(meterSlug, 600, startTime.Add(time.Minute)) - - // resetTime before snapshot - resetTime := startTime.Add(time.Hour * 5) - _, err = connector.ResetEntitlementUsage(ctx, - models.NamespacedID{Namespace: namespace, ID: ent.ID}, - meteredentitlement.ResetEntitlementUsageParams{ - At: resetTime, - }) - assert.NoError(t, err) - - // validate that lastReset time is properly set - ent, err = deps.entitlementDB.GetEntitlement(ctx, models.NamespacedID{Namespace: namespace, ID: ent.ID}) - assert.NoError(t, err) - assert.Equal(t, resetTime.Format(time.RFC3339), ent.LastReset.Format(time.RFC3339)) - }, - }, { name: "Should calculate balance for grants taking effect after last saved snapshot", run: func(t *testing.T, connector meteredentitlement.Connector, deps *testDependencies) { @@ -1212,6 +1175,51 @@ func TestResetEntitlementUsage(t *testing.T) { assert.Equal(t, g2.Amount, balanceAfterReset.Balance) // 1000 - 0 = 1000 }, }, + { + name: "Should properly handle grants expiring the same time as reset", + run: func(t *testing.T, connector meteredentitlement.Connector, deps *testDependencies) { + ctx := context.Background() + startTime := testutils.GetRFC3339Time(t, "2024-03-01T00:00:00Z") + resetTime := startTime.AddDate(0, 0, 3) + + // create featute in db + feature, err := deps.featureDB.CreateFeature(ctx, exampleFeature) + assert.NoError(t, err) + + // add 0 usage so meter is found in mock + deps.streaming.AddSimpleEvent(meterSlug, 0, startTime) + + // create entitlement in db + inp := getEntitlement(t, feature) + inp.MeasureUsageFrom = &startTime + ent, err := deps.entitlementDB.CreateEntitlement(ctx, inp) + assert.NoError(t, err) + + // issue grants + _, err = deps.grantDB.CreateGrant(ctx, credit.GrantRepoCreateGrantInput{ + OwnerID: credit.GrantOwner(ent.ID), + Namespace: namespace, + Amount: 1000, + Priority: 1, + EffectiveAt: startTime.Add(time.Hour * 2), + ExpiresAt: resetTime, + ResetMaxRollover: 1000, // full amount can be rolled over + }) + assert.NoError(t, err) + + // do a reset + balanceAfterReset, err := connector.ResetEntitlementUsage(ctx, + models.NamespacedID{Namespace: namespace, ID: ent.ID}, + meteredentitlement.ResetEntitlementUsageParams{ + At: resetTime, + }) + + // assert balance after reset is 0 for grant + assert.NoError(t, err) + assert.Equal(t, 0.0, balanceAfterReset.UsageInPeriod) // 0 usage right after reset + assert.Equal(t, 0.0, balanceAfterReset.Balance) // Grant expires at reset time so we should see no balance + }, + }, { name: "Should reseting without anchor update keeps the next reset time intact", run: func(t *testing.T, connector meteredentitlement.Connector, deps *testDependencies) { From 74eca75c322e808c183c4e12de19c29708ea4c42 Mon Sep 17 00:00:00 2001 From: Peter Turi Date: Mon, 1 Jul 2024 13:17:59 +0200 Subject: [PATCH 3/4] fix: rework regression tests --- pkg/clock/clock.go | 2 +- pkg/framework/entutils/mixins.go | 1 + test/entitlement/regression/framework_test.go | 169 ++++++++++++++++++ .../entitlement/regression/scenario_test.go | 126 ++----------- 4 files changed, 185 insertions(+), 113 deletions(-) create mode 100644 test/entitlement/regression/framework_test.go rename {e2e => test}/entitlement/regression/scenario_test.go (51%) diff --git a/pkg/clock/clock.go b/pkg/clock/clock.go index fe507e5a9..325504c0c 100644 --- a/pkg/clock/clock.go +++ b/pkg/clock/clock.go @@ -11,7 +11,7 @@ func Now() time.Time { } func SetTime(t time.Time) time.Time { - drift = time.Now().Sub(t) + drift = time.Since(t) return Now() } diff --git a/pkg/framework/entutils/mixins.go b/pkg/framework/entutils/mixins.go index df3816c2b..eb243b43a 100644 --- a/pkg/framework/entutils/mixins.go +++ b/pkg/framework/entutils/mixins.go @@ -7,6 +7,7 @@ import ( "entgo.io/ent/schema/index" "entgo.io/ent/schema/mixin" "github.com/oklog/ulid/v2" + "github.com/openmeterio/openmeter/pkg/clock" ) diff --git a/test/entitlement/regression/framework_test.go b/test/entitlement/regression/framework_test.go new file mode 100644 index 000000000..e88cec24b --- /dev/null +++ b/test/entitlement/regression/framework_test.go @@ -0,0 +1,169 @@ +package framework_test + +import ( + "context" + "log/slog" + "testing" + "time" + + "github.com/openmeterio/openmeter/internal/credit" + grantrepo "github.com/openmeterio/openmeter/internal/credit/postgresadapter" + grantdb "github.com/openmeterio/openmeter/internal/credit/postgresadapter/ent/db" + "github.com/openmeterio/openmeter/internal/entitlement" + booleanentitlement "github.com/openmeterio/openmeter/internal/entitlement/boolean" + meteredentitlement "github.com/openmeterio/openmeter/internal/entitlement/metered" + entitlementrepo "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter" + entitlementdb "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter/ent/db" + staticentitlement "github.com/openmeterio/openmeter/internal/entitlement/static" + "github.com/openmeterio/openmeter/internal/meter" + "github.com/openmeterio/openmeter/internal/productcatalog" + productcatalogrepo "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter" + productcatalogdb "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter/ent/db" + streamingtestutils "github.com/openmeterio/openmeter/internal/streaming/testutils" + "github.com/openmeterio/openmeter/internal/testutils" + "github.com/openmeterio/openmeter/pkg/models" +) + +type Dependencies struct { + GrantRepo credit.GrantRepo + GrantDB *grantdb.Client + BalanceSnapshotConnector credit.BalanceSnapshotConnector + GrantConnector credit.GrantConnector + + EntitlementRepo entitlement.EntitlementRepo + EntitlementDB entitlementdb.Client + + EntitlementConnector entitlement.Connector + StaticEntitlementConnector staticentitlement.Connector + BooleanEntitlementConnector booleanentitlement.Connector + MeteredEntitlementConnector meteredentitlement.Connector + + Streaming *streamingtestutils.MockStreamingConnector + + FeatureRepo productcatalog.FeatureRepo + ProductCatalogDB *productcatalogdb.Client + FeatureConnector productcatalog.FeatureConnector + + Log *slog.Logger +} + +func (d *Dependencies) Close() { + d.GrantDB.Close() + d.EntitlementDB.Close() + d.ProductCatalogDB.Close() +} + +func setupDependencies(t *testing.T) Dependencies { + log := slog.Default() + ctx := context.Background() + driver := testutils.InitPostgresDB(t) + + // Init product catalog + productCatalogDB := productcatalogdb.NewClient(productcatalogdb.Driver(driver)) + + if err := productCatalogDB.Schema.Create(ctx); err != nil { + t.Fatalf("failed to migrate database %s", err) + } + + featureRepo := productcatalogrepo.NewPostgresFeatureRepo(productCatalogDB, log) + + meters := []models.Meter{ + { + Namespace: "namespace-1", + ID: "meter-1", + Slug: "meter-1", + WindowSize: models.WindowSizeMinute, + Aggregation: models.MeterAggregationCount, + }, + } + + meterRepo := meter.NewInMemoryRepository(meters) + + featureConnector := productcatalog.NewFeatureConnector(featureRepo, meterRepo) // TODO: meter repo is needed + + // Init grants/credit + grantDB := grantdb.NewClient(grantdb.Driver(driver)) + if err := grantDB.Schema.Create(context.Background()); err != nil { + t.Fatalf("failed to migrate database %s", err) + } + + grantRepo := grantrepo.NewPostgresGrantRepo(grantDB) + balanceSnapshotRepo := grantrepo.NewPostgresBalanceSnapshotRepo(grantDB) + + // Init entitlements + streaming := streamingtestutils.NewMockStreamingConnector(t) + + entitlementDB := entitlementdb.NewClient(entitlementdb.Driver(driver)) + + if err := entitlementDB.Schema.Create(context.Background()); err != nil { + t.Fatalf("failed to migrate database %s", err) + } + + entitlementRepo := entitlementrepo.NewPostgresEntitlementRepo(entitlementDB) + usageResetRepo := entitlementrepo.NewPostgresUsageResetRepo(entitlementDB) + + owner := meteredentitlement.NewEntitlementGrantOwnerAdapter( + featureRepo, + entitlementRepo, + usageResetRepo, + meterRepo, + log, + ) + + balance := credit.NewBalanceConnector( + grantRepo, + balanceSnapshotRepo, + owner, + streaming, + log, + ) + + grant := credit.NewGrantConnector( + owner, + grantRepo, + balanceSnapshotRepo, + time.Minute, + ) + + meteredEntitlementConnector := meteredentitlement.NewMeteredEntitlementConnector( + streaming, + owner, + balance, + grant, + entitlementRepo) + + staticEntitlementConnector := staticentitlement.NewStaticEntitlementConnector() + booleanEntitlementConnector := booleanentitlement.NewBooleanEntitlementConnector() + + entitlementConnector := entitlement.NewEntitlementConnector( + entitlementRepo, + featureConnector, + meterRepo, + meteredEntitlementConnector, + staticEntitlementConnector, + booleanEntitlementConnector, + ) + + return Dependencies{ + GrantRepo: grantRepo, + GrantDB: grantDB, + GrantConnector: grant, + + EntitlementRepo: entitlementRepo, + EntitlementDB: *entitlementDB, + + EntitlementConnector: entitlementConnector, + StaticEntitlementConnector: staticEntitlementConnector, + BooleanEntitlementConnector: booleanEntitlementConnector, + MeteredEntitlementConnector: meteredEntitlementConnector, + + Streaming: streaming, + + FeatureRepo: featureRepo, + ProductCatalogDB: productCatalogDB, + FeatureConnector: featureConnector, + + Log: log, + } + +} diff --git a/e2e/entitlement/regression/scenario_test.go b/test/entitlement/regression/scenario_test.go similarity index 51% rename from e2e/entitlement/regression/scenario_test.go rename to test/entitlement/regression/scenario_test.go index e3c81fbbe..e802f7542 100644 --- a/e2e/entitlement/regression/scenario_test.go +++ b/test/entitlement/regression/scenario_test.go @@ -2,28 +2,14 @@ package framework_test import ( "context" - "log/slog" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/openmeterio/openmeter/internal/credit" - grantrepo "github.com/openmeterio/openmeter/internal/credit/postgresadapter" - grantdb "github.com/openmeterio/openmeter/internal/credit/postgresadapter/ent/db" "github.com/openmeterio/openmeter/internal/entitlement" - booleanentitlement "github.com/openmeterio/openmeter/internal/entitlement/boolean" meteredentitlement "github.com/openmeterio/openmeter/internal/entitlement/metered" - entitlementrepo "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter" - entitlementdb "github.com/openmeterio/openmeter/internal/entitlement/postgresadapter/ent/db" - - staticentitlement "github.com/openmeterio/openmeter/internal/entitlement/static" - - "github.com/openmeterio/openmeter/internal/meter" "github.com/openmeterio/openmeter/internal/productcatalog" - productcatalogrepo "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter" - productcatalogdb "github.com/openmeterio/openmeter/internal/productcatalog/postgresadapter/ent/db" - streamingtestutils "github.com/openmeterio/openmeter/internal/streaming/testutils" "github.com/openmeterio/openmeter/internal/testutils" "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/convert" @@ -31,100 +17,16 @@ import ( "github.com/openmeterio/openmeter/pkg/recurrence" ) -func TestScenario(t *testing.T) { +func TestGrantAndResetAtTheSameTime(t *testing.T) { defer clock.ResetTime() - log := slog.Default() + deps := setupDependencies(t) + defer deps.Close() ctx := context.Background() - driver := testutils.InitPostgresDB(t) - - // Init product catalog - productCatalogDB := productcatalogdb.NewClient(productcatalogdb.Driver(driver)) - defer productCatalogDB.Close() - - if err := productCatalogDB.Schema.Create(context.Background()); err != nil { - t.Fatalf("failed to migrate database %s", err) - } - - featureRepo := productcatalogrepo.NewPostgresFeatureRepo(productCatalogDB, log) - - meters := []models.Meter{ - { - Namespace: "namespace-1", - ID: "meter-1", - Slug: "meter-1", - WindowSize: models.WindowSizeMinute, - Aggregation: models.MeterAggregationCount, - }, - } - - meterRepo := meter.NewInMemoryRepository(meters) - assert := assert.New(t) - featureConnector := productcatalog.NewFeatureConnector(featureRepo, meterRepo) // TODO: meter repo is needed - - // Init grants/credit - grantDB := grantdb.NewClient(grantdb.Driver(driver)) - if err := grantDB.Schema.Create(context.Background()); err != nil { - t.Fatalf("failed to migrate database %s", err) - } - - grantRepo := grantrepo.NewPostgresGrantRepo(grantDB) - balanceSnapshotRepo := grantrepo.NewPostgresBalanceSnapshotRepo(grantDB) - - // Init entitlements - streaming := streamingtestutils.NewMockStreamingConnector(t) - entitlementDB := entitlementdb.NewClient(entitlementdb.Driver(driver)) - defer entitlementDB.Close() - - if err := entitlementDB.Schema.Create(context.Background()); err != nil { - t.Fatalf("failed to migrate database %s", err) - } - - entitlementRepo := entitlementrepo.NewPostgresEntitlementRepo(entitlementDB) - usageResetRepo := entitlementrepo.NewPostgresUsageResetRepo(entitlementDB) - - owner := meteredentitlement.NewEntitlementGrantOwnerAdapter( - featureRepo, - entitlementRepo, - usageResetRepo, - meterRepo, - log, - ) - - balance := credit.NewBalanceConnector( - grantRepo, - balanceSnapshotRepo, - owner, - streaming, - log, - ) - - grant := credit.NewGrantConnector( - owner, - grantRepo, - balanceSnapshotRepo, - time.Minute, - ) - - meteredEntitlementConnector := meteredentitlement.NewMeteredEntitlementConnector( - streaming, - owner, - balance, - grant, - entitlementRepo) - - entitlementConnector := entitlement.NewEntitlementConnector( - entitlementRepo, - featureConnector, - meterRepo, - meteredEntitlementConnector, - staticentitlement.NewStaticEntitlementConnector(), - booleanentitlement.NewBooleanEntitlementConnector(), - ) // Let's create a feature - - feature, err := featureConnector.CreateFeature(ctx, productcatalog.CreateFeatureInputs{ + clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:30:21Z")) + feature, err := deps.FeatureConnector.CreateFeature(ctx, productcatalog.CreateFeatureInputs{ Name: "feature-1", Key: "feature-1", Namespace: "namespace-1", @@ -136,7 +38,7 @@ func TestScenario(t *testing.T) { // Let's create a new entitlement for the feature clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:35:21Z")) - entitlement, err := entitlementConnector.CreateEntitlement(ctx, entitlement.CreateEntitlementInputs{ + entitlement, err := deps.EntitlementConnector.CreateEntitlement(ctx, entitlement.CreateEntitlementInputs{ Namespace: "namespace-1", FeatureID: &feature.ID, FeatureKey: &feature.Key, @@ -153,7 +55,7 @@ func TestScenario(t *testing.T) { // Let's grant some credit clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:35:24Z")) - grant1, err := grant.CreateGrant(ctx, + grant1, err := deps.GrantConnector.CreateGrant(ctx, credit.NamespacedGrantOwner{ Namespace: "namespace-1", ID: credit.GrantOwner(entitlement.ID), @@ -171,7 +73,7 @@ func TestScenario(t *testing.T) { assert.NotNil(grant1) clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-28T14:36:33Z")) - grant2, err := grant.CreateGrant(ctx, + grant2, err := deps.GrantConnector.CreateGrant(ctx, credit.NamespacedGrantOwner{ Namespace: "namespace-1", ID: credit.GrantOwner(entitlement.ID), @@ -190,10 +92,10 @@ func TestScenario(t *testing.T) { assert.NotNil(grant2) // Hack: this is in the future, but at least it won't return an error - streaming.AddSimpleEvent("meter-1", 1, testutils.GetRFC3339Time(t, "2025-06-28T14:36:00Z")) + deps.Streaming.AddSimpleEvent("meter-1", 1, testutils.GetRFC3339Time(t, "2025-06-28T14:36:00Z")) // Let's query the usage - currentBalance, err := meteredEntitlementConnector.GetEntitlementBalance(ctx, + currentBalance, err := deps.MeteredEntitlementConnector.GetEntitlementBalance(ctx, models.NamespacedID{ Namespace: "namespace-1", ID: entitlement.ID, @@ -205,7 +107,7 @@ func TestScenario(t *testing.T) { clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:30:41Z")) // Let's query the usage - currentBalance, err = meteredEntitlementConnector.GetEntitlementBalance(ctx, + currentBalance, err = deps.MeteredEntitlementConnector.GetEntitlementBalance(ctx, models.NamespacedID{ Namespace: "namespace-1", ID: entitlement.ID, @@ -216,7 +118,7 @@ func TestScenario(t *testing.T) { assert.Equal(10.0, currentBalance.Balance) clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:35:54Z")) - grant3, err := grant.CreateGrant(ctx, + grant3, err := deps.GrantConnector.CreateGrant(ctx, credit.NamespacedGrantOwner{ Namespace: "namespace-1", ID: credit.GrantOwner(entitlement.ID), @@ -235,7 +137,7 @@ func TestScenario(t *testing.T) { // There should be a snapshot created clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:37:18Z")) - reset, err := meteredEntitlementConnector.ResetEntitlementUsage(ctx, + reset, err := deps.MeteredEntitlementConnector.ResetEntitlementUsage(ctx, models.NamespacedID{ Namespace: "namespace-1", ID: entitlement.ID, @@ -250,7 +152,7 @@ func TestScenario(t *testing.T) { now := clock.SetTime(testutils.GetRFC3339Time(t, "2024-06-30T15:42:41Z")) // Let's query the usage - currentBalance, err = meteredEntitlementConnector.GetEntitlementBalance(ctx, + currentBalance, err = deps.MeteredEntitlementConnector.GetEntitlementBalance(ctx, models.NamespacedID{ Namespace: "namespace-1", ID: entitlement.ID, From 5363ecdbaeb27c4bf2a940a9cebeb83acd565f21 Mon Sep 17 00:00:00 2001 From: Peter Turi Date: Mon, 1 Jul 2024 13:40:56 +0200 Subject: [PATCH 4/4] fix: replace time.Now with clock.Now --- internal/entitlement/boolean/connector.go | 3 ++- internal/entitlement/static/connector.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/entitlement/boolean/connector.go b/internal/entitlement/boolean/connector.go index bf5763053..7db319832 100644 --- a/internal/entitlement/boolean/connector.go +++ b/internal/entitlement/boolean/connector.go @@ -6,6 +6,7 @@ import ( "github.com/openmeterio/openmeter/internal/entitlement" "github.com/openmeterio/openmeter/internal/productcatalog" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/recurrence" ) @@ -43,7 +44,7 @@ func (c *connector) BeforeCreate(model entitlement.CreateEntitlementInputs, feat if model.UsagePeriod != nil { usagePeriod = model.UsagePeriod - calculatedPeriod, err := usagePeriod.GetCurrentPeriodAt(time.Now()) + calculatedPeriod, err := usagePeriod.GetCurrentPeriodAt(clock.Now()) if err != nil { return nil, err } diff --git a/internal/entitlement/static/connector.go b/internal/entitlement/static/connector.go index cf87e4339..1edff8494 100644 --- a/internal/entitlement/static/connector.go +++ b/internal/entitlement/static/connector.go @@ -7,6 +7,7 @@ import ( "github.com/openmeterio/openmeter/internal/entitlement" "github.com/openmeterio/openmeter/internal/productcatalog" + "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/recurrence" ) @@ -63,7 +64,7 @@ func (c *connector) BeforeCreate(model entitlement.CreateEntitlementInputs, feat if model.UsagePeriod != nil { usagePeriod = model.UsagePeriod - calculatedPeriod, err := usagePeriod.GetCurrentPeriodAt(time.Now()) + calculatedPeriod, err := usagePeriod.GetCurrentPeriodAt(clock.Now()) if err != nil { return nil, err }