mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-01 15:53:42 +00:00
feat(api): feature flags (#7356)
* feat(api): feature API proto definitions * update proto based on discussion with @livio-a * cleanup old feature flag stuff * authz instance queries * align defaults * projection definitions * define commands and event reducers * implement system and instance setter APIs * api getter implementation * unit test repository package * command unit tests * unit test Get queries * grpc converter unit tests * migrate the V1 features * migrate oidc to dynamic features * projection unit test * fix instance by host * fix instance by id data type in sql * fix linting errors * add system projection test * fix behavior inversion * resolve proto file comments * rename SystemDefaultLoginInstanceEventType to SystemLoginDefaultOrgEventType so it's consistent with the instance level event * use write models and conditional set events * system features integration tests * instance features integration tests * error on empty request * documentation entry * typo in feature.proto * fix start unit tests * solve linting error on key case switch * remove system defaults after discussion with @eliobischof * fix system feature projection * resolve comments in defaults.yaml --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/i18n"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/limits"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
@@ -110,7 +109,7 @@ type InstanceSetup struct {
|
||||
SMTPConfiguration *smtp.Config
|
||||
OIDCSettings *OIDCSettings
|
||||
Quotas *SetQuotas
|
||||
Features map[domain.Feature]any
|
||||
Features *InstanceFeatures
|
||||
Limits *SetLimits
|
||||
Restrictions *SetRestrictions
|
||||
}
|
||||
@@ -313,9 +312,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
||||
setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain)
|
||||
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
|
||||
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
|
||||
if err := setupFeatures(c, &validations, setup.Features, instanceID); err != nil {
|
||||
return "", "", nil, nil, err
|
||||
}
|
||||
setupFeatures(&validations, setup.Features, instanceID)
|
||||
setupLimits(c, &validations, limitsAgg, setup.Limits)
|
||||
setupRestrictions(c, &validations, restrictionsAgg, setup.Restrictions)
|
||||
|
||||
@@ -368,20 +365,8 @@ func setupQuotas(commands *Commands, validations *[]preparation.Validation, setQ
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupFeatures(commands *Commands, validations *[]preparation.Validation, enableFeatures map[domain.Feature]any, instanceID string) error {
|
||||
for f, value := range enableFeatures {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
wm, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*validations = append(*validations, prepareSetFeature(wm, feature.Boolean{Boolean: v}, commands.idGenerator))
|
||||
default:
|
||||
return zerrors.ThrowInvalidArgument(nil, "INST-GE4tg", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func setupFeatures(validations *[]preparation.Validation, features *InstanceFeatures, instanceID string) {
|
||||
*validations = append(*validations, prepareSetFeatures(instanceID, features))
|
||||
}
|
||||
|
||||
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func (c *Commands) SetBooleanInstanceFeature(ctx context.Context, f domain.Feature, value bool) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
writeModel, err := NewInstanceFeatureWriteModel[feature.Boolean](instanceID, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
|
||||
prepareSetFeature(writeModel, feature.Boolean{Boolean: value}, c.idGenerator))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(&writeModel.FeatureWriteModel.WriteModel), nil
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(pushedEvents), nil
|
||||
}
|
||||
|
||||
func prepareSetFeature[T feature.SetEventType](writeModel *InstanceFeatureWriteModel[T], value T, idGenerator id.Generator) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
if !writeModel.feature.IsAFeature() || writeModel.feature == domain.FeatureUnspecified {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "FEAT-JK3td", "Errors.Feature.NotExisting")
|
||||
}
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err = writeModel.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
writeModel.AggregateID, err = idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
setEvent, err := writeModel.Set(ctx, value)
|
||||
if err != nil || setEvent == nil {
|
||||
return nil, err
|
||||
}
|
||||
return []eventstore.Command{setEvent}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type FeatureWriteModel[T feature.SetEventType] struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
feature domain.Feature
|
||||
|
||||
Value T
|
||||
}
|
||||
|
||||
func NewFeatureWriteModel[T feature.SetEventType](instanceID, resourceOwner string, feature domain.Feature) (*FeatureWriteModel[T], error) {
|
||||
wm := &FeatureWriteModel[T]{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
InstanceID: instanceID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
feature: feature,
|
||||
}
|
||||
if wm.Value.FeatureType() != feature.Type() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue")
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
func (wm *FeatureWriteModel[T]) Set(ctx context.Context, value T) (event *feature.SetEvent[T], err error) {
|
||||
if wm.Value == value {
|
||||
return nil, nil
|
||||
}
|
||||
return feature.NewSetEvent[T](
|
||||
ctx,
|
||||
&feature.NewAggregate(wm.AggregateID, wm.ResourceOwner).Aggregate,
|
||||
wm.eventType(),
|
||||
value,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(feature.AggregateType).
|
||||
EventTypes(wm.eventType()).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature.SetEvent[T]:
|
||||
wm.Value = e.Value
|
||||
default:
|
||||
return zerrors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported")
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *FeatureWriteModel[T]) eventType() eventstore.EventType {
|
||||
return feature.EventTypeFromFeature(wm.feature)
|
||||
}
|
||||
|
||||
type InstanceFeatureWriteModel[T feature.SetEventType] struct {
|
||||
FeatureWriteModel[T]
|
||||
}
|
||||
|
||||
func NewInstanceFeatureWriteModel[T feature.SetEventType](instanceID string, feature domain.Feature) (*InstanceFeatureWriteModel[T], error) {
|
||||
wm, err := NewFeatureWriteModel[T](instanceID, instanceID, feature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InstanceFeatureWriteModel[T]{
|
||||
FeatureWriteModel: *wm,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetBooleanInstanceFeature(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f domain.Feature
|
||||
value bool
|
||||
}
|
||||
type res struct {
|
||||
details *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"unknown feature",
|
||||
fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureUnspecified,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "FEAT-AS4k1", "Errors.Feature.InvalidValue"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"wrong type",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
// as there's currently no other [feature.SetEventType] than [feature.Boolean],
|
||||
// we need to use a completely other event type to demonstrate the behaviour
|
||||
instance.NewInstanceAddedEvent(context.Background(), &instance.NewAggregate("instanceID").Aggregate,
|
||||
"instance",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "FEAT-SDfjk", "Errors.Feature.TypeNotSupported"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"first set",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "featureID"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"update flag",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: false},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: false,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"no change",
|
||||
fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
feature.NewSetEvent[feature.Boolean](context.Background(), &feature.NewAggregate("featureID", "instanceID").Aggregate,
|
||||
feature.EventTypeFromFeature(domain.FeatureLoginDefaultOrg),
|
||||
feature.Boolean{Boolean: true},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
f: domain.FeatureLoginDefaultOrg,
|
||||
value: true,
|
||||
},
|
||||
res{
|
||||
details: &domain.ObjectDetails{
|
||||
ResourceOwner: "instanceID",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.SetBooleanInstanceFeature(tt.args.ctx, tt.args.f, tt.args.value)
|
||||
assert.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.details, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
69
internal/command/instance_features.go
Normal file
69
internal/command/instance_features.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type InstanceFeatures struct {
|
||||
LoginDefaultOrg *bool
|
||||
TriggerIntrospectionProjections *bool
|
||||
LegacyIntrospection *bool
|
||||
}
|
||||
|
||||
func (m *InstanceFeatures) isEmpty() bool {
|
||||
return m.LoginDefaultOrg == nil &&
|
||||
m.TriggerIntrospectionProjections == nil &&
|
||||
m.LegacyIntrospection == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||
if f.isEmpty() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vigh1", "Errors.NoChangesFound")
|
||||
}
|
||||
wm := NewInstanceFeaturesWriteModel(authz.GetInstance(ctx).InstanceID())
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds := wm.setCommands(ctx, f)
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
|
||||
func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
return wm.setCommands(ctx, f), nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
instanceID := authz.GetInstance(ctx).InstanceID()
|
||||
wm := NewInstanceFeaturesWriteModel(instanceID)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.isEmpty() {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
aggregate := feature_v2.NewAggregate(instanceID, instanceID)
|
||||
events, err := c.eventstore.Push(ctx, feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
93
internal/command/instance_features_model.go
Normal file
93
internal/command/instance_features_model.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type InstanceFeaturesWriteModel struct {
|
||||
*eventstore.WriteModel
|
||||
InstanceFeatures
|
||||
}
|
||||
|
||||
func NewInstanceFeaturesWriteModel(instanceID string) *InstanceFeaturesWriteModel {
|
||||
m := &InstanceFeaturesWriteModel{
|
||||
WriteModel: &eventstore.WriteModel{
|
||||
AggregateID: instanceID,
|
||||
ResourceOwner: instanceID,
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v1.SetEvent[feature_v1.Boolean]:
|
||||
err = m.reduceBoolFeature(
|
||||
feature_v1.DefaultLoginInstanceEventToV2(e),
|
||||
)
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v2.InstanceResetEventType,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) reduceReset() {
|
||||
m.LoginDefaultOrg = nil
|
||||
m.TriggerIntrospectionProjections = nil
|
||||
m.LegacyIntrospection = nil
|
||||
}
|
||||
|
||||
func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
_, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
m.LoginDefaultOrg = &event.Value
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
m.TriggerIntrospectionProjections = &event.Value
|
||||
case feature.KeyLegacyIntrospection:
|
||||
m.LegacyIntrospection = &event.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *InstanceFeatures) []eventstore.Command {
|
||||
aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.InstanceLoginDefaultOrgEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.InstanceTriggerIntrospectionProjectionsEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType)
|
||||
return cmds
|
||||
}
|
||||
323
internal/command/instance_features_test.go
Normal file
323
internal/command/instance_features_test.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
feature_v1 "github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f *InstanceFeatures
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "all nil, No Change",
|
||||
eventstore: expectEventstore(),
|
||||
args: args{ctx, &InstanceFeatures{}},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-Vigh1", "Errors.NoChangesFound"),
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg, update from v1",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v1.NewSetEvent[feature_v1.Boolean](
|
||||
ctx, &eventstore.Aggregate{
|
||||
ID: "instance1",
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
feature_v1.DefaultLoginInstanceEventType,
|
||||
feature_v1.Boolean{
|
||||
Boolean: false,
|
||||
},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set TriggerIntrospectionProjections",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LegacyIntrospection",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "set all",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set only updated",
|
||||
eventstore: expectEventstore(
|
||||
// throw in some set events, reset and set again.
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLegacyIntrospectionEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{ctx, &InstanceFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.SetInstanceFeatures(tt.args.ctx, tt.args.f)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ResetInstanceFeatures(t *testing.T) {
|
||||
ctx := authz.WithInstanceID(context.Background(), "instance1")
|
||||
aggregate := feature_v2.NewAggregate("instance1", "instance1")
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType),
|
||||
),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewResetEvent(ctx, aggregate, feature_v2.InstanceResetEventType),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change after previous reset",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
ctx, aggregate,
|
||||
feature_v2.InstanceResetEventType,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change without previous events",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "instance1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.ResetInstanceFeatures(ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@@ -215,6 +216,10 @@ func (m *mockInstance) SecurityPolicyAllowedOrigins() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockInstance) Features() feature.Features {
|
||||
return feature.Features{}
|
||||
}
|
||||
|
||||
func newMockPermissionCheckAllowed() domain.PermissionCheck {
|
||||
return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
|
||||
return nil
|
||||
|
||||
56
internal/command/system_features.go
Normal file
56
internal/command/system_features.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type SystemFeatures struct {
|
||||
LoginDefaultOrg *bool
|
||||
TriggerIntrospectionProjections *bool
|
||||
LegacyIntrospection *bool
|
||||
}
|
||||
|
||||
func (m *SystemFeatures) isEmpty() bool {
|
||||
return m.LoginDefaultOrg == nil &&
|
||||
m.TriggerIntrospectionProjections == nil &&
|
||||
m.LegacyIntrospection == nil
|
||||
}
|
||||
|
||||
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
|
||||
if f.isEmpty() {
|
||||
return nil, zerrors.ThrowInternal(nil, "COMMAND-Oop8a", "Errors.NoChangesFound")
|
||||
}
|
||||
wm := NewSystemFeaturesWriteModel()
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds := wm.setCommands(ctx, f)
|
||||
if len(cmds) == 0 {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ResetSystemFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
|
||||
wm := NewSystemFeaturesWriteModel()
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wm.isEmpty() {
|
||||
return writeModelToObjectDetails(wm.WriteModel), nil
|
||||
}
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
events, err := c.eventstore.Push(ctx, feature_v2.NewResetEvent(ctx, aggregate, feature_v2.SystemResetEventType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pushedEventsToObjectDetails(events), nil
|
||||
}
|
||||
94
internal/command/system_features_model.go
Normal file
94
internal/command/system_features_model.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
)
|
||||
|
||||
type SystemFeaturesWriteModel struct {
|
||||
*eventstore.WriteModel
|
||||
SystemFeatures
|
||||
}
|
||||
|
||||
func NewSystemFeaturesWriteModel() *SystemFeaturesWriteModel {
|
||||
m := &SystemFeaturesWriteModel{
|
||||
WriteModel: &eventstore.WriteModel{
|
||||
AggregateID: "SYSTEM",
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) Reduce() (err error) {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *feature_v2.ResetEvent:
|
||||
m.reduceReset()
|
||||
case *feature_v2.SetEvent[bool]:
|
||||
err = m.reduceBoolFeature(e)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AwaitOpenTransactions().
|
||||
AddQuery().
|
||||
AggregateTypes(feature_v2.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
feature_v2.SystemResetEventType,
|
||||
feature_v2.SystemLoginDefaultOrgEventType,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
|
||||
feature_v2.SystemLegacyIntrospectionEventType,
|
||||
).
|
||||
Builder().ResourceOwner(m.ResourceOwner)
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) reduceReset() {
|
||||
m.LoginDefaultOrg = nil
|
||||
m.TriggerIntrospectionProjections = nil
|
||||
m.LegacyIntrospection = nil
|
||||
}
|
||||
|
||||
func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
|
||||
_, key, err := event.FeatureInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch key {
|
||||
case feature.KeyUnspecified:
|
||||
return nil
|
||||
case feature.KeyLoginDefaultOrg:
|
||||
m.LoginDefaultOrg = &event.Value
|
||||
case feature.KeyTriggerIntrospectionProjections:
|
||||
m.TriggerIntrospectionProjections = &event.Value
|
||||
case feature.KeyLegacyIntrospection:
|
||||
m.LegacyIntrospection = &event.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFeatures) []eventstore.Command {
|
||||
aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner)
|
||||
cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.SystemLoginDefaultOrgEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.SystemTriggerIntrospectionProjectionsEventType)
|
||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType)
|
||||
return cmds
|
||||
}
|
||||
|
||||
func appendFeatureUpdate[T comparable](ctx context.Context, cmds []eventstore.Command, aggregate *feature_v2.Aggregate, oldValue, newValue *T, eventType eventstore.EventType) []eventstore.Command {
|
||||
if newValue != nil && (oldValue == nil || *oldValue != *newValue) {
|
||||
cmds = append(cmds, feature_v2.NewSetEvent[T](ctx, aggregate, eventType, *newValue))
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
290
internal/command/system_features_test.go
Normal file
290
internal/command/system_features_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature/feature_v2"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_SetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
f *SystemFeatures
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "all nil, No Change",
|
||||
eventstore: expectEventstore(),
|
||||
args: args{context.Background(), &SystemFeatures{}},
|
||||
wantErr: zerrors.ThrowInternal(nil, "COMMAND-Oop8a", "Errors.NoChangesFound"),
|
||||
},
|
||||
{
|
||||
name: "set LoginDefaultOrg",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set TriggerIntrospectionProjections",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
TriggerIntrospectionProjections: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set LegacyIntrospection",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "set all",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set only updated",
|
||||
eventstore: expectEventstore(
|
||||
// throw in some set events, reset and set again.
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, false,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLegacyIntrospectionEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
),
|
||||
feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemTriggerIntrospectionProjectionsEventType, false,
|
||||
),
|
||||
),
|
||||
),
|
||||
args: args{context.Background(), &SystemFeatures{
|
||||
LoginDefaultOrg: gu.Ptr(true),
|
||||
TriggerIntrospectionProjections: gu.Ptr(false),
|
||||
LegacyIntrospection: gu.Ptr(true),
|
||||
}},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.SetSystemFeatures(tt.args.ctx, tt.args.f)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_ResetSystemFeatures(t *testing.T) {
|
||||
aggregate := feature_v2.NewAggregate("SYSTEM", "SYSTEM")
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(*testing.T) *eventstore.Eventstore
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPushFailed(io.ErrClosedPipe,
|
||||
feature_v2.NewResetEvent(context.Background(), aggregate, feature_v2.SystemResetEventType),
|
||||
),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
feature_v2.NewResetEvent(context.Background(), aggregate, feature_v2.SystemResetEventType),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change after previous reset",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(feature_v2.NewSetEvent[bool](
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemLoginDefaultOrgEventType, true,
|
||||
)),
|
||||
eventFromEventPusher(feature_v2.NewResetEvent(
|
||||
context.Background(), aggregate,
|
||||
feature_v2.SystemResetEventType,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change without previous events",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "SYSTEM",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := c.ResetSystemFeatures(context.Background())
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user