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 e72f4d7fa1.

* 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
This commit is contained in:
Elio Bischof
2023-09-22 11:37:16 +02:00
committed by GitHub
parent e6d273b328
commit ae1af6bc8c
20 changed files with 1385 additions and 318 deletions

View File

@@ -110,7 +110,7 @@ type InstanceSetup struct {
RefreshTokenExpiration time.Duration
}
Quotas *struct {
Items []*AddQuota
Items []*SetQuota
}
}
@@ -283,7 +283,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
if err != nil {
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))
}
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"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(
ctx context.Context,
q *AddQuota,
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
}
if wm.active {
if wm.AggregateID != "" {
return nil, errors.ThrowAlreadyExists(nil, "COMMAND-WDfFf", "Errors.Quota.AlreadyExists")
}
aggregateId, err := c.idGenerator.Next()
if err != nil {
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 {
return nil, err
}
@@ -65,23 +64,52 @@ func (c *Commands) AddQuota(
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) {
instanceId := authz.GetInstance(ctx).InstanceID()
wm, err := c.getQuotaWriteModel(ctx, instanceId, instanceId, unit.Enum())
if err != nil {
return nil, err
}
if !wm.active {
if wm.AggregateID == "" {
return nil, errors.ThrowNotFound(nil, "COMMAND-WDfFf", "Errors.Quota.NotFound")
}
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...)
if err != nil {
return nil, err
@@ -104,6 +132,16 @@ type QuotaNotification struct {
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
func (q *QuotaNotification) validate() error {
@@ -111,94 +149,51 @@ func (q *QuotaNotification) validate() error {
if err != nil {
return errors.ThrowInvalidArgument(err, "QUOTA-bZ0Fj", "Errors.Quota.Invalid.CallURL")
}
if !u.IsAbs() || u.Host == "" {
return errors.ThrowInvalidArgument(nil, "QUOTA-HAYmN", "Errors.Quota.Invalid.CallURL")
}
if q.Percent < 1 {
return errors.ThrowInvalidArgument(nil, "QUOTA-pBfjq", "Errors.Quota.Invalid.Percent")
}
return nil
}
func (q *QuotaNotifications) toAddedEventNotifications(idGenerator id.Generator) ([]*quota.AddedEventNotification, 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] = &quota.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 {
func (q *SetQuota) validate() error {
for _, notification := range q.Notifications {
if err := notification.validate(); err != nil {
return err
}
}
if q.Unit.Enum() == quota.Unimplemented {
return errors.ThrowInvalidArgument(nil, "QUOTA-OTeSh", "Errors.Quota.Invalid.Unimplemented")
}
if q.Amount < 1 {
if q.Amount < 0 {
return errors.ThrowInvalidArgument(nil, "QUOTA-hOKSJ", "Errors.Quota.Invalid.Amount")
}
if q.ResetInterval < time.Minute {
return errors.ThrowInvalidArgument(nil, "QUOTA-R5otd", "Errors.Quota.Invalid.ResetInterval")
}
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) {
if err := q.validate(); err != nil {
return nil, err
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (cmd []eventstore.Command, err error) {
notifications, err := q.Notifications.toAddedEventNotifications(c.idGenerator)
if err != nil {
changes, err := wm.NewChanges(c.idGenerator, createNew, q.Amount, q.From, q.ResetInterval, q.Limit, q.Notifications...)
if len(changes) == 0 {
return nil, err
}
return []eventstore.Command{quota.NewAddedEvent(
ctx,
&a.Aggregate,
return []eventstore.Command{quota.NewSetEvent(
eventstore.NewBaseEventForPush(
ctx,
&a.Aggregate,
quota.SetEventType,
),
q.Unit.Enum(),
q.From,
q.ResetInterval,
q.Amount,
q.Limit,
notifications,
changes...,
)}, err
},
nil

View File

@@ -1,14 +1,26 @@
package command
import (
"errors"
"fmt"
"slices"
"time"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/quota"
)
type quotaWriteModel struct {
eventstore.WriteModel
unit quota.Unit
active bool
rollingAggregateID string
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
@@ -30,6 +42,7 @@ func (wm *quotaWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(quota.AggregateType).
EventTypes(
quota.AddedEventType,
quota.SetEventType,
quota.RemovedEventType,
).EventData(map[string]interface{}{"unit": wm.unit})
@@ -38,15 +51,137 @@ func (wm *quotaWriteModel) Query() *eventstore.SearchQueryBuilder {
func (wm *quotaWriteModel) Reduce() error {
for _, event := range wm.Events {
wm.ChangeDate = event.CreationDate()
switch e := event.(type) {
case *quota.AddedEvent:
wm.AggregateID = e.Aggregate().ID
wm.ChangeDate = e.CreationDate()
wm.active = true
case *quota.SetEvent:
wm.rollingAggregateID = e.Aggregate().ID
if e.Amount != nil {
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:
wm.AggregateID = e.Aggregate().ID
wm.active = false
wm.rollingAggregateID = ""
}
}
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] = &quota.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
}

View 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 := &quotaWriteModel{
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(),
&quota.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 }

View File

@@ -25,7 +25,7 @@ func TestQuota_AddQuota(t *testing.T) {
}
type args struct {
ctx context.Context
addQuota *AddQuota
setQuota *SetQuota
}
type res struct {
want *domain.ObjectDetails
@@ -44,14 +44,18 @@ func TestQuota_AddQuota(t *testing.T) {
t,
expectFilter(
eventFromEventPusher(
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Now(),
30*24*time.Hour,
1000,
false,
nil,
quota.ChangeFrom(time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC)),
quota.ChangeResetInterval(30*24*time.Hour),
quota.ChangeAmount(1000),
quota.ChangeLimit(false),
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
),
),
),
@@ -59,13 +63,12 @@ func TestQuota_AddQuota(t *testing.T) {
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
addQuota: &AddQuota{
setQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Time{},
ResetInterval: 0,
Amount: 0,
Limit: false,
Notifications: nil,
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
ResetInterval: 30 * 24 * time.Hour,
Amount: 1000,
Limit: true,
},
},
res: res{
@@ -83,7 +86,7 @@ func TestQuota_AddQuota(t *testing.T) {
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
addQuota: &AddQuota{
setQuota: &SetQuota{
Unit: "unimplemented",
From: time.Time{},
ResetInterval: 0,
@@ -108,25 +111,28 @@ func TestQuota_AddQuota(t *testing.T) {
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
30*24*time.Hour,
1000,
true,
nil,
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)),
),
),
},
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", quota.NewAddQuotaUnitUniqueConstraint(quota.RequestsAllAuthenticated)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
addQuota: &AddQuota{
setQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
ResetInterval: 30 * 24 * time.Hour,
@@ -142,21 +148,25 @@ func TestQuota_AddQuota(t *testing.T) {
},
},
{
name: "removed, ok",
name: "recreate quota, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Now(),
30*24*time.Hour,
1000,
true,
nil,
quota.ChangeFrom(time.Now()),
quota.ChangeResetInterval(30*24*time.Hour),
quota.ChangeAmount(1000),
quota.ChangeLimit(true),
quota.ChangeNotifications(make([]*quota.SetEventNotification, 0)),
),
),
eventFromEventPusherWithInstanceID(
@@ -171,25 +181,28 @@ func TestQuota_AddQuota(t *testing.T) {
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota2", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
30*24*time.Hour,
1000,
true,
nil,
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)),
),
),
},
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", quota.NewAddQuotaUnitUniqueConstraint(quota.RequestsAllAuthenticated)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota2"),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
addQuota: &AddQuota{
setQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
ResetInterval: 30 * 24 * time.Hour,
@@ -214,32 +227,35 @@ func TestQuota_AddQuota(t *testing.T) {
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
30*24*time.Hour,
1000,
true,
[]*quota.AddedEventNotification{
{
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",
},
},
}},
),
),
),
},
uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", quota.NewAddQuotaUnitUniqueConstraint(quota.RequestsAllAuthenticated)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "quota1", "notification1"),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
addQuota: &AddQuota{
setQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Date(2023, 9, 1, 0, 0, 0, 0, time.UTC),
ResetInterval: 30 * 24 * time.Hour,
@@ -267,7 +283,288 @@ func TestQuota_AddQuota(t *testing.T) {
eventstore: tt.fields.eventstore,
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(),
&quota.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(),
&quota.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(),
&quota.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(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
QuotaRequestsAllAuthenticated.Enum(),
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.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(),
&quota.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 {
assert.NoError(t, err)
}
@@ -325,14 +622,17 @@ func TestQuota_RemoveQuota(t *testing.T) {
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Now(),
30*24*time.Hour,
1000,
true,
nil,
quota.ChangeFrom(time.Now()),
quota.ChangeResetInterval(30*24*time.Hour),
quota.ChangeAmount(1000),
quota.ChangeLimit(true),
),
),
eventFromEventPusherWithInstanceID(
@@ -363,14 +663,17 @@ func TestQuota_RemoveQuota(t *testing.T) {
expectFilter(
eventFromEventPusherWithInstanceID(
"INSTANCE",
quota.NewAddedEvent(context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.NewSetEvent(
eventstore.NewBaseEventForPush(
context.Background(),
&quota.NewAggregate("quota1", "INSTANCE").Aggregate,
quota.SetEventType,
),
QuotaRequestsAllAuthenticated.Enum(),
time.Now(),
30*24*time.Hour,
1000,
false,
nil,
quota.ChangeFrom(time.Now()),
quota.ChangeResetInterval(30*24*time.Hour),
quota.ChangeAmount(1000),
quota.ChangeLimit(false),
),
),
),
@@ -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 {
addQuota *AddQuota
addQuota *SetQuota
}
type res struct {
err func(error) bool
@@ -532,7 +835,7 @@ func TestQuota_AddQuota_validate(t *testing.T) {
{
name: "notification url parse failed",
args: args{
addQuota: &AddQuota{
addQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Now(),
ResetInterval: time.Minute * 10,
@@ -556,7 +859,7 @@ func TestQuota_AddQuota_validate(t *testing.T) {
{
name: "unit unimplemented",
args: args{
addQuota: &AddQuota{
addQuota: &SetQuota{
Unit: "unimplemented",
From: time.Now(),
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",
args: args{
addQuota: &AddQuota{
addQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Now(),
ResetInterval: time.Second * 10,
@@ -610,7 +895,7 @@ func TestQuota_AddQuota_validate(t *testing.T) {
{
name: "validate, ok",
args: args{
addQuota: &AddQuota{
addQuota: &SetQuota{
Unit: QuotaRequestsAllAuthenticated,
From: time.Now(),
ResetInterval: time.Minute * 10,