mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-30 19:12:16 +00:00
feat(group): group service to create, update, and delete groups (#10455)
# Which Problems Are Solved This PR adds API definition and backend implementation for GroupService to manage user groups. # How the Problems Are Solved * API definition to create, update, retrieve, and delete groups is added * Command-side implementation to create, update, and delete user groups as part of the GroupV2 API is added # Additional Changes N/A # Additional Context - Related to #10089, #9702 (parent ticket) - User contribution: https://github.com/zitadel/zitadel/pull/9428/files - Additional functionalities to list/search user groups, add permissions, manage users in groups, group scopes will be added in subsequent PRs. - Also needs documentation, which will be added once the entire feature is available --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Livio Spring
parent
ee3654fe6f
commit
b81dedcaea
@@ -0,0 +1,160 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
repo "github.com/zitadel/zitadel/internal/repository/group"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type CreateGroup struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (g *CreateGroup) IsValid() error {
|
||||
if strings.TrimSpace(g.Name) == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "GROUP-m177lN", "Errors.Group.InvalidName")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateGroup creates a new user group in an organization
|
||||
func (c *Commands) CreateGroup(ctx context.Context, group *CreateGroup) (details *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
// todo: check permissions
|
||||
|
||||
if err = group.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if group.ResourceOwner == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "CMDGRP-msc0Tt", "Errors.Group.MissingOrganizationID")
|
||||
}
|
||||
// check whether the organization where the group should be created exists
|
||||
err = c.checkOrgExists(ctx, group.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "CMDGRP-j1mH8l", "Errors.Org.NotFound")
|
||||
}
|
||||
|
||||
// create a unique group ID if not provided
|
||||
if group.AggregateID == "" {
|
||||
group.AggregateID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// check if a group with the same ID already exists
|
||||
groupWriteModel, err := c.getGroupWriteModelByID(ctx, group.AggregateID, group.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupWriteModel.State.Exists() {
|
||||
return nil, zerrors.ThrowAlreadyExists(nil, "CMDGRP-shRut3", "Errors.Group.AlreadyExists")
|
||||
}
|
||||
|
||||
err = c.pushAppendAndReduce(ctx,
|
||||
groupWriteModel,
|
||||
repo.NewGroupAddedEvent(ctx,
|
||||
GroupAggregateFromWriteModel(ctx, &groupWriteModel.WriteModel),
|
||||
group.Name,
|
||||
group.Description,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&groupWriteModel.WriteModel), nil
|
||||
}
|
||||
|
||||
type UpdateGroup struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Name *string
|
||||
Description *string
|
||||
}
|
||||
|
||||
func (g *UpdateGroup) IsValid() error {
|
||||
if g.Name != nil && strings.TrimSpace(*g.Name) == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "GROUP-dUNd3r", "Errors.Group.InvalidName")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateGroup updates a user group in an organization
|
||||
func (c *Commands) UpdateGroup(ctx context.Context, groupUpdate *UpdateGroup) (details *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if err = groupUpdate.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// todo: check permissions
|
||||
existingGroup, err := c.getGroupWriteModelByID(ctx, groupUpdate.AggregateID, groupUpdate.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existingGroup.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "CMDGRP-b33zly", "Errors.Group.NotFound")
|
||||
}
|
||||
|
||||
changedEvent := existingGroup.NewChangedEvent(
|
||||
ctx,
|
||||
GroupAggregateFromWriteModel(ctx, &existingGroup.WriteModel),
|
||||
groupUpdate.Name,
|
||||
groupUpdate.Description,
|
||||
)
|
||||
if changedEvent == nil {
|
||||
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
|
||||
}
|
||||
|
||||
err = c.pushAppendAndReduce(ctx,
|
||||
existingGroup,
|
||||
changedEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
|
||||
}
|
||||
|
||||
// DeleteGroup deletes a user group from an organization
|
||||
func (c *Commands) DeleteGroup(ctx context.Context, groupID string) (details *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
existingGroup, err := c.getGroupWriteModelByID(ctx, groupID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !existingGroup.State.Exists() {
|
||||
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
|
||||
}
|
||||
|
||||
err = c.pushAppendAndReduce(ctx,
|
||||
existingGroup,
|
||||
repo.NewGroupRemovedEvent(ctx,
|
||||
GroupAggregateFromWriteModel(ctx, &existingGroup.WriteModel),
|
||||
existingGroup.Name,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&existingGroup.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) getGroupWriteModelByID(ctx context.Context, id, orgID string) (*GroupWriteModel, error) {
|
||||
groupWriteModel := NewGroupWriteModel(id, orgID)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, groupWriteModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groupWriteModel, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/group"
|
||||
)
|
||||
|
||||
// GroupWriteModel represents the write-model for a group.
|
||||
type GroupWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
Name string
|
||||
Description string
|
||||
|
||||
State domain.GroupState
|
||||
}
|
||||
|
||||
// NewGroupWriteModel initializes a new instance of GroupWriteModel from the given Group.
|
||||
func NewGroupWriteModel(id, orgID string) *GroupWriteModel {
|
||||
return &GroupWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: id,
|
||||
ResourceOwner: orgID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Query constructs a search query for retrieving group-related events based on the GroupWriteModel attributes.
|
||||
func (g *GroupWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(g.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(group.AggregateType).
|
||||
AggregateIDs(g.AggregateID).
|
||||
EventTypes(
|
||||
group.GroupAddedEventType,
|
||||
group.GroupChangedEventType,
|
||||
group.GroupRemovedEventType).Builder()
|
||||
}
|
||||
|
||||
func (g *GroupWriteModel) Reduce() error {
|
||||
for _, event := range g.Events {
|
||||
switch e := event.(type) {
|
||||
case *group.GroupAddedEvent:
|
||||
g.AggregateID = e.ID
|
||||
g.Name = e.Name
|
||||
g.Description = e.Description
|
||||
g.State = domain.GroupStateActive
|
||||
case *group.GroupChangedEvent:
|
||||
if e.Name != nil {
|
||||
g.Name = *e.Name
|
||||
}
|
||||
if e.Description != nil {
|
||||
g.Description = *e.Description
|
||||
}
|
||||
case *group.GroupRemovedEvent:
|
||||
g.State = domain.GroupStateRemoved
|
||||
}
|
||||
}
|
||||
return g.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (g *GroupWriteModel) NewChangedEvent(ctx context.Context, agg *eventstore.Aggregate, name, description *string) *group.GroupChangedEvent {
|
||||
changes := make([]group.GroupChanges, 0)
|
||||
oldName := ""
|
||||
|
||||
if name != nil && g.Name != *name {
|
||||
oldName = g.Name
|
||||
changes = append(changes, group.ChangeName(name))
|
||||
}
|
||||
if description != nil && g.Description != *description {
|
||||
changes = append(changes, group.ChangeDescription(description))
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return group.NewGroupChangedEvent(ctx, agg, oldName, changes)
|
||||
}
|
||||
|
||||
// GroupAggregateFromWriteModel maps a WriteModel to a group-specific Aggregate using its type and version.
|
||||
func GroupAggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
|
||||
return eventstore.AggregateFromWriteModelCtx(ctx, wm, group.AggregateType, group.AggregateVersion)
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/group"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestCommands_CreateGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pushErr := errors.New("push error")
|
||||
filterErr := errors.New("filter error")
|
||||
idGeneratorErr := errors.New("id generator error")
|
||||
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
group *CreateGroup
|
||||
aggregateID func() (string, error)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "invalid group name, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: " ",
|
||||
Description: "example group",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "missing organization id, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "org not found, precondition error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.IsPreconditionFailed,
|
||||
},
|
||||
{
|
||||
name: "failed to generate group id, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
aggregateID: func() (string, error) {
|
||||
return "", idGeneratorErr
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, idGeneratorErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group already exists, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"example",
|
||||
"example group",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "1234",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.IsErrorAlreadyExists,
|
||||
},
|
||||
{
|
||||
name: "failed to get org write model, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(filterErr),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, zerrors.ThrowPreconditionFailed(nil, "CMDGRP-j1mH8l", "Errors.Org.NotFound"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed to get group write model, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilterError(filterErr),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
aggregateID: func() (string, error) {
|
||||
return "12345", nil
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, filterErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed to push group added event, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPushFailed(
|
||||
pushErr,
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("12345", "org1").Aggregate,
|
||||
"example",
|
||||
"example group",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
aggregateID: func() (string, error) {
|
||||
return "12345", nil
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, pushErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group without user provided id, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("12345", "org1").Aggregate,
|
||||
"example",
|
||||
"example group",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
aggregateID: func() (string, error) {
|
||||
return "12345", nil
|
||||
}, // mock value generated by the id generator
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "12345",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group with user provided id, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(context.Background(),
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("9090", "org1").Aggregate,
|
||||
"example",
|
||||
"example group",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &CreateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "9090",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Name: "example",
|
||||
Description: "example group",
|
||||
},
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "9090",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// setup mock id generator
|
||||
// it's not defined directly in the tests because,
|
||||
// when run individually, it affects other tests that don't need this mock.
|
||||
if tt.args.aggregateID != nil {
|
||||
aggregateID, err := tt.args.aggregateID()
|
||||
if err != nil {
|
||||
tt.fields.idGenerator = mock.NewIDGeneratorExpectError(t, err)
|
||||
} else {
|
||||
tt.fields.idGenerator = mock.ExpectID(t, aggregateID)
|
||||
}
|
||||
}
|
||||
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
|
||||
got, err := c.CreateGroup(tt.args.ctx, tt.args.group)
|
||||
if tt.wantErr == nil {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got.ID)
|
||||
assertObjectDetails(t, tt.want, got)
|
||||
return
|
||||
}
|
||||
require.True(t, tt.wantErr(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_UpdateGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filterErr := errors.New("filter error")
|
||||
pushErr := errors.New("push error")
|
||||
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
group *UpdateGroup
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "invalid group name, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
Name: gu.Ptr(" "),
|
||||
Description: gu.Ptr("example group"),
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "group not found, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "1234",
|
||||
},
|
||||
Name: gu.Ptr("updated name"),
|
||||
Description: gu.Ptr("updated description"),
|
||||
},
|
||||
},
|
||||
wantErr: zerrors.IsNotFound,
|
||||
},
|
||||
{
|
||||
name: "failed to get group write model, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(filterErr),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
Name: gu.Ptr("updated group name"),
|
||||
Description: gu.Ptr("updated group description"),
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, filterErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed to push group changed event, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPushFailed(
|
||||
pushErr,
|
||||
group.NewGroupChangedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
[]group.GroupChanges{
|
||||
group.ChangeName(gu.Ptr("updated group name")),
|
||||
group.ChangeDescription(gu.Ptr("updated group description")),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
Name: gu.Ptr("updated group name"),
|
||||
Description: gu.Ptr("updated group description"),
|
||||
},
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, pushErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no change, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "1234",
|
||||
},
|
||||
Name: gu.Ptr("group1"),
|
||||
Description: gu.Ptr("group1 description"),
|
||||
},
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "1234",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update group name, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
group.NewGroupChangedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
[]group.GroupChanges{
|
||||
group.ChangeName(gu.Ptr("groupXX")),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "1234",
|
||||
},
|
||||
Name: gu.Ptr("groupXX"),
|
||||
},
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "1234",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update group description, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
group.NewGroupChangedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
[]group.GroupChanges{
|
||||
group.ChangeDescription(gu.Ptr("updated group description")),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "1234",
|
||||
},
|
||||
Description: gu.Ptr("updated group description"),
|
||||
},
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "1234",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full update (group name and description), ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
group.NewGroupChangedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "org1").Aggregate,
|
||||
"group1",
|
||||
[]group.GroupChanges{
|
||||
group.ChangeName(gu.Ptr("groupXX")),
|
||||
group.ChangeDescription(gu.Ptr("updated group description")),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
group: &UpdateGroup{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "1234",
|
||||
},
|
||||
Name: gu.Ptr("groupXX"),
|
||||
Description: gu.Ptr("updated group description"),
|
||||
},
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "1234",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := c.UpdateGroup(tt.args.ctx, tt.args.group)
|
||||
if tt.wantErr == nil {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got.ID)
|
||||
assertObjectDetails(t, tt.want, got)
|
||||
return
|
||||
}
|
||||
require.True(t, tt.wantErr(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_DeleteGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filterErr := errors.New("filter error")
|
||||
pushErr := errors.New("push error")
|
||||
|
||||
type fields struct {
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
groupID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "failed to get group write model, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(filterErr),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
groupID: "1234",
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, filterErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group not found, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
groupID: "1234",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed to push group delete event, error",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPushFailed(
|
||||
pushErr,
|
||||
group.NewGroupRemovedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "").Aggregate,
|
||||
"group1",
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
groupID: "1234",
|
||||
},
|
||||
wantErr: func(err error) bool {
|
||||
return errors.Is(err, pushErr)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete group, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupAddedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "").Aggregate,
|
||||
"group1",
|
||||
"group1 description",
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
eventFromEventPusher(
|
||||
group.NewGroupRemovedEvent(context.Background(),
|
||||
&group.NewAggregate("1234", "").Aggregate,
|
||||
"group1",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
groupID: "1234",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ID: "1234",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore(t),
|
||||
}
|
||||
got, err := c.DeleteGroup(tt.args.ctx, tt.args.groupID)
|
||||
if tt.wantErr == nil {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, got.ID)
|
||||
assertObjectDetails(t, tt.want, got)
|
||||
return
|
||||
}
|
||||
require.True(t, tt.wantErr(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user