diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index e89530bf0d..b8dac72b9a 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -209,10 +209,18 @@ func Init(ctx context.Context) error { return nil } -func Start(ctx context.Context) { +func Start(ctx context.Context) error { + projectionTableMap := make(map[string]bool, len(projections)) for _, projection := range projections { + table := projection.String() + if projectionTableMap[table] { + return fmt.Errorf("projeciton for %s already added", table) + } + projectionTableMap[table] = true + projection.Start(ctx) } + return nil } func ProjectInstance(ctx context.Context) error { diff --git a/internal/query/projection/projection_mock.go b/internal/query/projection/projection_mock.go new file mode 100644 index 0000000000..6ba50082ae --- /dev/null +++ b/internal/query/projection/projection_mock.go @@ -0,0 +1,130 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: projection.go +// +// Generated by this command: +// +// mockgen -source projection.go -destination ./projection_mock.go -package projection +// + +// Package projection is a generated GoMock package. +package projection + +import ( + context "context" + reflect "reflect" + + eventstore "github.com/zitadel/zitadel/internal/eventstore" + handler "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + gomock "go.uber.org/mock/gomock" +) + +// Mockprojection is a mock of projection interface. +type Mockprojection struct { + ctrl *gomock.Controller + recorder *MockprojectionMockRecorder +} + +// MockprojectionMockRecorder is the mock recorder for Mockprojection. +type MockprojectionMockRecorder struct { + mock *Mockprojection +} + +// NewMockprojection creates a new mock instance. +func NewMockprojection(ctrl *gomock.Controller) *Mockprojection { + mock := &Mockprojection{ctrl: ctrl} + mock.recorder = &MockprojectionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mockprojection) EXPECT() *MockprojectionMockRecorder { + return m.recorder +} + +// Execute mocks base method. +func (m *Mockprojection) Execute(ctx context.Context, startedEvent eventstore.Event) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execute", ctx, startedEvent) + ret0, _ := ret[0].(error) + return ret0 +} + +// Execute indicates an expected call of Execute. +func (mr *MockprojectionMockRecorder) Execute(ctx, startedEvent any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*Mockprojection)(nil).Execute), ctx, startedEvent) +} + +// Init mocks base method. +func (m *Mockprojection) Init(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockprojectionMockRecorder) Init(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*Mockprojection)(nil).Init), ctx) +} + +// ProjectionName mocks base method. +func (m *Mockprojection) ProjectionName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectionName") + ret0, _ := ret[0].(string) + return ret0 +} + +// ProjectionName indicates an expected call of ProjectionName. +func (mr *MockprojectionMockRecorder) ProjectionName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectionName", reflect.TypeOf((*Mockprojection)(nil).ProjectionName)) +} + +// Start mocks base method. +func (m *Mockprojection) Start(ctx context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Start", ctx) +} + +// Start indicates an expected call of Start. +func (mr *MockprojectionMockRecorder) Start(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*Mockprojection)(nil).Start), ctx) +} + +// String mocks base method. +func (m *Mockprojection) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String. +func (mr *MockprojectionMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*Mockprojection)(nil).String)) +} + +// Trigger mocks base method. +func (m *Mockprojection) Trigger(ctx context.Context, opts ...handler.TriggerOpt) (context.Context, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Trigger", varargs...) + ret0, _ := ret[0].(context.Context) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Trigger indicates an expected call of Trigger. +func (mr *MockprojectionMockRecorder) Trigger(ctx any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Trigger", reflect.TypeOf((*Mockprojection)(nil).Trigger), varargs...) +} diff --git a/internal/query/projection/projection_test.go b/internal/query/projection/projection_test.go new file mode 100644 index 0000000000..c7b071163a --- /dev/null +++ b/internal/query/projection/projection_test.go @@ -0,0 +1,69 @@ +package projection + +import ( + "fmt" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" +) + +func TestStart(t *testing.T) { + duplicateName := gofakeit.Name() + tests := []struct { + name string + projections func(t *testing.T) []projection + err error + }{ + { + name: "happy path", + projections: func(t *testing.T) []projection { + ctrl := gomock.NewController(t) + projections := make([]projection, 5) + + for i := range 5 { + mock := NewMockprojection(ctrl) + mock.EXPECT().Start(gomock.Any()) + mock.EXPECT().String().Return(gofakeit.Name()) + projections[i] = mock + } + + return projections + }, + }, + { + name: "same projection used twice error", + projections: func(t *testing.T) []projection { + projections := make([]projection, 5) + + ctrl := gomock.NewController(t) + mock := NewMockprojection(ctrl) + mock.EXPECT().String().Return(duplicateName) + mock.EXPECT().Start(gomock.Any()) + projections[0] = mock + + for i := 1; i < 4; i++ { + mock := NewMockprojection(ctrl) + mock.EXPECT().String().Return(gofakeit.Name()) + mock.EXPECT().Start(gomock.Any()) + projections[i] = mock + } + + mock = NewMockprojection(ctrl) + mock.EXPECT().String().Return(duplicateName) + projections[4] = mock + + return projections + }, + err: fmt.Errorf("projeciton for %s already added", duplicateName), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + projections = tt.projections(t) + err := Start(t.Context()) + require.Equal(t, tt.err, err) + }) + } +} diff --git a/internal/query/query.go b/internal/query/query.go index e2e7f58ffc..7be38c6bda 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -90,7 +90,10 @@ func StartQueries( return nil, err } if startProjections { - projection.Start(ctx) + err = projection.Start(ctx) + if err != nil { + return nil, err + } } repo.caches, err = startCaches(