Files
zitadel/internal/command/group_users_test.go
Gayathri Vijayan ad8e8bf61f feat(group): manage users in user groups (#10940)
# Which Problems Are Solved

1. Adding users to user groups and removing users from user groups.
2. Searching for users in user groups by group IDs or user IDs

# How the Problems Are Solved

By adding:
1. The API definitions to manage users in users groups
3. The command-layer implementation of adding users/removing users
to/from user groups.
4. The projection table group_users1
5. Query-side implementation to search for users in user groups

# Additional Changes

1. Remove debug statements from unit tests.
2. Fix removal of groups when orgs are removed
3. Add unit tests for groups projection

# Additional Context

* Related to #9702 
* Follow-up for PRs 
  * https://github.com/zitadel/zitadel/pull/10455
  * https://github.com/zitadel/zitadel/pull/10758
  * https://github.com/zitadel/zitadel/pull/10853
2025-10-28 13:23:54 +00:00

542 lines
13 KiB
Go

package command
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/group"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommands_AddUsersToGroup(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
checkPermission domain.PermissionCheck
}
type args struct {
groupID string
userIDs []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{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: func(err error) bool {
return errors.Is(err, filterErr)
},
},
{
name: "group not found, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: zerrors.IsPreconditionFailed,
},
{
name: "missing permission, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
)),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: zerrors.IsPermissionDenied,
},
{
name: "user not found, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
),
expectFilter(), // to get the user write model for user1
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: zerrors.IsPreconditionFailed,
},
{
name: "some users already exist in the group, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1"}),
),
),
expectFilter( // to get the user write model to check if user1 exists
eventFromEventPusher(
addNewUserEvent("user2", "org1"),
),
),
expectPush(
group.NewGroupUsersAddedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user2"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
want: &domain.ObjectDetails{
ID: "group1",
ResourceOwner: "org1",
},
},
{
name: "all users already exist in the group, no events pushed, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
want: &domain.ObjectDetails{
ID: "group1",
ResourceOwner: "org1",
},
},
{
name: "failed to push events, error",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
),
expectFilter( // to get the user write model for user1
eventFromEventPusher(
addNewUserEvent("user1", "org1"),
),
),
expectPushFailed(
pushErr,
group.NewGroupUsersAddedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user1"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1"},
},
wantErr: func(err error) bool {
return errors.Is(err, pushErr)
},
},
{
name: "all users added, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
),
expectFilter( // to get the user write model for user1
eventFromEventPusher(
addNewUserEvent("user1", "org1"),
),
),
expectFilter( // to get the user write model for user2
eventFromEventPusher(
addNewUserEvent("user2", "org1"),
),
),
expectPush(
group.NewGroupUsersAddedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user1", "user2"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
want: &domain.ObjectDetails{
ID: "group1",
ResourceOwner: "org1",
},
},
{
name: "all users added (with duplicate users), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
),
expectFilter( // to get the user write model for user1
eventFromEventPusher(
addNewUserEvent("user1", "org1"),
),
),
expectFilter( // to get the user write model for user2
eventFromEventPusher(
addNewUserEvent("user2", "org1"),
),
),
expectPush(
group.NewGroupUsersAddedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user1", "user2"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2", "user2", "user1"},
},
want: &domain.ObjectDetails{
ID: "group1",
ResourceOwner: "org1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := c.AddUsersToGroup(context.Background(), tt.args.groupID, tt.args.userIDs)
if tt.wantErr != nil {
require.True(t, tt.wantErr(err))
return
}
assertObjectDetails(t, tt.want, got)
})
}
}
func addNewGroupEvent(groupID, orgID string) *group.GroupAddedEvent {
return group.NewGroupAddedEvent(context.Background(),
&group.NewAggregate(groupID, orgID).Aggregate,
groupID,
"group description",
)
}
func TestCommands_RemoveUsersFromGroup(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
checkPermission domain.PermissionCheck
}
type args struct {
groupID string
userIDs []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{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: func(err error) bool {
return errors.Is(err, filterErr)
},
},
{
name: "group not found, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: zerrors.IsPreconditionFailed,
},
{
name: "missing permission, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
)),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
wantErr: zerrors.IsPermissionDenied,
},
{
name: "remove users, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
),
),
expectPush(
group.NewGroupUsersRemovedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user1", "user2"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2"},
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
ID: "group1",
},
},
{
name: "remove users (with duplicates), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
),
),
expectPush(
group.NewGroupUsersRemovedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user1", "user2"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1", "user2", "user1", "user2", "user1", "user2"},
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
ID: "group1",
},
},
{
name: "some users not in group, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
),
),
expectPush(
group.NewGroupUsersRemovedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user2"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user2", "user3"},
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
ID: "group1",
},
},
{
name: "all users not in group, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user3", "user4"},
},
want: &domain.ObjectDetails{
ResourceOwner: "org1",
ID: "group1",
},
},
{
name: "failed to push events, error",
fields: fields{
eventstore: expectEventstore(
expectFilter( // to get the group write model
eventFromEventPusher(
addNewGroupEvent("group1", "org1"),
),
eventFromEventPusher(
addNewGroupUsersAddedEvent("group1", "org1", []string{"user1", "user2"}),
),
),
expectPushFailed(
pushErr,
group.NewGroupUsersRemovedEvent(context.Background(),
&group.NewAggregate("group1", "org1").Aggregate,
[]string{"user1"},
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
groupID: "group1",
userIDs: []string{"user1"},
},
wantErr: func(err error) bool {
return errors.Is(err, pushErr)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := c.RemoveUsersFromGroup(context.Background(), tt.args.groupID, tt.args.userIDs)
if tt.wantErr != nil {
require.True(t, tt.wantErr(err))
return
}
assertObjectDetails(t, tt.want, got)
})
}
}
func addNewGroupUsersAddedEvent(groupID, orgID string, userIds []string) *group.GroupUsersAddedEvent {
return group.NewGroupUsersAddedEvent(context.Background(),
&group.NewAggregate(groupID, orgID).Aggregate,
userIds,
)
}
func addNewUserEvent(userID, orgID string) *user.HumanAddedEvent {
return user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate(userID, orgID).Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
AllowedLanguage,
domain.GenderUnspecified,
"email@test.ch",
true,
)
}