Files
zitadel/internal/eventstore/v3/event_test.go
T
6082e59d47 fix(eventstore): allow overwriting resource owner of events (#12261)
# Which Problems Are Solved

- The eventstore did not support intentionally overwriting the resource
owner when creating events for aggregates that may be reused across
owners.
- Resource owner handling was implicit and could not be controlled per
command/event type.
- We needed a safe way to distinguish between:
  - keeping the existing aggregate owner, and
  - explicitly setting a new owner for specific create-like events.

# How the Problems Are Solved

- Introduced a new eventstore command type with an explicit
enforce_owner flag.
- Updated eventstore.commands_to_events and eventstore.push so owner
assignment is now explicit:
  - if enforce_owner is true, the command owner is written
- if enforce_owner is false, the existing aggregate owner is retained
when present
- Added EnforceResourceOwnerCommand and wiring so command types can opt
in to enforced owner behavior.
- Wired the new behavior through the v3 eventstore push path, including
compatibility fallback for older command type mapping.
- Added migration/setup changes to register and use the new command type
and SQL functions.
- Added and updated tests for owner overwrite and aggregate ID reuse
scenarios.

# Additional Changes

- Added small migration/setup robustness improvements related to
eventstore setup ordering and helper reuse.
- Added focused test coverage for enforced owner behavior and
sequencing.
- Events that currently allow owner changes (implement
EnforceResourceOwner) are:
  - AddedEvent (action)
  - GroupAddedEvent
  - StartedEvent (idp intent)
  - ProjectAddedEvent
  - HumanAddedEvent
  - HumanRegisteredEvent
  - MachineAddedEvent
  - CreatedEvent (schema user)

# Additional Context

- Follow-up for eventstore owner-handling correctness in create flows
and aggregate ID reuse cases.
- No additional issue link was attached for this change.

---------

Co-authored-by: abhishek kumar gupta <[email protected]>
2026-06-15 11:24:37 +02:00

557 lines
11 KiB
Go

package eventstore
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
)
func Test_commandToEvent(t *testing.T) {
payload := struct {
ID string
}{
ID: "test",
}
payloadMarshalled, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal of payload failed: %v", err)
}
type args struct {
command eventstore.Command
}
type want struct {
event *event
err func(t *testing.T, err error)
}
tests := []struct {
name string
args args
want want
}{
{
name: "no payload",
args: args{
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: nil,
},
},
want: want{
event: mockEvent(
mockAggregate("V3-Red9I"),
0,
nil,
).(*event),
},
},
{
name: "struct payload",
args: args{
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: payload,
},
},
want: want{
event: mockEvent(
mockAggregate("V3-Red9I"),
0,
payloadMarshalled,
).(*event),
},
},
{
name: "pointer payload",
args: args{
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: &payload,
},
},
want: want{
event: mockEvent(
mockAggregate("V3-Red9I"),
0,
payloadMarshalled,
).(*event),
},
},
{
name: "invalid payload",
args: args{
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: func() {},
},
},
want: want{
err: func(t *testing.T, err error) {
assert.Error(t, err)
},
},
},
{
name: "enforced resource owner",
args: args{
command: &enforcedMockCommand{
mockCommand: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: nil,
},
},
},
want: want{
event: &event{
command: &command{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
Owner: "ro",
Creator: "creator",
Revision: 1,
CommandType: "event.type",
Payload: nil,
EnforceOwner: true,
},
},
},
},
}
for _, tt := range tests {
if tt.want.err == nil {
tt.want.err = func(t *testing.T, err error) {
require.NoError(t, err)
}
}
t.Run(tt.name, func(t *testing.T) {
got, err := commandToEvent(tt.args.command)
tt.want.err(t, err)
if tt.want.event == nil {
assert.Nil(t, got)
return
}
assert.Equal(t, tt.want.event, got)
})
}
}
func Test_commandToEventOld(t *testing.T) {
payload := struct {
ID string
}{
ID: "test",
}
payloadMarshalled, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal of payload failed: %v", err)
}
type args struct {
sequence *latestSequence
command eventstore.Command
}
type want struct {
event *event
err func(t *testing.T, err error)
}
tests := []struct {
name string
args args
want want
}{
{
name: "no payload",
args: args{
sequence: &latestSequence{
aggregate: mockAggregate("V3-Red9I"),
sequence: 0,
},
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: nil,
},
},
want: want{
event: mockEvent(
mockAggregate("V3-Red9I"),
0,
nil,
).(*event),
},
},
{
name: "struct payload",
args: args{
sequence: &latestSequence{
aggregate: mockAggregate("V3-Red9I"),
sequence: 0,
},
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: payload,
},
},
want: want{
event: mockEvent(
mockAggregate("V3-Red9I"),
0,
payloadMarshalled,
).(*event),
},
},
{
name: "pointer payload",
args: args{
sequence: &latestSequence{
aggregate: mockAggregate("V3-Red9I"),
sequence: 0,
},
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: &payload,
},
},
want: want{
event: mockEvent(
mockAggregate("V3-Red9I"),
0,
payloadMarshalled,
).(*event),
},
},
{
name: "invalid payload",
args: args{
sequence: &latestSequence{
aggregate: mockAggregate("V3-Red9I"),
sequence: 0,
},
command: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: func() {},
},
},
want: want{
err: func(t *testing.T, err error) {
assert.Error(t, err)
},
},
},
}
for _, tt := range tests {
if tt.want.err == nil {
tt.want.err = func(t *testing.T, err error) {
require.NoError(t, err)
}
}
t.Run(tt.name, func(t *testing.T) {
got, err := commandToEventOld(tt.args.sequence, tt.args.command)
tt.want.err(t, err)
assert.Equal(t, tt.want.event, got)
})
}
}
func Test_commandsToEvents(t *testing.T) {
ctx := context.Background()
payload := struct {
ID string
}{
ID: "test",
}
payloadMarshalled, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal of payload failed: %v", err)
}
type args struct {
ctx context.Context
cmds []eventstore.Command
}
type want struct {
events []eventstore.Event
commands []*command
err func(t *testing.T, err error)
}
tests := []struct {
name string
args args
want want
}{
{
name: "no commands",
args: args{
ctx: ctx,
cmds: nil,
},
want: want{
events: []eventstore.Event{},
commands: []*command{},
err: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
},
{
name: "single command no payload",
args: args{
ctx: ctx,
cmds: []eventstore.Command{
&mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: nil,
},
},
},
want: want{
events: []eventstore.Event{
mockEvent(
mockAggregate("V3-Red9I"),
0,
nil,
),
},
commands: []*command{
{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
Owner: "ro",
CommandType: "event.type",
Revision: 1,
Payload: nil,
Creator: "creator",
},
},
err: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
},
{
name: "single command no instance id",
args: args{
ctx: authz.WithInstanceID(ctx, "instance from ctx"),
cmds: []eventstore.Command{
&mockCommand{
aggregate: mockAggregateWithInstance("V3-Red9I", ""),
payload: nil,
},
},
},
want: want{
events: []eventstore.Event{
mockEvent(
mockAggregateWithInstance("V3-Red9I", "instance from ctx"),
0,
nil,
),
},
commands: []*command{
{
InstanceID: "instance from ctx",
AggregateType: "type",
AggregateID: "V3-Red9I",
Owner: "ro",
CommandType: "event.type",
Revision: 1,
Payload: nil,
Creator: "creator",
},
},
err: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
},
{
name: "single command with payload",
args: args{
ctx: ctx,
cmds: []eventstore.Command{
&mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: payload,
},
},
},
want: want{
events: []eventstore.Event{
mockEvent(
mockAggregate("V3-Red9I"),
0,
payloadMarshalled,
),
},
commands: []*command{
{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
Owner: "ro",
CommandType: "event.type",
Revision: 1,
Payload: payloadMarshalled,
Creator: "creator",
},
},
err: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
},
{
name: "multiple commands",
args: args{
ctx: ctx,
cmds: []eventstore.Command{
&mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: payload,
},
&mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: nil,
},
},
},
want: want{
events: []eventstore.Event{
mockEvent(
mockAggregate("V3-Red9I"),
0,
payloadMarshalled,
),
mockEvent(
mockAggregate("V3-Red9I"),
0,
nil,
),
},
commands: []*command{
{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
CommandType: "event.type",
Revision: 1,
Payload: payloadMarshalled,
Creator: "creator",
Owner: "ro",
},
{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
CommandType: "event.type",
Revision: 1,
Payload: nil,
Creator: "creator",
Owner: "ro",
},
},
err: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
},
{
name: "invalid command",
args: args{
ctx: ctx,
cmds: []eventstore.Command{
&mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: func() {},
},
},
},
want: want{
events: nil,
commands: nil,
err: func(t *testing.T, err error) {
assert.Error(t, err)
},
},
},
{
name: "single enforced command",
args: args{
ctx: ctx,
cmds: []eventstore.Command{
&enforcedMockCommand{
mockCommand: &mockCommand{
aggregate: mockAggregate("V3-Red9I"),
payload: nil,
},
},
},
},
want: want{
events: []eventstore.Event{
&event{
command: &command{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
Owner: "ro",
Creator: "creator",
Revision: 1,
CommandType: "event.type",
Payload: nil,
EnforceOwner: true,
},
},
},
commands: []*command{
{
InstanceID: "instance",
AggregateType: "type",
AggregateID: "V3-Red9I",
Owner: "ro",
CommandType: "event.type",
Revision: 1,
Payload: nil,
Creator: "creator",
EnforceOwner: true,
},
},
err: func(t *testing.T, err error) {
require.NoError(t, err)
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotEvents, gotCommands, err := commandsToEvents(tt.args.ctx, tt.args.cmds)
tt.want.err(t, err)
assert.Equal(t, tt.want.events, gotEvents)
require.Len(t, gotCommands, len(tt.want.commands))
for i, wantCommand := range tt.want.commands {
assertCommand(t, wantCommand, gotCommands[i])
}
})
}
}
func assertCommand(t *testing.T, want, got *command) {
t.Helper()
assert.Equal(t, want.CommandType, got.CommandType)
assert.Equal(t, want.Payload, got.Payload)
assert.Equal(t, want.Creator, got.Creator)
assert.Equal(t, want.Owner, got.Owner)
assert.Equal(t, want.AggregateID, got.AggregateID)
assert.Equal(t, want.AggregateType, got.AggregateType)
assert.Equal(t, want.InstanceID, got.InstanceID)
assert.Equal(t, want.Revision, got.Revision)
assert.Equal(t, want.EnforceOwner, got.EnforceOwner)
}