mirror of
https://github.com/zitadel/zitadel.git
synced 2025-05-30 19:38:21 +00:00
fix: set quotas (#6597)
* feat: set quotas * fix: start new period on younger anchor * cleanup e2e config * fix set notifications * lint * test: fix quota projection tests * fix add quota tests * make quota fields nullable * enable amount 0 * fix initial setup * create a prerelease * avoid success comments * fix quota projection primary key * Revert "fix quota projection primary key" This reverts commit e72f4d7fa17d03d36493912168490350a320e04f. * simplify write model * fix aggregate id * avoid push without changes * test set quota lifecycle * test set quota mutations * fix quota unit test * fix: quotas * test quota.set event projection * use SetQuota in integration tests * fix: release quotas 3 * reset releaserc * fix comment * test notification order doesn't matter * test notification order doesn't matter * test with unmarshalled events * test with unmarshalled events (cherry picked from commit ae1af6bc8cd2294f47f6d6412c4b46192105ade5)
This commit is contained in:
parent
41e31aad41
commit
1d4ec6cdba
26
cmd/setup/13.go
Normal file
26
cmd/setup/13.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed 13/13_fix_quota_constraints.sql
|
||||||
|
fixQuotaConstraints string
|
||||||
|
)
|
||||||
|
|
||||||
|
type FixQuotaConstraints struct {
|
||||||
|
dbClient *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *FixQuotaConstraints) Execute(ctx context.Context) error {
|
||||||
|
_, err := mig.dbClient.ExecContext(ctx, fixQuotaConstraints)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *FixQuotaConstraints) String() string {
|
||||||
|
return "13_fix_quota_constraints"
|
||||||
|
}
|
4
cmd/setup/13/13_fix_quota_constraints.sql
Normal file
4
cmd/setup/13/13_fix_quota_constraints.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE IF EXISTS projections.quotas ALTER COLUMN from_anchor DROP NOT NULL;
|
||||||
|
ALTER TABLE IF EXISTS projections.quotas ALTER COLUMN amount DROP NOT NULL;
|
||||||
|
ALTER TABLE IF EXISTS projections.quotas ALTER COLUMN interval DROP NOT NULL;
|
||||||
|
ALTER TABLE IF EXISTS projections.quotas ALTER COLUMN limit_usage DROP NOT NULL;
|
@ -56,18 +56,19 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Steps struct {
|
type Steps struct {
|
||||||
s1ProjectionTable *ProjectionTable
|
s1ProjectionTable *ProjectionTable
|
||||||
s2AssetsTable *AssetTable
|
s2AssetsTable *AssetTable
|
||||||
FirstInstance *FirstInstance
|
FirstInstance *FirstInstance
|
||||||
s4EventstoreIndexes *EventstoreIndexesNew
|
s4EventstoreIndexes *EventstoreIndexesNew
|
||||||
s5LastFailed *LastFailed
|
s5LastFailed *LastFailed
|
||||||
s6OwnerRemoveColumns *OwnerRemoveColumns
|
s6OwnerRemoveColumns *OwnerRemoveColumns
|
||||||
s7LogstoreTables *LogstoreTables
|
s7LogstoreTables *LogstoreTables
|
||||||
s8AuthTokens *AuthTokenIndexes
|
s8AuthTokens *AuthTokenIndexes
|
||||||
s9EventstoreIndexes2 *EventstoreIndexesNew
|
s9EventstoreIndexes2 *EventstoreIndexesNew
|
||||||
CorrectCreationDate *CorrectCreationDate
|
CorrectCreationDate *CorrectCreationDate
|
||||||
AddEventCreatedAt *AddEventCreatedAt
|
AddEventCreatedAt *AddEventCreatedAt
|
||||||
s12AddOTPColumns *AddOTPColumns
|
s12AddOTPColumns *AddOTPColumns
|
||||||
|
s13FixQuotaProjection *FixQuotaConstraints
|
||||||
}
|
}
|
||||||
|
|
||||||
type encryptionKeyConfig struct {
|
type encryptionKeyConfig struct {
|
||||||
|
@ -95,6 +95,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
|||||||
steps.AddEventCreatedAt.dbClient = dbClient
|
steps.AddEventCreatedAt.dbClient = dbClient
|
||||||
steps.AddEventCreatedAt.step10 = steps.CorrectCreationDate
|
steps.AddEventCreatedAt.step10 = steps.CorrectCreationDate
|
||||||
steps.s12AddOTPColumns = &AddOTPColumns{dbClient: dbClient}
|
steps.s12AddOTPColumns = &AddOTPColumns{dbClient: dbClient}
|
||||||
|
steps.s13FixQuotaProjection = &FixQuotaConstraints{dbClient: dbClient}
|
||||||
|
|
||||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
|
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
|
||||||
logging.OnError(err).Fatal("unable to start projections")
|
logging.OnError(err).Fatal("unable to start projections")
|
||||||
@ -137,6 +138,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
|
|||||||
logging.OnError(err).Fatal("unable to migrate step 11")
|
logging.OnError(err).Fatal("unable to migrate step 11")
|
||||||
err = migration.Migrate(ctx, eventstoreClient, steps.s12AddOTPColumns)
|
err = migration.Migrate(ctx, eventstoreClient, steps.s12AddOTPColumns)
|
||||||
logging.OnError(err).Fatal("unable to migrate step 12")
|
logging.OnError(err).Fatal("unable to migrate step 12")
|
||||||
|
err = migration.Migrate(ctx, eventstoreClient, steps.s13FixQuotaProjection)
|
||||||
|
logging.OnError(err).Fatal("unable to migrate step 13")
|
||||||
|
|
||||||
for _, repeatableStep := range repeatableSteps {
|
for _, repeatableStep := range repeatableSteps {
|
||||||
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)
|
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)
|
||||||
|
@ -17,19 +17,6 @@ FirstInstance:
|
|||||||
Human:
|
Human:
|
||||||
PasswordChangeRequired: false
|
PasswordChangeRequired: false
|
||||||
|
|
||||||
LogStore:
|
|
||||||
Access:
|
|
||||||
Database:
|
|
||||||
Enabled: true
|
|
||||||
Debounce:
|
|
||||||
MinFrequency: 0s
|
|
||||||
MaxBulkSize: 0
|
|
||||||
Execution:
|
|
||||||
Database:
|
|
||||||
Enabled: true
|
|
||||||
Stdout:
|
|
||||||
Enabled: true
|
|
||||||
|
|
||||||
Console:
|
Console:
|
||||||
InstanceManagementURL: "https://example.com/instances/{{.InstanceID}}"
|
InstanceManagementURL: "https://example.com/instances/{{.InstanceID}}"
|
||||||
|
|
||||||
@ -40,6 +27,10 @@ Projections:
|
|||||||
|
|
||||||
Quotas:
|
Quotas:
|
||||||
Access:
|
Access:
|
||||||
|
Enabled: true
|
||||||
|
Debounce:
|
||||||
|
MinFrequency: 0s
|
||||||
|
MaxBulkSize: 0
|
||||||
ExhaustedCookieKey: "zitadel.quota.limiting"
|
ExhaustedCookieKey: "zitadel.quota.limiting"
|
||||||
ExhaustedCookieMaxAge: "600s"
|
ExhaustedCookieMaxAge: "600s"
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
"github.com/zitadel/zitadel/internal/api/grpc/object"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||||
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) AddQuota(ctx context.Context, req *system.AddQuotaRequest) (*system.AddQuotaResponse, error) {
|
func (s *Server) AddQuota(ctx context.Context, req *system.AddQuotaRequest) (*system.AddQuotaResponse, error) {
|
||||||
@ -16,7 +15,20 @@ func (s *Server) AddQuota(ctx context.Context, req *system.AddQuotaRequest) (*sy
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &system_pb.AddQuotaResponse{
|
return &system.AddQuotaResponse{
|
||||||
|
Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) SetQuota(ctx context.Context, req *system.SetQuotaRequest) (*system.SetQuotaResponse, error) {
|
||||||
|
details, err := s.command.SetQuota(
|
||||||
|
ctx,
|
||||||
|
instanceQuotaPbToCommand(req),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &system.SetQuotaResponse{
|
||||||
Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -26,7 +38,7 @@ func (s *Server) RemoveQuota(ctx context.Context, req *system.RemoveQuotaRequest
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &system_pb.RemoveQuotaResponse{
|
return &system.RemoveQuotaResponse{
|
||||||
Details: object.ChangeToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
Details: object.ChangeToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,27 @@ package system
|
|||||||
import (
|
import (
|
||||||
"github.com/zitadel/zitadel/internal/command"
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/quota"
|
"github.com/zitadel/zitadel/pkg/grpc/quota"
|
||||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func instanceQuotaPbToCommand(req *system.AddQuotaRequest) *command.AddQuota {
|
type setQuotaRequest interface {
|
||||||
return &command.AddQuota{
|
GetUnit() quota.Unit
|
||||||
Unit: instanceQuotaUnitPbToCommand(req.Unit),
|
GetFrom() *timestamppb.Timestamp
|
||||||
From: req.From.AsTime(),
|
GetResetInterval() *durationpb.Duration
|
||||||
ResetInterval: req.ResetInterval.AsDuration(),
|
GetAmount() uint64
|
||||||
Amount: req.Amount,
|
GetLimit() bool
|
||||||
Limit: req.Limit,
|
GetNotifications() []*quota.Notification
|
||||||
Notifications: instanceQuotaNotificationsPbToCommand(req.Notifications),
|
}
|
||||||
|
|
||||||
|
func instanceQuotaPbToCommand(req setQuotaRequest) *command.SetQuota {
|
||||||
|
return &command.SetQuota{
|
||||||
|
Unit: instanceQuotaUnitPbToCommand(req.GetUnit()),
|
||||||
|
From: req.GetFrom().AsTime(),
|
||||||
|
ResetInterval: req.GetResetInterval().AsDuration(),
|
||||||
|
Amount: req.GetAmount(),
|
||||||
|
Limit: req.GetLimit(),
|
||||||
|
Notifications: instanceQuotaNotificationsPbToCommand(req.GetNotifications()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ func TestServer_QuotaNotification_Limit(t *testing.T) {
|
|||||||
percent := 50
|
percent := 50
|
||||||
percentAmount := amount * percent / 100
|
percentAmount := amount * percent / 100
|
||||||
|
|
||||||
_, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{
|
_, err := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{
|
||||||
InstanceId: instanceID,
|
InstanceId: instanceID,
|
||||||
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
||||||
From: timestamppb.Now(),
|
From: timestamppb.Now(),
|
||||||
@ -72,7 +72,7 @@ func TestServer_QuotaNotification_NoLimit(t *testing.T) {
|
|||||||
percent := 50
|
percent := 50
|
||||||
percentAmount := amount * percent / 100
|
percentAmount := amount * percent / 100
|
||||||
|
|
||||||
_, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{
|
_, err := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{
|
||||||
InstanceId: instanceID,
|
InstanceId: instanceID,
|
||||||
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
||||||
From: timestamppb.Now(),
|
From: timestamppb.Now(),
|
||||||
@ -151,7 +151,7 @@ func awaitNotification(t *testing.T, bodies chan []byte, unit quota.Unit, percen
|
|||||||
func TestServer_AddAndRemoveQuota(t *testing.T) {
|
func TestServer_AddAndRemoveQuota(t *testing.T) {
|
||||||
_, instanceID, _ := Tester.UseIsolatedInstance(CTX, SystemCTX)
|
_, instanceID, _ := Tester.UseIsolatedInstance(CTX, SystemCTX)
|
||||||
|
|
||||||
got, err := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{
|
got, err := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{
|
||||||
InstanceId: instanceID,
|
InstanceId: instanceID,
|
||||||
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
||||||
From: timestamppb.Now(),
|
From: timestamppb.Now(),
|
||||||
@ -169,7 +169,7 @@ func TestServer_AddAndRemoveQuota(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, got.Details.ResourceOwner, instanceID)
|
require.Equal(t, got.Details.ResourceOwner, instanceID)
|
||||||
|
|
||||||
gotAlreadyExisting, errAlreadyExisting := Tester.Client.System.AddQuota(SystemCTX, &system.AddQuotaRequest{
|
gotAlreadyExisting, errAlreadyExisting := Tester.Client.System.SetQuota(SystemCTX, &system.SetQuotaRequest{
|
||||||
InstanceId: instanceID,
|
InstanceId: instanceID,
|
||||||
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
Unit: quota_pb.Unit_UNIT_REQUESTS_ALL_AUTHENTICATED,
|
||||||
From: timestamppb.Now(),
|
From: timestamppb.Now(),
|
||||||
@ -184,8 +184,8 @@ func TestServer_AddAndRemoveQuota(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.Error(t, errAlreadyExisting)
|
require.Nil(t, errAlreadyExisting)
|
||||||
require.Nil(t, gotAlreadyExisting)
|
require.Equal(t, gotAlreadyExisting.Details.ResourceOwner, instanceID)
|
||||||
|
|
||||||
gotRemove, errRemove := Tester.Client.System.RemoveQuota(SystemCTX, &system.RemoveQuotaRequest{
|
gotRemove, errRemove := Tester.Client.System.RemoveQuota(SystemCTX, &system.RemoveQuotaRequest{
|
||||||
InstanceId: instanceID,
|
InstanceId: instanceID,
|
||||||
|
@ -110,7 +110,7 @@ type InstanceSetup struct {
|
|||||||
RefreshTokenExpiration time.Duration
|
RefreshTokenExpiration time.Duration
|
||||||
}
|
}
|
||||||
Quotas *struct {
|
Quotas *struct {
|
||||||
Items []*AddQuota
|
Items []*SetQuota
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +283,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, nil, err
|
return "", "", nil, nil, err
|
||||||
}
|
}
|
||||||
validations = append(validations, c.AddQuotaCommand(quota.NewAggregate(quotaId, instanceID), q))
|
validations = append(validations, c.SetQuotaCommand(quota.NewAggregate(quotaId, instanceID), nil, true, q))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/id"
|
|
||||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,25 +31,25 @@ func (q QuotaUnit) Enum() quota.Unit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddQuota returns and error if the quota already exists.
|
||||||
|
// AddQuota is deprecated. Use SetQuota instead.
|
||||||
func (c *Commands) AddQuota(
|
func (c *Commands) AddQuota(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
q *AddQuota,
|
q *SetQuota,
|
||||||
) (*domain.ObjectDetails, error) {
|
) (*domain.ObjectDetails, error) {
|
||||||
instanceId := authz.GetInstance(ctx).InstanceID()
|
instanceId := authz.GetInstance(ctx).InstanceID()
|
||||||
|
|
||||||
wm, err := c.getQuotaWriteModel(ctx, instanceId, instanceId, q.Unit.Enum())
|
wm, err := c.getQuotaWriteModel(ctx, instanceId, instanceId, q.Unit.Enum())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if wm.AggregateID != "" {
|
||||||
if wm.active {
|
|
||||||
return nil, errors.ThrowAlreadyExists(nil, "COMMAND-WDfFf", "Errors.Quota.AlreadyExists")
|
return nil, errors.ThrowAlreadyExists(nil, "COMMAND-WDfFf", "Errors.Quota.AlreadyExists")
|
||||||
}
|
}
|
||||||
aggregateId, err := c.idGenerator.Next()
|
aggregateId, err := c.idGenerator.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddQuotaCommand(quota.NewAggregate(aggregateId, instanceId), q))
|
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.SetQuotaCommand(quota.NewAggregate(aggregateId, instanceId), wm, true, q))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -65,23 +64,52 @@ func (c *Commands) AddQuota(
|
|||||||
return writeModelToObjectDetails(&wm.WriteModel), nil
|
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetQuota creates a new quota or updates an existing one.
|
||||||
|
func (c *Commands) SetQuota(
|
||||||
|
ctx context.Context,
|
||||||
|
q *SetQuota,
|
||||||
|
) (*domain.ObjectDetails, error) {
|
||||||
|
instanceId := authz.GetInstance(ctx).InstanceID()
|
||||||
|
wm, err := c.getQuotaWriteModel(ctx, instanceId, instanceId, q.Unit.Enum())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
aggregateId := wm.AggregateID
|
||||||
|
createNewQuota := aggregateId == ""
|
||||||
|
if aggregateId == "" {
|
||||||
|
aggregateId, err = c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.SetQuotaCommand(quota.NewAggregate(aggregateId, instanceId), wm, createNewQuota, q))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(cmds) > 0 {
|
||||||
|
events, err := c.eventstore.Push(ctx, cmds...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = AppendAndReduce(wm, events...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Commands) RemoveQuota(ctx context.Context, unit QuotaUnit) (*domain.ObjectDetails, error) {
|
func (c *Commands) RemoveQuota(ctx context.Context, unit QuotaUnit) (*domain.ObjectDetails, error) {
|
||||||
instanceId := authz.GetInstance(ctx).InstanceID()
|
instanceId := authz.GetInstance(ctx).InstanceID()
|
||||||
|
|
||||||
wm, err := c.getQuotaWriteModel(ctx, instanceId, instanceId, unit.Enum())
|
wm, err := c.getQuotaWriteModel(ctx, instanceId, instanceId, unit.Enum())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if wm.AggregateID == "" {
|
||||||
if !wm.active {
|
|
||||||
return nil, errors.ThrowNotFound(nil, "COMMAND-WDfFf", "Errors.Quota.NotFound")
|
return nil, errors.ThrowNotFound(nil, "COMMAND-WDfFf", "Errors.Quota.NotFound")
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregate := quota.NewAggregate(wm.AggregateID, instanceId)
|
aggregate := quota.NewAggregate(wm.AggregateID, instanceId)
|
||||||
|
events := []eventstore.Command{quota.NewRemovedEvent(ctx, &aggregate.Aggregate, unit.Enum())}
|
||||||
events := []eventstore.Command{
|
|
||||||
quota.NewRemovedEvent(ctx, &aggregate.Aggregate, unit.Enum()),
|
|
||||||
}
|
|
||||||
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -104,6 +132,16 @@ type QuotaNotification struct {
|
|||||||
CallURL string
|
CallURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetQuota configures a quota and activates it if it isn't active already
|
||||||
|
type SetQuota struct {
|
||||||
|
Unit QuotaUnit `json:"unit"`
|
||||||
|
From time.Time `json:"from"`
|
||||||
|
ResetInterval time.Duration `json:"ResetInterval,omitempty"`
|
||||||
|
Amount uint64 `json:"Amount,omitempty"`
|
||||||
|
Limit bool `json:"Limit,omitempty"`
|
||||||
|
Notifications QuotaNotifications `json:"Notifications,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type QuotaNotifications []*QuotaNotification
|
type QuotaNotifications []*QuotaNotification
|
||||||
|
|
||||||
func (q *QuotaNotification) validate() error {
|
func (q *QuotaNotification) validate() error {
|
||||||
@ -111,94 +149,51 @@ func (q *QuotaNotification) validate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.ThrowInvalidArgument(err, "QUOTA-bZ0Fj", "Errors.Quota.Invalid.CallURL")
|
return errors.ThrowInvalidArgument(err, "QUOTA-bZ0Fj", "Errors.Quota.Invalid.CallURL")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !u.IsAbs() || u.Host == "" {
|
if !u.IsAbs() || u.Host == "" {
|
||||||
return errors.ThrowInvalidArgument(nil, "QUOTA-HAYmN", "Errors.Quota.Invalid.CallURL")
|
return errors.ThrowInvalidArgument(nil, "QUOTA-HAYmN", "Errors.Quota.Invalid.CallURL")
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.Percent < 1 {
|
if q.Percent < 1 {
|
||||||
return errors.ThrowInvalidArgument(nil, "QUOTA-pBfjq", "Errors.Quota.Invalid.Percent")
|
return errors.ThrowInvalidArgument(nil, "QUOTA-pBfjq", "Errors.Quota.Invalid.Percent")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QuotaNotifications) toAddedEventNotifications(idGenerator id.Generator) ([]*quota.AddedEventNotification, error) {
|
func (q *SetQuota) validate() error {
|
||||||
if q == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications := make([]*quota.AddedEventNotification, len(*q))
|
|
||||||
for idx, notification := range *q {
|
|
||||||
|
|
||||||
id, err := idGenerator.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
notifications[idx] = "a.AddedEventNotification{
|
|
||||||
ID: id,
|
|
||||||
Percent: notification.Percent,
|
|
||||||
Repeat: notification.Repeat,
|
|
||||||
CallURL: notification.CallURL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return notifications, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddQuota struct {
|
|
||||||
Unit QuotaUnit
|
|
||||||
From time.Time
|
|
||||||
ResetInterval time.Duration
|
|
||||||
Amount uint64
|
|
||||||
Limit bool
|
|
||||||
Notifications QuotaNotifications
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *AddQuota) validate() error {
|
|
||||||
for _, notification := range q.Notifications {
|
for _, notification := range q.Notifications {
|
||||||
if err := notification.validate(); err != nil {
|
if err := notification.validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.Unit.Enum() == quota.Unimplemented {
|
if q.Unit.Enum() == quota.Unimplemented {
|
||||||
return errors.ThrowInvalidArgument(nil, "QUOTA-OTeSh", "Errors.Quota.Invalid.Unimplemented")
|
return errors.ThrowInvalidArgument(nil, "QUOTA-OTeSh", "Errors.Quota.Invalid.Unimplemented")
|
||||||
}
|
}
|
||||||
|
if q.Amount < 0 {
|
||||||
if q.Amount < 1 {
|
|
||||||
return errors.ThrowInvalidArgument(nil, "QUOTA-hOKSJ", "Errors.Quota.Invalid.Amount")
|
return errors.ThrowInvalidArgument(nil, "QUOTA-hOKSJ", "Errors.Quota.Invalid.Amount")
|
||||||
}
|
}
|
||||||
|
|
||||||
if q.ResetInterval < time.Minute {
|
if q.ResetInterval < time.Minute {
|
||||||
return errors.ThrowInvalidArgument(nil, "QUOTA-R5otd", "Errors.Quota.Invalid.ResetInterval")
|
return errors.ThrowInvalidArgument(nil, "QUOTA-R5otd", "Errors.Quota.Invalid.ResetInterval")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) AddQuotaCommand(a *quota.Aggregate, q *AddQuota) preparation.Validation {
|
func (c *Commands) SetQuotaCommand(a *quota.Aggregate, wm *quotaWriteModel, createNew bool, q *SetQuota) preparation.Validation {
|
||||||
return func() (preparation.CreateCommands, error) {
|
return func() (preparation.CreateCommands, error) {
|
||||||
|
|
||||||
if err := q.validate(); err != nil {
|
if err := q.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (cmd []eventstore.Command, err error) {
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (cmd []eventstore.Command, err error) {
|
||||||
|
changes, err := wm.NewChanges(c.idGenerator, createNew, q.Amount, q.From, q.ResetInterval, q.Limit, q.Notifications...)
|
||||||
notifications, err := q.Notifications.toAddedEventNotifications(c.idGenerator)
|
if len(changes) == 0 {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return []eventstore.Command{quota.NewSetEvent(
|
||||||
return []eventstore.Command{quota.NewAddedEvent(
|
eventstore.NewBaseEventForPush(
|
||||||
ctx,
|
ctx,
|
||||||
&a.Aggregate,
|
&a.Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
q.Unit.Enum(),
|
q.Unit.Enum(),
|
||||||
q.From,
|
changes...,
|
||||||
q.ResetInterval,
|
|
||||||
q.Amount,
|
|
||||||
q.Limit,
|
|
||||||
notifications,
|
|
||||||
)}, err
|
)}, err
|
||||||
},
|
},
|
||||||
nil
|
nil
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||||
)
|
)
|
||||||
|
|
||||||
type quotaWriteModel struct {
|
type quotaWriteModel struct {
|
||||||
eventstore.WriteModel
|
eventstore.WriteModel
|
||||||
unit quota.Unit
|
rollingAggregateID string
|
||||||
active bool
|
unit quota.Unit
|
||||||
|
from time.Time
|
||||||
|
resetInterval time.Duration
|
||||||
|
amount uint64
|
||||||
|
limit bool
|
||||||
|
notifications []*quota.SetEventNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
// newQuotaWriteModel aggregateId is filled by reducing unit matching events
|
// newQuotaWriteModel aggregateId is filled by reducing unit matching events
|
||||||
@ -30,6 +42,7 @@ func (wm *quotaWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
AggregateTypes(quota.AggregateType).
|
AggregateTypes(quota.AggregateType).
|
||||||
EventTypes(
|
EventTypes(
|
||||||
quota.AddedEventType,
|
quota.AddedEventType,
|
||||||
|
quota.SetEventType,
|
||||||
quota.RemovedEventType,
|
quota.RemovedEventType,
|
||||||
).EventData(map[string]interface{}{"unit": wm.unit})
|
).EventData(map[string]interface{}{"unit": wm.unit})
|
||||||
|
|
||||||
@ -38,15 +51,137 @@ func (wm *quotaWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
|
|
||||||
func (wm *quotaWriteModel) Reduce() error {
|
func (wm *quotaWriteModel) Reduce() error {
|
||||||
for _, event := range wm.Events {
|
for _, event := range wm.Events {
|
||||||
|
wm.ChangeDate = event.CreationDate()
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *quota.AddedEvent:
|
case *quota.SetEvent:
|
||||||
wm.AggregateID = e.Aggregate().ID
|
wm.rollingAggregateID = e.Aggregate().ID
|
||||||
wm.ChangeDate = e.CreationDate()
|
if e.Amount != nil {
|
||||||
wm.active = true
|
wm.amount = *e.Amount
|
||||||
|
}
|
||||||
|
if e.From != nil {
|
||||||
|
wm.from = *e.From
|
||||||
|
}
|
||||||
|
if e.Limit != nil {
|
||||||
|
wm.limit = *e.Limit
|
||||||
|
}
|
||||||
|
if e.ResetInterval != nil {
|
||||||
|
wm.resetInterval = *e.ResetInterval
|
||||||
|
}
|
||||||
|
if e.Notifications != nil {
|
||||||
|
wm.notifications = *e.Notifications
|
||||||
|
}
|
||||||
case *quota.RemovedEvent:
|
case *quota.RemovedEvent:
|
||||||
wm.AggregateID = e.Aggregate().ID
|
wm.rollingAggregateID = ""
|
||||||
wm.active = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wm.WriteModel.Reduce()
|
if err := wm.WriteModel.Reduce(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// wm.WriteModel.Reduce() sets the aggregateID to the first event's aggregateID, but we need the last one
|
||||||
|
wm.AggregateID = wm.rollingAggregateID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChanges returns all changes that need to be applied to the aggregate.
|
||||||
|
// If createNew is true, all quota properties are set.
|
||||||
|
func (wm *quotaWriteModel) NewChanges(
|
||||||
|
idGenerator id.Generator,
|
||||||
|
createNew bool,
|
||||||
|
amount uint64,
|
||||||
|
from time.Time,
|
||||||
|
resetInterval time.Duration,
|
||||||
|
limit bool,
|
||||||
|
notifications ...*QuotaNotification,
|
||||||
|
) (changes []quota.QuotaChange, err error) {
|
||||||
|
setEventNotifications, err := QuotaNotifications(notifications).newSetEventNotifications(idGenerator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// we sort the input notifications already, so we can return early if they have duplicates
|
||||||
|
err = sortSetEventNotifications(setEventNotifications)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if createNew {
|
||||||
|
return []quota.QuotaChange{
|
||||||
|
quota.ChangeAmount(amount),
|
||||||
|
quota.ChangeFrom(from),
|
||||||
|
quota.ChangeResetInterval(resetInterval),
|
||||||
|
quota.ChangeLimit(limit),
|
||||||
|
quota.ChangeNotifications(setEventNotifications),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
changes = make([]quota.QuotaChange, 0, 5)
|
||||||
|
if wm.amount != amount {
|
||||||
|
changes = append(changes, quota.ChangeAmount(amount))
|
||||||
|
}
|
||||||
|
if wm.from != from {
|
||||||
|
changes = append(changes, quota.ChangeFrom(from))
|
||||||
|
}
|
||||||
|
if wm.resetInterval != resetInterval {
|
||||||
|
changes = append(changes, quota.ChangeResetInterval(resetInterval))
|
||||||
|
}
|
||||||
|
if wm.limit != limit {
|
||||||
|
changes = append(changes, quota.ChangeLimit(limit))
|
||||||
|
}
|
||||||
|
// If the number of notifications differs, we renew the notifications and we can return early
|
||||||
|
if len(setEventNotifications) != len(wm.notifications) {
|
||||||
|
changes = append(changes, quota.ChangeNotifications(setEventNotifications))
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
// Now we sort the existing notifications too, so comparing the input properties with the existing ones is easier.
|
||||||
|
// We ignore the sorting error for the existing notifications, because this is system state, not user input.
|
||||||
|
// If sorting fails this time, the notifications are listed in the event payload and the projection cleans them up anyway.
|
||||||
|
_ = sortSetEventNotifications(wm.notifications)
|
||||||
|
for i, notification := range setEventNotifications {
|
||||||
|
if notification.CallURL != wm.notifications[i].CallURL ||
|
||||||
|
notification.Percent != wm.notifications[i].Percent ||
|
||||||
|
notification.Repeat != wm.notifications[i].Repeat {
|
||||||
|
changes = append(changes, quota.ChangeNotifications(setEventNotifications))
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSetEventNotifications returns quota.SetEventNotification elements with generated IDs.
|
||||||
|
func (q QuotaNotifications) newSetEventNotifications(idGenerator id.Generator) (setNotifications []*quota.SetEventNotification, err error) {
|
||||||
|
if q == nil {
|
||||||
|
return make([]*quota.SetEventNotification, 0), nil
|
||||||
|
}
|
||||||
|
notifications := make([]*quota.SetEventNotification, len(q))
|
||||||
|
for idx, notification := range q {
|
||||||
|
notifications[idx] = "a.SetEventNotification{
|
||||||
|
Percent: notification.Percent,
|
||||||
|
Repeat: notification.Repeat,
|
||||||
|
CallURL: notification.CallURL,
|
||||||
|
}
|
||||||
|
notifications[idx].ID, err = idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortSetEventNotifications reports an error if there are duplicate notifications or if a pointer is nil
|
||||||
|
func sortSetEventNotifications(notifications []*quota.SetEventNotification) (err error) {
|
||||||
|
slices.SortFunc(notifications, func(i, j *quota.SetEventNotification) int {
|
||||||
|
if i == nil || j == nil {
|
||||||
|
err = zitadel_errors.ThrowInternal(errors.New("sorting slices of *quota.SetEventNotification with nil pointers is not supported"), "QUOTA-8YXPk", "Errors.Internal")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if i.Percent == j.Percent && i.CallURL == j.CallURL && i.Repeat == j.Repeat {
|
||||||
|
// TODO: translate
|
||||||
|
err = zitadel_errors.ThrowInternal(fmt.Errorf("%+v", i), "QUOTA-Pty2n", "Errors.Quota.Notifications.Duplicate")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if i.Percent < j.Percent ||
|
||||||
|
i.Percent == j.Percent && i.CallURL < j.CallURL ||
|
||||||
|
i.Percent == j.Percent && i.CallURL == j.CallURL && i.Repeat == false && j.Repeat == true {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return +1
|
||||||
|
})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
373
internal/command/quota_model_test.go
Normal file
373
internal/command/quota_model_test.go
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/id"
|
||||||
|
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuotaWriteModel_NewChanges(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
from time.Time
|
||||||
|
resetInterval time.Duration
|
||||||
|
amount uint64
|
||||||
|
limit bool
|
||||||
|
notifications []*quota.SetEventNotification
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
idGenerator id.Generator
|
||||||
|
createNew bool
|
||||||
|
amount uint64
|
||||||
|
from time.Time
|
||||||
|
resetInterval time.Duration
|
||||||
|
limit bool
|
||||||
|
notifications []*QuotaNotification
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantEvent quota.SetEvent
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{{
|
||||||
|
name: "change reset interval",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*quota.SetEventNotification, 0),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Minute,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*QuotaNotification, 0),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{
|
||||||
|
ResetInterval: durationPtr(time.Minute),
|
||||||
|
},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change reset interval and amount",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*quota.SetEventNotification, 0),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 10,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Minute,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*QuotaNotification, 0),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{
|
||||||
|
ResetInterval: durationPtr(time.Minute),
|
||||||
|
Amount: uint64Ptr(10),
|
||||||
|
},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change nothing",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*quota.SetEventNotification{},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{},
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change limit to zero value",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*quota.SetEventNotification, 0),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: false,
|
||||||
|
notifications: make([]*QuotaNotification, 0),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{Limit: boolPtr(false)},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change amount to zero value",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*quota.SetEventNotification, 0),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 0,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*QuotaNotification, 0),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{Amount: uint64Ptr(0)},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change from to zero value",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*quota.SetEventNotification, 0),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Time{},
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: make([]*QuotaNotification, 0),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{From: &time.Time{}},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "add notification",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{{
|
||||||
|
Percent: 20,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "notification1"),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{Notifications: &[]*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 20,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}}},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change nothing with notification",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{{
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
idGenerator: id_mock.NewIDGenerator(t),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change nothing but notification order",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}, {
|
||||||
|
ID: "notification2",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: false,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{{
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: false,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}, {
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "newnotification1", "newnotification2"),
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "change notification to zero value",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{},
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{Notifications: &[]*quota.SetEventNotification{}},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "create new without notification",
|
||||||
|
fields: fields{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{},
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{Notifications: &[]*quota.SetEventNotification{}},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "create new with all values values",
|
||||||
|
args: args{
|
||||||
|
amount: 5,
|
||||||
|
from: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
resetInterval: time.Hour,
|
||||||
|
limit: true,
|
||||||
|
notifications: []*QuotaNotification{{
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "notification1"),
|
||||||
|
createNew: true,
|
||||||
|
},
|
||||||
|
wantEvent: quota.SetEvent{
|
||||||
|
From: timePtr(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
ResetInterval: durationPtr(time.Hour),
|
||||||
|
Amount: uint64Ptr(5),
|
||||||
|
Limit: boolPtr(true),
|
||||||
|
Notifications: &[]*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 10,
|
||||||
|
Repeat: true,
|
||||||
|
CallURL: "https://call.url",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
}, {
|
||||||
|
name: "create new with zero values",
|
||||||
|
args: args{createNew: true},
|
||||||
|
wantEvent: quota.SetEvent{
|
||||||
|
From: &time.Time{},
|
||||||
|
ResetInterval: durationPtr(0),
|
||||||
|
Amount: uint64Ptr(0),
|
||||||
|
Limit: boolPtr(false),
|
||||||
|
Notifications: &[]*quota.SetEventNotification{},
|
||||||
|
},
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
wm := "aWriteModel{
|
||||||
|
from: tt.fields.from,
|
||||||
|
resetInterval: tt.fields.resetInterval,
|
||||||
|
amount: tt.fields.amount,
|
||||||
|
limit: tt.fields.limit,
|
||||||
|
notifications: tt.fields.notifications,
|
||||||
|
}
|
||||||
|
gotChanges, err := wm.NewChanges(tt.args.idGenerator, tt.args.createNew, tt.args.amount, tt.args.from, tt.args.resetInterval, tt.args.limit, tt.args.notifications...)
|
||||||
|
if !tt.wantErr(t, err, fmt.Sprintf("NewChanges(%v, %v, %v, %v, %v, %v)", tt.args.createNew, tt.args.amount, tt.args.from, tt.args.resetInterval, tt.args.limit, tt.args.notifications)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
marshalled, err := json.Marshal(quota.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "instance1").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
|
quota.Unimplemented,
|
||||||
|
gotChanges...,
|
||||||
|
))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
unmarshalled := new(quota.SetEvent)
|
||||||
|
assert.NoError(t, json.Unmarshal(marshalled, unmarshalled))
|
||||||
|
assert.Equalf(t, tt.wantEvent, *unmarshalled, "NewChanges(%v, %v, %v, %v, %v, %v)", tt.args.createNew, tt.args.amount, tt.args.from, tt.args.resetInterval, tt.args.limit, tt.args.notifications)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint64Ptr(i uint64) *uint64 { return &i }
|
||||||
|
func boolPtr(b bool) *bool { return &b }
|
||||||
|
func durationPtr(d time.Duration) *time.Duration { return &d }
|
||||||
|
func timePtr(t time.Time) *time.Time { return &t }
|
@ -25,7 +25,7 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
addQuota *AddQuota
|
setQuota *SetQuota
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
want *domain.ObjectDetails
|
want *domain.ObjectDetails
|
||||||
@ -44,14 +44,18 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
t,
|
t,
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusher(
|
eventFromEventPusher(
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Now(),
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
false,
|
quota.ChangeLimit(false),
|
||||||
nil,
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -59,13 +63,12 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
addQuota: &AddQuota{
|
setQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Time{},
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
ResetInterval: 0,
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
Amount: 0,
|
Amount: 1000,
|
||||||
Limit: false,
|
Limit: true,
|
||||||
Notifications: nil,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
@ -83,7 +86,7 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
addQuota: &AddQuota{
|
setQuota: &SetQuota{
|
||||||
Unit: "unimplemented",
|
Unit: "unimplemented",
|
||||||
From: time.Time{},
|
From: time.Time{},
|
||||||
ResetInterval: 0,
|
ResetInterval: 0,
|
||||||
@ -108,25 +111,28 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"INSTANCE",
|
"INSTANCE",
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
true,
|
quota.ChangeLimit(true),
|
||||||
nil,
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", quota.NewAddQuotaUnitUniqueConstraint(quota.RequestsAllAuthenticated)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
addQuota: &AddQuota{
|
setQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
ResetInterval: 30 * 24 * time.Hour,
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
@ -142,21 +148,25 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "removed, ok",
|
name: "recreate quota, ok",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
eventstore: eventstoreExpect(
|
eventstore: eventstoreExpect(
|
||||||
t,
|
t,
|
||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"INSTANCE",
|
"INSTANCE",
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Now(),
|
quota.ChangeFrom(time.Now()),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
true,
|
quota.ChangeLimit(true),
|
||||||
nil,
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
@ -171,25 +181,28 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"INSTANCE",
|
"INSTANCE",
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota2", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
true,
|
quota.ChangeLimit(true),
|
||||||
nil,
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", quota.NewAddQuotaUnitUniqueConstraint(quota.RequestsAllAuthenticated)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota2"),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
addQuota: &AddQuota{
|
setQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
ResetInterval: 30 * 24 * time.Hour,
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
@ -214,32 +227,35 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
[]*repository.Event{
|
[]*repository.Event{
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"INSTANCE",
|
"INSTANCE",
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
true,
|
quota.ChangeLimit(true),
|
||||||
[]*quota.AddedEventNotification{
|
quota.ChangeNotifications(
|
||||||
{
|
[]*quota.SetEventNotification{{
|
||||||
ID: "notification1",
|
ID: "notification1",
|
||||||
Percent: 20,
|
Percent: 20,
|
||||||
Repeat: false,
|
Repeat: false,
|
||||||
CallURL: "https://url.com",
|
CallURL: "https://url.com",
|
||||||
},
|
}},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", quota.NewAddQuotaUnitUniqueConstraint(quota.RequestsAllAuthenticated)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1", "notification1"),
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1", "notification1"),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
addQuota: &AddQuota{
|
setQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
ResetInterval: 30 * 24 * time.Hour,
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
@ -267,7 +283,288 @@ func TestQuota_AddQuota(t *testing.T) {
|
|||||||
eventstore: tt.fields.eventstore,
|
eventstore: tt.fields.eventstore,
|
||||||
idGenerator: tt.fields.idGenerator,
|
idGenerator: tt.fields.idGenerator,
|
||||||
}
|
}
|
||||||
got, err := r.AddQuota(tt.args.ctx, tt.args.addQuota)
|
got, err := r.AddQuota(tt.args.ctx, tt.args.setQuota)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuota_SetQuota(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
idGenerator id.Generator
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
setQuota *SetQuota
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want *domain.ObjectDetails
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "already existing",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
quota.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
|
quota.ChangeAmount(1000),
|
||||||
|
quota.ChangeLimit(true),
|
||||||
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
setQuota: &SetQuota{
|
||||||
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
|
Amount: 1000,
|
||||||
|
Limit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "INSTANCE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create quota, validation fail",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
setQuota: &SetQuota{
|
||||||
|
Unit: "unimplemented",
|
||||||
|
From: time.Time{},
|
||||||
|
ResetInterval: 0,
|
||||||
|
Amount: 0,
|
||||||
|
Limit: false,
|
||||||
|
Notifications: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "QUOTA-OTeSh", ""))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create quota, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusherWithInstanceID(
|
||||||
|
"INSTANCE",
|
||||||
|
quota.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
|
quota.ChangeAmount(1000),
|
||||||
|
quota.ChangeLimit(true),
|
||||||
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
setQuota: &SetQuota{
|
||||||
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
|
Amount: 1000,
|
||||||
|
Limit: true,
|
||||||
|
Notifications: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "INSTANCE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recreate quota, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusherWithInstanceID(
|
||||||
|
"INSTANCE",
|
||||||
|
quota.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
|
quota.ChangeFrom(time.Now()),
|
||||||
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
|
quota.ChangeAmount(1000),
|
||||||
|
quota.ChangeLimit(true),
|
||||||
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusherWithInstanceID(
|
||||||
|
"INSTANCE",
|
||||||
|
quota.NewRemovedEvent(context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusherWithInstanceID(
|
||||||
|
"INSTANCE",
|
||||||
|
quota.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota2", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
|
quota.ChangeAmount(1000),
|
||||||
|
quota.ChangeLimit(true),
|
||||||
|
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota2"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
setQuota: &SetQuota{
|
||||||
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
|
Amount: 1000,
|
||||||
|
Limit: true,
|
||||||
|
Notifications: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "INSTANCE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create quota with notifications, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusherWithInstanceID(
|
||||||
|
"INSTANCE",
|
||||||
|
quota.NewSetEvent(
|
||||||
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
|
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
|
quota.ChangeAmount(1000),
|
||||||
|
quota.ChangeLimit(true),
|
||||||
|
quota.ChangeNotifications(
|
||||||
|
[]*quota.SetEventNotification{{
|
||||||
|
ID: "notification1",
|
||||||
|
Percent: 20,
|
||||||
|
Repeat: false,
|
||||||
|
CallURL: "https://url.com",
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1", "notification1"),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
|
||||||
|
setQuota: &SetQuota{
|
||||||
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
|
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
ResetInterval: 30 * 24 * time.Hour,
|
||||||
|
Amount: 1000,
|
||||||
|
Limit: true,
|
||||||
|
Notifications: QuotaNotifications{
|
||||||
|
{
|
||||||
|
Percent: 20,
|
||||||
|
Repeat: false,
|
||||||
|
CallURL: "https://url.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "INSTANCE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore,
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
}
|
||||||
|
got, err := r.SetQuota(tt.args.ctx, tt.args.setQuota)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
@ -325,14 +622,17 @@ func TestQuota_RemoveQuota(t *testing.T) {
|
|||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"INSTANCE",
|
"INSTANCE",
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Now(),
|
quota.ChangeFrom(time.Now()),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
true,
|
quota.ChangeLimit(true),
|
||||||
nil,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
@ -363,14 +663,17 @@ func TestQuota_RemoveQuota(t *testing.T) {
|
|||||||
expectFilter(
|
expectFilter(
|
||||||
eventFromEventPusherWithInstanceID(
|
eventFromEventPusherWithInstanceID(
|
||||||
"INSTANCE",
|
"INSTANCE",
|
||||||
quota.NewAddedEvent(context.Background(),
|
quota.NewSetEvent(
|
||||||
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
eventstore.NewBaseEventForPush(
|
||||||
|
context.Background(),
|
||||||
|
"a.NewAggregate("quota1", "INSTANCE").Aggregate,
|
||||||
|
quota.SetEventType,
|
||||||
|
),
|
||||||
QuotaRequestsAllAuthenticated.Enum(),
|
QuotaRequestsAllAuthenticated.Enum(),
|
||||||
time.Now(),
|
quota.ChangeFrom(time.Now()),
|
||||||
30*24*time.Hour,
|
quota.ChangeResetInterval(30*24*time.Hour),
|
||||||
1000,
|
quota.ChangeAmount(1000),
|
||||||
false,
|
quota.ChangeLimit(false),
|
||||||
nil,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -517,9 +820,9 @@ func TestQuota_QuotaNotification_validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQuota_AddQuota_validate(t *testing.T) {
|
func TestQuota_SetQuota_validate(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
addQuota *AddQuota
|
addQuota *SetQuota
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
err func(error) bool
|
err func(error) bool
|
||||||
@ -532,7 +835,7 @@ func TestQuota_AddQuota_validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "notification url parse failed",
|
name: "notification url parse failed",
|
||||||
args: args{
|
args: args{
|
||||||
addQuota: &AddQuota{
|
addQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Now(),
|
From: time.Now(),
|
||||||
ResetInterval: time.Minute * 10,
|
ResetInterval: time.Minute * 10,
|
||||||
@ -556,7 +859,7 @@ func TestQuota_AddQuota_validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "unit unimplemented",
|
name: "unit unimplemented",
|
||||||
args: args{
|
args: args{
|
||||||
addQuota: &AddQuota{
|
addQuota: &SetQuota{
|
||||||
Unit: "unimplemented",
|
Unit: "unimplemented",
|
||||||
From: time.Now(),
|
From: time.Now(),
|
||||||
ResetInterval: time.Minute * 10,
|
ResetInterval: time.Minute * 10,
|
||||||
@ -571,28 +874,10 @@ func TestQuota_AddQuota_validate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "amount 0",
|
|
||||||
args: args{
|
|
||||||
addQuota: &AddQuota{
|
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
|
||||||
From: time.Now(),
|
|
||||||
ResetInterval: time.Minute * 10,
|
|
||||||
Amount: 0,
|
|
||||||
Limit: true,
|
|
||||||
Notifications: nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
res: res{
|
|
||||||
err: func(err error) bool {
|
|
||||||
return errors.Is(err, caos_errors.ThrowInvalidArgument(nil, "QUOTA-hOKSJ", ""))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "reset interval under 1 min",
|
name: "reset interval under 1 min",
|
||||||
args: args{
|
args: args{
|
||||||
addQuota: &AddQuota{
|
addQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Now(),
|
From: time.Now(),
|
||||||
ResetInterval: time.Second * 10,
|
ResetInterval: time.Second * 10,
|
||||||
@ -610,7 +895,7 @@ func TestQuota_AddQuota_validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "validate, ok",
|
name: "validate, ok",
|
||||||
args: args{
|
args: args{
|
||||||
addQuota: &AddQuota{
|
addQuota: &SetQuota{
|
||||||
Unit: QuotaRequestsAllAuthenticated,
|
Unit: QuotaRequestsAllAuthenticated,
|
||||||
From: time.Now(),
|
From: time.Now(),
|
||||||
ResetInterval: time.Minute * 10,
|
ResetInterval: time.Minute * 10,
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
package crdb
|
package crdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||||
)
|
)
|
||||||
@ -14,8 +18,9 @@ type execOption func(*execConfig)
|
|||||||
type execConfig struct {
|
type execConfig struct {
|
||||||
tableName string
|
tableName string
|
||||||
|
|
||||||
args []interface{}
|
args []interface{}
|
||||||
err error
|
err error
|
||||||
|
ignoreNotFound bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithTableSuffix(name string) func(*execConfig) {
|
func WithTableSuffix(name string) func(*execConfig) {
|
||||||
@ -24,6 +29,12 @@ func WithTableSuffix(name string) func(*execConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithIgnoreNotFound() func(*execConfig) {
|
||||||
|
return func(o *execConfig) {
|
||||||
|
o.ignoreNotFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewCreateStatement(event eventstore.Event, values []handler.Column, opts ...execOption) *handler.Statement {
|
func NewCreateStatement(event eventstore.Event, values []handler.Column, opts ...execOption) *handler.Statement {
|
||||||
cols, params, args := columnsToQuery(values)
|
cols, params, args := columnsToQuery(values)
|
||||||
columnNames := strings.Join(cols, ", ")
|
columnNames := strings.Join(cols, ", ")
|
||||||
@ -436,7 +447,11 @@ func exec(config execConfig, q query, opts []execOption) Exec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := ex.Exec(q(config), config.args...); err != nil {
|
if _, err := ex.Exec(q(config), config.args...); err != nil {
|
||||||
return caos_errs.ThrowInternal(err, "CRDB-pKtsr", "exec failed")
|
if config.ignoreNotFound && errors.Is(err, sql.ErrNoRows) {
|
||||||
|
logging.WithError(err).Debugf("ignored not found: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return zitadel_errors.ThrowInternal(err, "CRDB-pKtsr", "exec failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/database"
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
"github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
|
||||||
@ -65,10 +65,10 @@ func newQuotaProjection(ctx context.Context, config crdb.StatementHandlerConfig)
|
|||||||
crdb.NewColumn(QuotaColumnID, crdb.ColumnTypeText),
|
crdb.NewColumn(QuotaColumnID, crdb.ColumnTypeText),
|
||||||
crdb.NewColumn(QuotaColumnInstanceID, crdb.ColumnTypeText),
|
crdb.NewColumn(QuotaColumnInstanceID, crdb.ColumnTypeText),
|
||||||
crdb.NewColumn(QuotaColumnUnit, crdb.ColumnTypeEnum),
|
crdb.NewColumn(QuotaColumnUnit, crdb.ColumnTypeEnum),
|
||||||
crdb.NewColumn(QuotaColumnAmount, crdb.ColumnTypeInt64),
|
crdb.NewColumn(QuotaColumnAmount, crdb.ColumnTypeInt64, crdb.Nullable()),
|
||||||
crdb.NewColumn(QuotaColumnFrom, crdb.ColumnTypeTimestamp),
|
crdb.NewColumn(QuotaColumnFrom, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||||
crdb.NewColumn(QuotaColumnInterval, crdb.ColumnTypeInterval),
|
crdb.NewColumn(QuotaColumnInterval, crdb.ColumnTypeInterval, crdb.Nullable()),
|
||||||
crdb.NewColumn(QuotaColumnLimit, crdb.ColumnTypeBool),
|
crdb.NewColumn(QuotaColumnLimit, crdb.ColumnTypeBool, crdb.Nullable()),
|
||||||
},
|
},
|
||||||
crdb.NewPrimaryKey(QuotaColumnInstanceID, QuotaColumnUnit),
|
crdb.NewPrimaryKey(QuotaColumnInstanceID, QuotaColumnUnit),
|
||||||
),
|
),
|
||||||
@ -118,31 +118,20 @@ func (q *quotaProjection) reducers() []handler.AggregateReducer {
|
|||||||
EventRedusers: []handler.EventReducer{
|
EventRedusers: []handler.EventReducer{
|
||||||
{
|
{
|
||||||
Event: quota.AddedEventType,
|
Event: quota.AddedEventType,
|
||||||
Reduce: q.reduceQuotaAdded,
|
Reduce: q.reduceQuotaSet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Event: quota.SetEventType,
|
||||||
|
Reduce: q.reduceQuotaSet,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Aggregate: quota.AggregateType,
|
|
||||||
EventRedusers: []handler.EventReducer{
|
|
||||||
{
|
{
|
||||||
Event: quota.RemovedEventType,
|
Event: quota.RemovedEventType,
|
||||||
Reduce: q.reduceQuotaRemoved,
|
Reduce: q.reduceQuotaRemoved,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Aggregate: quota.AggregateType,
|
|
||||||
EventRedusers: []handler.EventReducer{
|
|
||||||
{
|
{
|
||||||
Event: quota.NotificationDueEventType,
|
Event: quota.NotificationDueEventType,
|
||||||
Reduce: q.reduceQuotaNotificationDue,
|
Reduce: q.reduceQuotaNotificationDue,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Aggregate: quota.AggregateType,
|
|
||||||
EventRedusers: []handler.EventReducer{
|
|
||||||
{
|
{
|
||||||
Event: quota.NotifiedEventType,
|
Event: quota.NotifiedEventType,
|
||||||
Reduce: q.reduceQuotaNotified,
|
Reduce: q.reduceQuotaNotified,
|
||||||
@ -156,26 +145,53 @@ func (q *quotaProjection) reduceQuotaNotified(event eventstore.Event) (*handler.
|
|||||||
return crdb.NewNoOpStatement(event), nil
|
return crdb.NewNoOpStatement(event), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *quotaProjection) reduceQuotaAdded(event eventstore.Event) (*handler.Statement, error) {
|
func (q *quotaProjection) reduceQuotaSet(event eventstore.Event) (*handler.Statement, error) {
|
||||||
e, err := assertEvent[*quota.AddedEvent](event)
|
e, err := assertEvent[*quota.SetEvent](event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var statements []func(e eventstore.Event) crdb.Exec
|
||||||
|
|
||||||
createStatements := make([]func(e eventstore.Event) crdb.Exec, len(e.Notifications)+1)
|
// 1. Insert or update quota if the event has not only notification changes
|
||||||
createStatements[0] = crdb.AddCreateStatement(
|
quotaConflictColumns := []handler.Column{
|
||||||
[]handler.Column{
|
handler.NewCol(QuotaColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
handler.NewCol(QuotaColumnID, e.Aggregate().ID),
|
handler.NewCol(QuotaColumnUnit, e.Unit),
|
||||||
handler.NewCol(QuotaColumnInstanceID, e.Aggregate().InstanceID),
|
}
|
||||||
handler.NewCol(QuotaColumnUnit, e.Unit),
|
quotaUpdateCols := make([]handler.Column, 0, 4+1+len(quotaConflictColumns))
|
||||||
handler.NewCol(QuotaColumnAmount, e.Amount),
|
if e.Limit != nil {
|
||||||
handler.NewCol(QuotaColumnFrom, e.From),
|
quotaUpdateCols = append(quotaUpdateCols, handler.NewCol(QuotaColumnLimit, *e.Limit))
|
||||||
handler.NewCol(QuotaColumnInterval, e.ResetInterval),
|
}
|
||||||
handler.NewCol(QuotaColumnLimit, e.Limit),
|
if e.Amount != nil {
|
||||||
})
|
quotaUpdateCols = append(quotaUpdateCols, handler.NewCol(QuotaColumnAmount, *e.Amount))
|
||||||
for i := range e.Notifications {
|
}
|
||||||
notification := e.Notifications[i]
|
if e.From != nil {
|
||||||
createStatements[i+1] = crdb.AddCreateStatement(
|
quotaUpdateCols = append(quotaUpdateCols, handler.NewCol(QuotaColumnFrom, *e.From))
|
||||||
|
}
|
||||||
|
if e.ResetInterval != nil {
|
||||||
|
quotaUpdateCols = append(quotaUpdateCols, handler.NewCol(QuotaColumnInterval, *e.ResetInterval))
|
||||||
|
}
|
||||||
|
if len(quotaUpdateCols) > 0 {
|
||||||
|
// TODO: Add the quota ID to the primary key in a migration?
|
||||||
|
quotaUpdateCols = append(quotaUpdateCols, handler.NewCol(QuotaColumnID, e.Aggregate().ID))
|
||||||
|
quotaUpdateCols = append(quotaUpdateCols, quotaConflictColumns...)
|
||||||
|
statements = append(statements, crdb.AddUpsertStatement(quotaConflictColumns, quotaUpdateCols))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Delete existing notifications
|
||||||
|
if e.Notifications == nil {
|
||||||
|
return crdb.NewMultiStatement(e, statements...), nil
|
||||||
|
}
|
||||||
|
statements = append(statements, crdb.AddDeleteStatement(
|
||||||
|
[]handler.Condition{
|
||||||
|
handler.NewCond(QuotaNotificationColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
|
handler.NewCond(QuotaNotificationColumnUnit, e.Unit),
|
||||||
|
},
|
||||||
|
crdb.WithTableSuffix(quotaNotificationsTableSuffix),
|
||||||
|
))
|
||||||
|
notifications := *e.Notifications
|
||||||
|
for i := range notifications {
|
||||||
|
notification := notifications[i]
|
||||||
|
statements = append(statements, crdb.AddCreateStatement(
|
||||||
[]handler.Column{
|
[]handler.Column{
|
||||||
handler.NewCol(QuotaNotificationColumnInstanceID, e.Aggregate().InstanceID),
|
handler.NewCol(QuotaNotificationColumnInstanceID, e.Aggregate().InstanceID),
|
||||||
handler.NewCol(QuotaNotificationColumnUnit, e.Unit),
|
handler.NewCol(QuotaNotificationColumnUnit, e.Unit),
|
||||||
@ -185,10 +201,9 @@ func (q *quotaProjection) reduceQuotaAdded(event eventstore.Event) (*handler.Sta
|
|||||||
handler.NewCol(QuotaNotificationColumnRepeat, notification.Repeat),
|
handler.NewCol(QuotaNotificationColumnRepeat, notification.Repeat),
|
||||||
},
|
},
|
||||||
crdb.WithTableSuffix(quotaNotificationsTableSuffix),
|
crdb.WithTableSuffix(quotaNotificationsTableSuffix),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
return crdb.NewMultiStatement(e, statements...), nil
|
||||||
return crdb.NewMultiStatement(e, createStatements...), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *quotaProjection) reduceQuotaNotificationDue(event eventstore.Event) (*handler.Statement, error) {
|
func (q *quotaProjection) reduceQuotaNotificationDue(event eventstore.Event) (*handler.Statement, error) {
|
||||||
@ -207,6 +222,8 @@ func (q *quotaProjection) reduceQuotaNotificationDue(event eventstore.Event) (*h
|
|||||||
handler.NewCond(QuotaNotificationColumnID, e.ID),
|
handler.NewCond(QuotaNotificationColumnID, e.ID),
|
||||||
},
|
},
|
||||||
crdb.WithTableSuffix(quotaNotificationsTableSuffix),
|
crdb.WithTableSuffix(quotaNotificationsTableSuffix),
|
||||||
|
// The notification could have been removed in the meantime
|
||||||
|
crdb.WithIgnoreNotFound(),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +296,7 @@ func (q *quotaProjection) IncrementUsage(ctx context.Context, unit quota.Unit, i
|
|||||||
instanceID, unit, periodStart, count,
|
instanceID, unit, periodStart, count,
|
||||||
).Scan(&sum)
|
).Scan(&sum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.ThrowInternalf(err, "PROJ-SJL3h", "incrementing usage for unit %d failed for at least one quota period", unit)
|
return 0, zitadel_errors.ThrowInternalf(err, "PROJ-SJL3h", "incrementing usage for unit %d failed for at least one quota period", unit)
|
||||||
}
|
}
|
||||||
return sum, err
|
return sum, err
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func TestQuotasProjection_reduces(t *testing.T) {
|
|||||||
want wantReduce
|
want wantReduce
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "reduceQuotaAdded",
|
name: "reduceQuotaSet with added type",
|
||||||
args: args{
|
args: args{
|
||||||
event: getEvent(testEvent(
|
event: getEvent(testEvent(
|
||||||
repository.EventType(quota.AddedEventType),
|
repository.EventType(quota.AddedEventType),
|
||||||
@ -41,9 +41,9 @@ func TestQuotasProjection_reduces(t *testing.T) {
|
|||||||
"from": "2023-01-01T00:00:00Z",
|
"from": "2023-01-01T00:00:00Z",
|
||||||
"interval": 300000000000
|
"interval": 300000000000
|
||||||
}`),
|
}`),
|
||||||
), quota.AddedEventMapper),
|
), quota.SetEventMapper),
|
||||||
},
|
},
|
||||||
reduce: ("aProjection{}).reduceQuotaAdded,
|
reduce: ("aProjection{}).reduceQuotaSet,
|
||||||
want: wantReduce{
|
want: wantReduce{
|
||||||
aggregateType: eventstore.AggregateType("quota"),
|
aggregateType: eventstore.AggregateType("quota"),
|
||||||
sequence: 15,
|
sequence: 15,
|
||||||
@ -51,15 +51,15 @@ func TestQuotasProjection_reduces(t *testing.T) {
|
|||||||
executer: &testExecuter{
|
executer: &testExecuter{
|
||||||
executions: []execution{
|
executions: []execution{
|
||||||
{
|
{
|
||||||
expectedStmt: "INSERT INTO projections.quotas (id, instance_id, unit, amount, from_anchor, interval, limit_usage) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
expectedStmt: "INSERT INTO projections.quotas (limit_usage, amount, from_anchor, interval, id, instance_id, unit) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, unit) DO UPDATE SET (limit_usage, amount, from_anchor, interval, id) = (EXCLUDED.limit_usage, EXCLUDED.amount, EXCLUDED.from_anchor, EXCLUDED.interval, EXCLUDED.id)",
|
||||||
expectedArgs: []interface{}{
|
expectedArgs: []interface{}{
|
||||||
"agg-id",
|
true,
|
||||||
"instance-id",
|
|
||||||
quota.RequestsAllAuthenticated,
|
|
||||||
uint64(10),
|
uint64(10),
|
||||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
time.Minute * 5,
|
time.Minute * 5,
|
||||||
true,
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -67,7 +67,7 @@ func TestQuotasProjection_reduces(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "reduceQuotaAdded with notification",
|
name: "reduceQuotaAdded with added type and notification",
|
||||||
args: args{
|
args: args{
|
||||||
event: getEvent(testEvent(
|
event: getEvent(testEvent(
|
||||||
repository.EventType(quota.AddedEventType),
|
repository.EventType(quota.AddedEventType),
|
||||||
@ -87,9 +87,9 @@ func TestQuotasProjection_reduces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`),
|
}`),
|
||||||
), quota.AddedEventMapper),
|
), quota.SetEventMapper),
|
||||||
},
|
},
|
||||||
reduce: ("aProjection{}).reduceQuotaAdded,
|
reduce: ("aProjection{}).reduceQuotaSet,
|
||||||
want: wantReduce{
|
want: wantReduce{
|
||||||
aggregateType: eventstore.AggregateType("quota"),
|
aggregateType: eventstore.AggregateType("quota"),
|
||||||
sequence: 15,
|
sequence: 15,
|
||||||
@ -97,17 +97,126 @@ func TestQuotasProjection_reduces(t *testing.T) {
|
|||||||
executer: &testExecuter{
|
executer: &testExecuter{
|
||||||
executions: []execution{
|
executions: []execution{
|
||||||
{
|
{
|
||||||
expectedStmt: "INSERT INTO projections.quotas (id, instance_id, unit, amount, from_anchor, interval, limit_usage) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
expectedStmt: "INSERT INTO projections.quotas (limit_usage, amount, from_anchor, interval, id, instance_id, unit) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, unit) DO UPDATE SET (limit_usage, amount, from_anchor, interval, id) = (EXCLUDED.limit_usage, EXCLUDED.amount, EXCLUDED.from_anchor, EXCLUDED.interval, EXCLUDED.id)",
|
||||||
expectedArgs: []interface{}{
|
expectedArgs: []interface{}{
|
||||||
"agg-id",
|
true,
|
||||||
"instance-id",
|
|
||||||
quota.RequestsAllAuthenticated,
|
|
||||||
uint64(10),
|
uint64(10),
|
||||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
time.Minute * 5,
|
time.Minute * 5,
|
||||||
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.quotas_notifications WHERE (instance_id = $1) AND (unit = $2)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.quotas_notifications (instance_id, unit, id, call_url, percent, repeat) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
|
"id",
|
||||||
|
"url",
|
||||||
|
uint16(100),
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceQuotaSet with set type",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
repository.EventType(quota.SetEventType),
|
||||||
|
quota.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"unit": 1,
|
||||||
|
"amount": 10,
|
||||||
|
"limit": true,
|
||||||
|
"from": "2023-01-01T00:00:00Z",
|
||||||
|
"interval": 300000000000
|
||||||
|
}`),
|
||||||
|
), quota.SetEventMapper),
|
||||||
|
},
|
||||||
|
reduce: ("aProjection{}).reduceQuotaSet,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("quota"),
|
||||||
|
sequence: 15,
|
||||||
|
previousSequence: 10,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.quotas (limit_usage, amount, from_anchor, interval, id, instance_id, unit) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, unit) DO UPDATE SET (limit_usage, amount, from_anchor, interval, id) = (EXCLUDED.limit_usage, EXCLUDED.amount, EXCLUDED.from_anchor, EXCLUDED.interval, EXCLUDED.id)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
true,
|
||||||
|
uint64(10),
|
||||||
|
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Minute * 5,
|
||||||
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reduceQuotaAdded with set type and notification",
|
||||||
|
args: args{
|
||||||
|
event: getEvent(testEvent(
|
||||||
|
repository.EventType(quota.SetEventType),
|
||||||
|
quota.AggregateType,
|
||||||
|
[]byte(`{
|
||||||
|
"unit": 1,
|
||||||
|
"amount": 10,
|
||||||
|
"limit": true,
|
||||||
|
"from": "2023-01-01T00:00:00Z",
|
||||||
|
"interval": 300000000000,
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"id": "id",
|
||||||
|
"percent": 100,
|
||||||
|
"repeat": true,
|
||||||
|
"callURL": "url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`),
|
||||||
|
), quota.SetEventMapper),
|
||||||
|
},
|
||||||
|
reduce: ("aProjection{}).reduceQuotaSet,
|
||||||
|
want: wantReduce{
|
||||||
|
aggregateType: eventstore.AggregateType("quota"),
|
||||||
|
sequence: 15,
|
||||||
|
previousSequence: 10,
|
||||||
|
executer: &testExecuter{
|
||||||
|
executions: []execution{
|
||||||
|
{
|
||||||
|
expectedStmt: "INSERT INTO projections.quotas (limit_usage, amount, from_anchor, interval, id, instance_id, unit) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (instance_id, unit) DO UPDATE SET (limit_usage, amount, from_anchor, interval, id) = (EXCLUDED.limit_usage, EXCLUDED.amount, EXCLUDED.from_anchor, EXCLUDED.interval, EXCLUDED.id)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
true,
|
||||||
|
uint64(10),
|
||||||
|
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
time.Minute * 5,
|
||||||
|
"agg-id",
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectedStmt: "DELETE FROM projections.quotas_notifications WHERE (instance_id = $1) AND (unit = $2)",
|
||||||
|
expectedArgs: []interface{}{
|
||||||
|
"instance-id",
|
||||||
|
quota.RequestsAllAuthenticated,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
expectedStmt: "INSERT INTO projections.quotas_notifications (instance_id, unit, id, call_url, percent, repeat) VALUES ($1, $2, $3, $4, $5, $6)",
|
expectedStmt: "INSERT INTO projections.quotas_notifications (instance_id, unit, id, call_url, percent, repeat) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||||
expectedArgs: []interface{}{
|
expectedArgs: []interface{}{
|
||||||
|
@ -49,7 +49,8 @@ func (q *Queries) GetRemainingQuotaUsage(ctx context.Context, instanceID string,
|
|||||||
QuotaColumnLimit.identifier(): true,
|
QuotaColumnLimit.identifier(): true,
|
||||||
},
|
},
|
||||||
sq.Expr("age(" + QuotaPeriodColumnStart.identifier() + ") < " + QuotaColumnInterval.identifier()),
|
sq.Expr("age(" + QuotaPeriodColumnStart.identifier() + ") < " + QuotaColumnInterval.identifier()),
|
||||||
sq.Expr(QuotaPeriodColumnStart.identifier() + " < now()"),
|
sq.Expr(QuotaPeriodColumnStart.identifier() + " <= now()"),
|
||||||
|
sq.Expr(QuotaPeriodColumnStart.identifier() + " >= " + QuotaColumnFrom.identifier()),
|
||||||
}).
|
}).
|
||||||
ToSql()
|
ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,14 +74,14 @@ func prepareRemainingQuotaUsageQuery(ctx context.Context, db prepareDatabase) (s
|
|||||||
From(quotaPeriodsTable.identifier()).
|
From(quotaPeriodsTable.identifier()).
|
||||||
Join(join(QuotaColumnUnit, QuotaPeriodColumnUnit) + db.Timetravel(call.Took(ctx))).
|
Join(join(QuotaColumnUnit, QuotaPeriodColumnUnit) + db.Timetravel(call.Took(ctx))).
|
||||||
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*uint64, error) {
|
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*uint64, error) {
|
||||||
usage := new(uint64)
|
remaining := new(uint64)
|
||||||
err := row.Scan(usage)
|
err := row.Scan(remaining)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, zitadel_errors.ThrowNotFound(err, "QUERY-quiowi2", "Errors.Internal")
|
return nil, zitadel_errors.ThrowNotFound(err, "QUERY-quiowi2", "Errors.Internal")
|
||||||
}
|
}
|
||||||
return nil, zitadel_errors.ThrowInternal(err, "QUERY-81j1jn2", "Errors.Internal")
|
return nil, zitadel_errors.ThrowInternal(err, "QUERY-81j1jn2", "Errors.Internal")
|
||||||
}
|
}
|
||||||
return usage, nil
|
return remaining, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ const (
|
|||||||
UniqueQuotaNameType = "quota_units"
|
UniqueQuotaNameType = "quota_units"
|
||||||
eventTypePrefix = eventstore.EventType("quota.")
|
eventTypePrefix = eventstore.EventType("quota.")
|
||||||
AddedEventType = eventTypePrefix + "added"
|
AddedEventType = eventTypePrefix + "added"
|
||||||
|
SetEventType = eventTypePrefix + "set"
|
||||||
NotifiedEventType = eventTypePrefix + "notified"
|
NotifiedEventType = eventTypePrefix + "notified"
|
||||||
NotificationDueEventType = eventTypePrefix + "notificationdue"
|
NotificationDueEventType = eventTypePrefix + "notificationdue"
|
||||||
RemovedEventType = eventTypePrefix + "removed"
|
RemovedEventType = eventTypePrefix + "removed"
|
||||||
@ -43,65 +44,87 @@ func NewRemoveQuotaNameUniqueConstraint(unit Unit) *eventstore.EventUniqueConstr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddedEvent struct {
|
// SetEvent describes that a quota is added or modified and contains only changed properties
|
||||||
|
type SetEvent struct {
|
||||||
eventstore.BaseEvent `json:"-"`
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
Unit Unit `json:"unit"`
|
||||||
Unit Unit `json:"unit"`
|
From *time.Time `json:"from,omitempty"`
|
||||||
From time.Time `json:"from"`
|
ResetInterval *time.Duration `json:"interval,omitempty"`
|
||||||
ResetInterval time.Duration `json:"interval,omitempty"`
|
Amount *uint64 `json:"amount,omitempty"`
|
||||||
Amount uint64 `json:"amount"`
|
Limit *bool `json:"limit,omitempty"`
|
||||||
Limit bool `json:"limit"`
|
Notifications *[]*SetEventNotification `json:"notifications,omitempty"`
|
||||||
Notifications []*AddedEventNotification `json:"notifications,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddedEventNotification struct {
|
type SetEventNotification struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Percent uint16 `json:"percent"`
|
Percent uint16 `json:"percent"`
|
||||||
Repeat bool `json:"repeat,omitempty"`
|
Repeat bool `json:"repeat"`
|
||||||
CallURL string `json:"callUrl"`
|
CallURL string `json:"callUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AddedEvent) Data() interface{} {
|
func (e *SetEvent) Data() interface{} {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
func (e *SetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
return []*eventstore.EventUniqueConstraint{NewAddQuotaUnitUniqueConstraint(e.Unit)}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddedEvent(
|
func NewSetEvent(
|
||||||
ctx context.Context,
|
base *eventstore.BaseEvent,
|
||||||
aggregate *eventstore.Aggregate,
|
|
||||||
unit Unit,
|
unit Unit,
|
||||||
from time.Time,
|
changes ...QuotaChange,
|
||||||
resetInterval time.Duration,
|
) *SetEvent {
|
||||||
amount uint64,
|
changedEvent := &SetEvent{
|
||||||
limit bool,
|
BaseEvent: *base,
|
||||||
notifications []*AddedEventNotification,
|
Unit: unit,
|
||||||
) *AddedEvent {
|
}
|
||||||
return &AddedEvent{
|
for _, change := range changes {
|
||||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
change(changedEvent)
|
||||||
ctx,
|
}
|
||||||
aggregate,
|
return changedEvent
|
||||||
AddedEventType,
|
}
|
||||||
),
|
|
||||||
Unit: unit,
|
type QuotaChange func(*SetEvent)
|
||||||
From: from,
|
|
||||||
ResetInterval: resetInterval,
|
func ChangeAmount(amount uint64) QuotaChange {
|
||||||
Amount: amount,
|
return func(e *SetEvent) {
|
||||||
Limit: limit,
|
e.Amount = &amount
|
||||||
Notifications: notifications,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
func ChangeLimit(limit bool) QuotaChange {
|
||||||
e := &AddedEvent{
|
return func(e *SetEvent) {
|
||||||
|
e.Limit = &limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeFrom(from time.Time) QuotaChange {
|
||||||
|
return func(event *SetEvent) {
|
||||||
|
event.From = &from
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeResetInterval(interval time.Duration) QuotaChange {
|
||||||
|
return func(event *SetEvent) {
|
||||||
|
event.ResetInterval = &interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeNotifications(notifications []*SetEventNotification) QuotaChange {
|
||||||
|
return func(event *SetEvent) {
|
||||||
|
event.Notifications = ¬ifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||||
|
e := &SetEvent{
|
||||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.Unmarshal(event.Data, e)
|
err := json.Unmarshal(event.Data, e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.ThrowInternal(err, "QUOTA-4n8vs", "unable to unmarshal quota added")
|
return nil, errors.ThrowInternal(err, "QUOTA-kmIpI", "unable to unmarshal quota set")
|
||||||
}
|
}
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
|
@ -5,7 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func RegisterEventMappers(es *eventstore.Eventstore) {
|
func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||||
es.RegisterFilterEventMapper(AggregateType, AddedEventType, AddedEventMapper).
|
// AddedEventType is not emitted anymore.
|
||||||
|
// For ease of use, old events are directly mapped to SetEvent.
|
||||||
|
// This works, because the data structures are compatible.
|
||||||
|
es.RegisterFilterEventMapper(AggregateType, AddedEventType, SetEventMapper).
|
||||||
|
RegisterFilterEventMapper(AggregateType, SetEventType, SetEventMapper).
|
||||||
RegisterFilterEventMapper(AggregateType, RemovedEventType, RemovedEventMapper).
|
RegisterFilterEventMapper(AggregateType, RemovedEventType, RemovedEventMapper).
|
||||||
RegisterFilterEventMapper(AggregateType, NotificationDueEventType, NotificationDueEventMapper).
|
RegisterFilterEventMapper(AggregateType, NotificationDueEventType, NotificationDueEventMapper).
|
||||||
RegisterFilterEventMapper(AggregateType, NotifiedEventType, NotifiedEventMapper)
|
RegisterFilterEventMapper(AggregateType, NotifiedEventType, NotifiedEventMapper)
|
||||||
|
@ -361,6 +361,8 @@ service SystemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new quota
|
// Creates a new quota
|
||||||
|
// Returns an error if the quota already exists for the specified unit
|
||||||
|
// Deprecated: use SetQuota instead
|
||||||
rpc AddQuota(AddQuotaRequest) returns (AddQuotaResponse) {
|
rpc AddQuota(AddQuotaRequest) returns (AddQuotaResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/instances/{instance_id}/quotas"
|
post: "/instances/{instance_id}/quotas"
|
||||||
@ -372,6 +374,19 @@ service SystemService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets quota configuration properties
|
||||||
|
// Creates a new quota if it doesn't exist for the specified unit
|
||||||
|
rpc SetQuota(SetQuotaRequest) returns (SetQuotaResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
put: "/instances/{instance_id}/quotas"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "authenticated";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Removes a quota
|
// Removes a quota
|
||||||
rpc RemoveQuota(RemoveQuotaRequest) returns (RemoveQuotaResponse) {
|
rpc RemoveQuota(RemoveQuotaRequest) returns (RemoveQuotaResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
@ -598,6 +613,54 @@ message AddQuotaResponse {
|
|||||||
zitadel.v1.ObjectDetails details = 1;
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetQuotaRequest {
|
||||||
|
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
|
// the unit a quota should be imposed on
|
||||||
|
zitadel.quota.v1.Unit unit = 2 [
|
||||||
|
(validate.rules).enum = {defined_only: true, not_in: [0]},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "the unit a quota should be imposed on";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// the starting time from which the current quota period is calculated from. This is relevant for querying the current usage.
|
||||||
|
google.protobuf.Timestamp from = 3 [
|
||||||
|
(validate.rules).timestamp.required = true,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"2019-04-01T08:45:00.000000Z\"";
|
||||||
|
description: "the starting time from which the current quota period is calculated from. This is relevant for querying the current usage.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// the quota periods duration
|
||||||
|
google.protobuf.Duration reset_interval = 4 [
|
||||||
|
(validate.rules).duration.required = true,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "the quota periods duration";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// the quota amount of units
|
||||||
|
uint64 amount = 5 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "the quota amount of units";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// whether ZITADEL should block further usage when the configured amount is used
|
||||||
|
bool limit = 6 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "whether ZITADEL should block further usage when the configured amount is used";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// the handlers, ZITADEL executes when certain quota percentages are reached
|
||||||
|
repeated zitadel.quota.v1.Notification notifications = 7 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "the handlers, ZITADEL executes when certain quota percentages are reached";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetQuotaResponse {
|
||||||
|
zitadel.v1.ObjectDetails details = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message RemoveQuotaRequest {
|
message RemoveQuotaRequest {
|
||||||
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||||
zitadel.quota.v1.Unit unit = 2;
|
zitadel.quota.v1.Unit unit = 2;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user