feat: add action v2 execution on requests and responses (#7637)

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: add execution of targets to grpc calls

* feat: split request and response logic to handle the different context information

* feat: split request and response logic to handle the different context information

* fix: integration test

* fix: import alias

* fix: refactor execution package

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* fix: refactor execution interceptor integration and unit tests

* docs: basic documentation for executions and targets

* fix: change order for interceptors

* fix: merge back origin/main

* fix: change target definition command and query side (#7735)

* fix: change target definition command and query side

* fix: correct refactoring name changes

* fix: correct refactoring name changes

* fix: changing execution defintion with target list and type

* fix: changing execution definition with target list and type

* fix: add back search queries for target and include

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* fix: projections change for execution with targets suffix table

* docs: add example to actions v2

* docs: add example to actions v2

* fix: correct integration tests on query for executions

* fix: add separate event for execution v2 as content changed

* fix: add separate event for execution v2 as content changed

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

---------

Co-authored-by: adlerhurst <[email protected]>

* fix: added review comment changes

* fix: added review comment changes

* Update internal/api/grpc/server/middleware/execution_interceptor.go

Co-authored-by: Silvan <[email protected]>

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

* fix: added review comment changes

---------

Co-authored-by: adlerhurst <[email protected]>
Co-authored-by: Elio Bischof <[email protected]>
This commit is contained in:
Stefan Benz
2024-05-04 11:55:57 +02:00
committed by GitHub
co-authored by adlerhurst Elio Bischof
parent 7e345444bf
commit 1c5ecba42a
67 changed files with 4395 additions and 1554 deletions
+77 -34
View File
@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
@@ -33,46 +34,46 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
return nil, err
}
targets := make([]*execution.Target, len(req.Targets))
for i, target := range req.Targets {
switch t := target.GetType().(type) {
case *action.ExecutionTargetType_Include:
include, err := conditionToInclude(t.Include)
if err != nil {
return nil, err
}
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeInclude, Target: include}
case *action.ExecutionTargetType_Target:
targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: t.Target}
}
}
set := &command.SetExecution{
Targets: req.GetTargets(),
Includes: req.GetIncludes(),
Targets: targets,
}
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), set, authz.GetInstance(ctx).InstanceID())
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
@@ -82,6 +83,36 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque
}, nil
}
func conditionToInclude(cond *action.Condition) (string, error) {
switch t := cond.GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(), nil
case *action.Condition_Function:
cond := command.ExecutionFunctionCondition(t.Function.GetName())
if err := cond.IsValid(); err != nil {
return "", err
}
return cond.ID(), nil
}
return "", nil
}
func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutionRequest) (*action.DeleteExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
@@ -91,37 +122,25 @@ func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutio
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
cond := executionConditionFromRequest(t.Request)
details, err = s.command.DeleteExecutionRequest(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
cond := executionConditionFromResponse(t.Response)
details, err = s.command.DeleteExecutionResponse(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
cond := executionConditionFromEvent(t.Event)
details, err = s.command.DeleteExecutionEvent(ctx, cond, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
case *action.Condition_Function:
details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function), authz.GetInstance(ctx).InstanceID())
details, err = s.command.DeleteExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
@@ -130,3 +149,27 @@ func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutio
Details: object.DomainToDetailsPb(details),
}, nil
}
func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: request.GetMethod(),
Service: request.GetService(),
All: request.GetAll(),
}
}
func executionConditionFromResponse(response *action.ResponseExecution) *command.ExecutionAPICondition {
return &command.ExecutionAPICondition{
Method: response.GetMethod(),
Service: response.GetService(),
All: response.GetAll(),
}
}
func executionConditionFromEvent(event *action.EventExecution) *command.ExecutionEventCondition {
return &command.ExecutionEventCondition{
Event: event.GetEvent(),
Group: event.GetGroup(),
All: event.GetAll(),
}
}
@@ -9,14 +9,23 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
)
func executionTargetsSingleTarget(id string) []*action.ExecutionTargetType {
return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Target{Target: id}}}
}
func executionTargetsSingleInclude(include *action.Condition) []*action.ExecutionTargetType {
return []*action.ExecutionTargetType{{Type: &action.ExecutionTargetType_Include{Include: include}}}
}
func TestServer_SetExecution_Request(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -48,7 +57,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
Request: &action.RequestExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -65,7 +74,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -82,7 +91,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -104,7 +113,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -121,7 +130,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -143,7 +152,7 @@ func TestServer_SetExecution_Request(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -163,27 +172,28 @@ func TestServer_SetExecution_Request(t *testing.T) {
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Request_Include(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
executionCond := "request"
Tester.SetExecution(CTX, t,
&action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
executionCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
[]string{targetResp.GetId()},
[]string{},
}
Tester.SetExecution(CTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetId()),
)
tests := []struct {
@@ -206,7 +216,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Includes: []string{executionCond},
Targets: executionTargetsSingleInclude(executionCond),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -228,7 +238,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Includes: []string{executionCond},
Targets: executionTargetsSingleInclude(executionCond),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -237,6 +247,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
/* circular
{
name: "all, ok",
ctx: CTX,
@@ -250,7 +261,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
Includes: []string{executionCond},
Targets: executionTargetsSingleInclude(executionCond),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -259,6 +270,7 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -270,13 +282,16 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_DeleteExecution_Request(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -332,7 +347,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
name: "method, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -373,7 +388,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
name: "service, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -398,7 +413,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
name: "all, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -441,7 +456,7 @@ func TestServer_DeleteExecution_Request(t *testing.T) {
func TestServer_SetExecution_Response(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -473,7 +488,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -490,7 +505,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -507,7 +522,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -529,7 +544,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -546,7 +561,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -568,7 +583,7 @@ func TestServer_SetExecution_Response(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -588,13 +603,16 @@ func TestServer_SetExecution_Response(t *testing.T) {
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_DeleteExecution_Response(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -652,7 +670,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
name: "method, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -693,7 +711,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
name: "service, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -718,7 +736,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
name: "all, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -761,7 +779,7 @@ func TestServer_DeleteExecution_Response(t *testing.T) {
func TestServer_SetExecution_Event(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -795,7 +813,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
Event: &action.EventExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -833,7 +851,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -876,7 +894,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -898,7 +916,7 @@ func TestServer_SetExecution_Event(t *testing.T) {
},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -918,13 +936,16 @@ func TestServer_SetExecution_Event(t *testing.T) {
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_DeleteExecution_Event(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -985,7 +1006,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
name: "event, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -1026,7 +1047,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
name: "group, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -1061,18 +1082,13 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
},
},
},
want: &action.DeleteExecutionResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
wantErr: true,
},
{
name: "all, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
@@ -1115,7 +1131,7 @@ func TestServer_DeleteExecution_Event(t *testing.T) {
func TestServer_SetExecution_Function(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -1147,7 +1163,7 @@ func TestServer_SetExecution_Function(t *testing.T) {
Response: &action.ResponseExecution{},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -1157,10 +1173,10 @@ func TestServer_SetExecution_Function(t *testing.T) {
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "xxx",
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
wantErr: true,
},
@@ -1170,10 +1186,10 @@ func TestServer_SetExecution_Function(t *testing.T) {
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication",
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
},
},
Targets: []string{targetResp.GetId()},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
want: &action.SetExecutionResponse{
Details: &object.Details{
@@ -1193,13 +1209,16 @@ func TestServer_SetExecution_Function(t *testing.T) {
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_DeleteExecution_Function(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
@@ -1243,7 +1262,7 @@ func TestServer_DeleteExecution_Function(t *testing.T) {
req: &action.DeleteExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "xxx",
Function: &action.FunctionExecution{Name: "xxx"},
},
},
},
@@ -1253,13 +1272,13 @@ func TestServer_DeleteExecution_Function(t *testing.T) {
name: "function, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.DeleteExecutionRequest) error {
Tester.SetExecution(ctx, t, request.GetCondition(), []string{targetResp.GetId()}, []string{})
Tester.SetExecution(ctx, t, request.GetCondition(), executionTargetsSingleTarget(targetResp.GetId()))
return nil
},
req: &action.DeleteExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication",
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
},
},
},
@@ -0,0 +1,323 @@
//go:build integration
package action_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
func TestServer_ExecutionTarget(t *testing.T) {
ensureFeatureEnabled(t)
fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID"
tests := []struct {
name string
ctx context.Context
dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) (func(), error)
clean func(context.Context)
req *action.GetTargetByIDRequest
want *action.GetTargetByIDResponse
wantErr bool
}{
{
name: "GetTargetByID, request and response, ok",
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) {
instanceID := Tester.Instance.InstanceID()
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID
// create target for target changes
targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1)
targetCreatedURL := "https://nonexistent"
targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false)
// request received by target
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request}
changedRequest := &action.GetTargetByIDRequest{TargetId: targetCreated.GetId()}
// replace original request with different targetID
urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusOK, changedRequest)
targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, false)
Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId()))
// GetTargetByID with used target
request.TargetId = targetRequest.GetId()
// expected response from the GetTargetByID
expectedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: targetCreated.GetId(),
Details: targetCreated.GetDetails(),
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
}
// has to be set separately because of the pointers
response.Target = &action.Target{
TargetId: targetCreated.GetId(),
Details: targetCreated.GetDetails(),
Name: targetCreatedName,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
}
// content for partial update
changedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: "changed",
},
}
// change partial updated content on returned response
response.Target.TargetId = changedResponse.Target.TargetId
// response received by target
wantResponse := &middleware.ContextInfoResponse{
FullMethod: fullMethod,
InstanceID: instanceID,
OrgID: orgID,
ProjectID: projectID,
UserID: userID,
Request: changedRequest,
Response: expectedResponse,
}
// after request with different targetID, return changed response
targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusOK, changedResponse)
targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, false)
Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId()))
return func() {
closeRequest()
closeResponse()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetByIDRequest{},
want: &action.GetTargetByIDResponse{},
},
/*{
name: "GetTargetByID, request, interrupt",
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) {
fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID"
instanceID := Tester.Instance.InstanceID()
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID
// request received by target
wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instanceID, OrgID: orgID, ProjectID: projectID, UserID: userID, Request: request}
urlRequest, closeRequest := testServerCall(wantRequest, 0, http.StatusInternalServerError, &action.GetTargetByIDRequest{TargetId: "notchanged"})
targetRequest := Tester.CreateTarget(ctx, t, "", urlRequest, domain.TargetTypeCall, true)
Tester.SetExecution(ctx, t, conditionRequestFullMethod(fullMethod), executionTargetsSingleTarget(targetRequest.GetId()))
// GetTargetByID with used target
request.TargetId = targetRequest.GetId()
return func() {
closeRequest()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod))
},
req: &action.GetTargetByIDRequest{},
wantErr: true,
},
{
name: "GetTargetByID, response, interrupt",
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) (func(), error) {
fullMethod := "/zitadel.action.v3alpha.ActionService/GetTargetByID"
instanceID := Tester.Instance.InstanceID()
orgID := Tester.Organisation.ID
projectID := ""
userID := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner).ID
// create target for target changes
targetCreatedName := fmt.Sprint("GetTargetByID", time.Now().UnixNano()+1)
targetCreatedURL := "https://nonexistent"
targetCreated := Tester.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false)
// GetTargetByID with used target
request.TargetId = targetCreated.GetId()
// expected response from the GetTargetByID
expectedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: targetCreated.GetId(),
Details: targetCreated.GetDetails(),
Name: targetCreatedName,
Endpoint: targetCreatedURL,
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
}
// content for partial update
changedResponse := &action.GetTargetByIDResponse{
Target: &action.Target{
TargetId: "changed",
},
}
// response received by target
wantResponse := &middleware.ContextInfoResponse{
FullMethod: fullMethod,
InstanceID: instanceID,
OrgID: orgID,
ProjectID: projectID,
UserID: userID,
Request: request,
Response: expectedResponse,
}
// after request with different targetID, return changed response
targetResponseURL, closeResponse := testServerCall(wantResponse, 0, http.StatusInternalServerError, changedResponse)
targetResponse := Tester.CreateTarget(ctx, t, "", targetResponseURL, domain.TargetTypeCall, true)
Tester.SetExecution(ctx, t, conditionResponseFullMethod(fullMethod), executionTargetsSingleTarget(targetResponse.GetId()))
return func() {
closeResponse()
}, nil
},
clean: func(ctx context.Context) {
Tester.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod))
},
req: &action.GetTargetByIDRequest{},
wantErr: true,
},*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.dep != nil {
close, err := tt.dep(tt.ctx, tt.req, tt.want)
require.NoError(t, err)
defer close()
}
got, err := Client.GetTargetByID(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget())
assert.Equal(t, tt.want.Target.TargetId, got.Target.TargetId)
if tt.clean != nil {
tt.clean(tt.ctx)
}
})
}
}
func conditionRequestFullMethod(fullMethod string) *action.Condition {
return &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: fullMethod,
},
},
},
}
}
func conditionResponseFullMethod(fullMethod string) *action.Condition {
return &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: fullMethod,
},
},
},
}
}
func testServerCall(
reqBody interface{},
sleep time.Duration,
statusCode int,
respBody interface{},
) (string, func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(reqBody)
if err != nil {
http.Error(w, "error, marshall: "+err.Error(), http.StatusInternalServerError)
return
}
sentBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "error, read body: "+err.Error(), http.StatusInternalServerError)
return
}
if !reflect.DeepEqual(data, sentBody) {
http.Error(w, "error, equal:\n"+string(data)+"\nsent:\n"+string(sentBody), http.StatusInternalServerError)
return
}
if statusCode != http.StatusOK {
http.Error(w, "error, statusCode", statusCode)
return
}
time.Sleep(sleep)
w.Header().Set("Content-Type", "application/json")
resp, err := json.Marshal(respBody)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if _, err := io.WriteString(w, string(resp)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
return server.URL, server.Close
}
+91 -25
View File
@@ -2,6 +2,7 @@ package action
import (
"context"
"strings"
"google.golang.org/protobuf/types/known/durationpb"
@@ -67,8 +68,6 @@ func targetFieldNameToSortingColumn(field action.TargetFieldName) query.Column {
return query.TargetColumnURL
case action.TargetFieldName_FIELD_NAME_TIMEOUT:
return query.TargetColumnTimeout
case action.TargetFieldName_FIELD_NAME_ASYNC:
return query.TargetColumnAsync
case action.TargetFieldName_FIELD_NAME_INTERRUPT_ON_ERROR:
return query.TargetColumnInterruptOnError
default:
@@ -134,19 +133,16 @@ func targetToPb(t *query.Target) *action.Target {
TargetId: t.ID,
Name: t.Name,
Timeout: durationpb.New(t.Timeout),
}
if t.Async {
target.ExecutionType = &action.Target_IsAsync{IsAsync: t.Async}
}
if t.InterruptOnError {
target.ExecutionType = &action.Target_InterruptOnError{InterruptOnError: t.InterruptOnError}
Endpoint: t.Endpoint,
}
switch t.TargetType {
case domain.TargetTypeWebhook:
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{Url: t.URL}}
case domain.TargetTypeRequestResponse:
target.TargetType = &action.Target_RestRequestResponse{RestRequestResponse: &action.SetRESTRequestResponse{Url: t.URL}}
target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.SetRESTWebhook{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeCall:
target.TargetType = &action.Target_RestCall{RestCall: &action.SetRESTCall{InterruptOnError: t.InterruptOnError}}
case domain.TargetTypeAsync:
target.TargetType = &action.Target_RestAsync{RestAsync: &action.SetRESTAsync{}}
default:
target.TargetType = nil
}
@@ -205,10 +201,14 @@ func executionQueryToQuery(searchQuery *action.SearchQuery) (query.SearchQuery,
return inConditionsQueryToQuery(q.InConditionsQuery)
case *action.SearchQuery_ExecutionTypeQuery:
return executionTypeToQuery(q.ExecutionTypeQuery)
case *action.SearchQuery_TargetQuery:
return query.NewExecutionTargetSearchQuery(q.TargetQuery.GetTargetId())
case *action.SearchQuery_IncludeQuery:
return query.NewExecutionIncludeSearchQuery(q.IncludeQuery.GetInclude())
include, err := conditionToInclude(q.IncludeQuery.GetInclude())
if err != nil {
return nil, err
}
return query.NewIncludeSearchQuery(include)
case *action.SearchQuery_TargetQuery:
return query.NewTargetSearchQuery(q.TargetQuery.GetTargetId())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
@@ -267,7 +267,7 @@ func conditionToID(q *action.Condition) (string, error) {
}
return cond.ID(), nil
case *action.Condition_Function:
return t.Function, nil
return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
@@ -282,17 +282,83 @@ func executionsToPb(executions []*query.Execution) []*action.Execution {
}
func executionToPb(e *query.Execution) *action.Execution {
var targets, includes []string
if len(e.Targets) > 0 {
targets = e.Targets
}
if len(e.Includes) > 0 {
includes = e.Includes
targets := make([]*action.ExecutionTargetType, len(e.Targets))
for i := range e.Targets {
switch e.Targets[i].Type {
case domain.ExecutionTargetTypeInclude:
targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Include{Include: executionIDToCondition(e.Targets[i].Target)}}
case domain.ExecutionTargetTypeTarget:
targets[i] = &action.ExecutionTargetType{Type: &action.ExecutionTargetType_Target{Target: e.Targets[i].Target}}
case domain.ExecutionTargetTypeUnspecified:
continue
default:
continue
}
}
return &action.Execution{
Details: object.DomainToDetailsPb(&e.ObjectDetails),
ExecutionId: e.ID,
Targets: targets,
Includes: includes,
Details: object.DomainToDetailsPb(&e.ObjectDetails),
Condition: executionIDToCondition(e.ID),
Targets: targets,
}
}
func executionIDToCondition(include string) *action.Condition {
if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) {
return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) {
return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) {
return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String()))
}
if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) {
return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String()))
}
return nil
}
func includeRequestToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 2:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}}
case 1:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case 0:
return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}
default:
return nil
}
}
func includeResponseToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 2:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}}
case 1:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}}
case 0:
return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}
default:
return nil
}
}
func includeEventToCondition(id string) *action.Condition {
switch strings.Count(id, "/") {
case 1:
if strings.HasSuffix(id, command.EventGroupSuffix) {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}}
} else {
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}}
}
case 0:
return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}
default:
return nil
}
}
func includeFunctionToCondition(id string) *action.Condition {
return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}}
}
@@ -5,6 +5,7 @@ package action_test
import (
"context"
"fmt"
"reflect"
"testing"
"time"
@@ -12,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -52,7 +54,7 @@ func TestServer_GetTargetByID(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
@@ -69,10 +71,9 @@ func TestServer_GetTargetByID(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
},
@@ -84,7 +85,7 @@ func TestServer_GetTargetByID(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, true, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
@@ -101,23 +102,21 @@ func TestServer_GetTargetByID(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_IsAsync{IsAsync: true},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, interruptOnError, ok",
name: "get, webhook interruptOnError, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, true)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
@@ -134,13 +133,79 @@ func TestServer_GetTargetByID(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, call, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, call interruptOnError, ok",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.GetTargetByIDRequest, response *action.GetTargetByIDResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true)
request.TargetId = resp.GetId()
response.Target.TargetId = resp.GetId()
response.Target.Name = name
response.Target.Details.ResourceOwner = resp.GetDetails().GetResourceOwner()
response.Target.Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Target.Details.Sequence = resp.GetDetails().GetSequence()
return nil
},
req: &action.GetTargetByIDRequest{},
},
want: &action.GetTargetByIDResponse{
Target: &action.Target{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
@@ -163,15 +228,11 @@ func TestServer_GetTargetByID(t *testing.T) {
assert.Error(ttt, getErr, "Error: "+getErr.Error())
} else {
assert.NoError(ttt, getErr)
}
if getErr != nil {
fmt.Println("Error: " + getErr.Error())
return
}
integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget())
integration.AssertDetails(t, tt.want.GetTarget(), got.GetTarget())
assert.Equal(t, tt.want.Target, got.Target)
assert.Equal(t, tt.want.Target, got.Target)
}
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
@@ -227,14 +288,14 @@ func TestServer_ListTargets(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{resp.GetId()},
},
}
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
//response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
@@ -255,9 +316,10 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
@@ -270,7 +332,7 @@ func TestServer_ListTargets(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
name := fmt.Sprint(time.Now().UnixNano() + 1)
resp := Tester.CreateTargetWithNameAndType(ctx, t, name, false, false)
resp := Tester.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false)
request.Queries[0].Query = &action.TargetSearchQuery_TargetNameQuery{
TargetNameQuery: &action.TargetNameQuery{
TargetName: name,
@@ -298,9 +360,10 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
@@ -316,9 +379,9 @@ func TestServer_ListTargets(t *testing.T) {
name1 := fmt.Sprint(time.Now().UnixNano() + 1)
name2 := fmt.Sprint(time.Now().UnixNano() + 3)
name3 := fmt.Sprint(time.Now().UnixNano() + 5)
resp1 := Tester.CreateTargetWithNameAndType(ctx, t, name1, false, false)
resp2 := Tester.CreateTargetWithNameAndType(ctx, t, name2, true, false)
resp3 := Tester.CreateTargetWithNameAndType(ctx, t, name3, false, true)
resp1 := Tester.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false)
resp2 := Tester.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true)
resp3 := Tester.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false)
request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
@@ -354,9 +417,10 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
@@ -365,25 +429,23 @@ func TestServer_ListTargets(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_IsAsync{IsAsync: true},
Timeout: durationpb.New(10 * time.Second),
},
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true},
Timeout: durationpb.New(10 * time.Second),
},
},
},
@@ -422,9 +484,9 @@ func TestServer_ListTargets(t *testing.T) {
}
}
func TestServer_ListExecutions_Request(t *testing.T) {
func TestServer_ListExecutions(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
type args struct {
ctx context.Context
@@ -446,17 +508,20 @@ func TestServer_ListExecutions_Request(t *testing.T) {
wantErr: true,
},
{
name: "list single condition",
name: "list request single condition",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
resp := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{})
cond := request.Queries[0].GetInConditionsQuery().GetConditions()[0]
resp := Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId()))
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
// response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
// Set expected response with used values for SetExecution
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].Condition = cond
return nil
},
req: &action.ListExecutionsRequest{
@@ -471,8 +536,7 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
},
},
}},
},
},
}},
@@ -487,18 +551,26 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession",
Targets: []string{targetResp.GetId()},
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/GetSession",
},
},
},
},
Targets: executionTargetsSingleTarget(targetResp.GetId()),
},
},
},
},
{
name: "list single target",
name: "list request single target",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
target := Tester.CreateTarget(ctx, t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
// add target as query to the request
request.Queries[0] = &action.SearchQuery{
Query: &action.SearchQuery_TargetQuery{
@@ -507,7 +579,7 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
}
resp := Tester.SetExecution(ctx, t, &action.Condition{
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
@@ -515,14 +587,17 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
}, []string{target.GetId()}, []string{})
}
targets := executionTargetsSingleTarget(target.GetId())
resp := Tester.SetExecution(ctx, t, cond, targets)
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].Targets[0] = target.GetId()
response.Result[0].Condition = cond
response.Result[0].Targets = targets
return nil
},
req: &action.ListExecutionsRequest{
@@ -538,17 +613,17 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.management.v1.ManagementService/UpdateAction",
Targets: []string{""},
Condition: &action.Condition{},
Targets: executionTargetsSingleTarget(""),
},
},
},
}, {
name: "list single include",
name: "list request single include",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
Tester.SetExecution(ctx, t, &action.Condition{
cond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
@@ -556,8 +631,11 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
}, []string{targetResp.GetId()}, []string{})
resp2 := Tester.SetExecution(ctx, t, &action.Condition{
}
Tester.SetExecution(ctx, t, cond, executionTargetsSingleTarget(targetResp.GetId()))
request.Queries[0].GetIncludeQuery().Include = cond
includeCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
@@ -565,19 +643,23 @@ func TestServer_ListExecutions_Request(t *testing.T) {
},
},
},
}, []string{}, []string{"request./zitadel.management.v1.ManagementService/GetAction"})
}
includeTargets := executionTargetsSingleInclude(cond)
resp2 := Tester.SetExecution(ctx, t, includeCond, includeTargets)
response.Details.Timestamp = resp2.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp2.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp2.GetDetails().GetSequence()
response.Result[0].Condition = includeCond
response.Result[0].Targets = includeTargets
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_IncludeQuery{
IncludeQuery: &action.IncludeQuery{Include: "request./zitadel.management.v1.ManagementService/GetAction"},
IncludeQuery: &action.IncludeQuery{},
},
}},
},
@@ -591,8 +673,6 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.management.v1.ManagementService/ListActions",
Includes: []string{"request./zitadel.management.v1.ManagementService/GetAction"},
},
},
},
@@ -603,19 +683,32 @@ func TestServer_ListExecutions_Request(t *testing.T) {
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
resp1 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[0], []string{targetResp.GetId()}, []string{})
cond1 := request.Queries[0].GetInConditionsQuery().GetConditions()[0]
targets1 := executionTargetsSingleTarget(targetResp.GetId())
resp1 := Tester.SetExecution(ctx, t, cond1, targets1)
response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence()
response.Result[0].Condition = cond1
response.Result[0].Targets = targets1
resp2 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[1], []string{targetResp.GetId()}, []string{})
cond2 := request.Queries[0].GetInConditionsQuery().GetConditions()[1]
targets2 := executionTargetsSingleTarget(targetResp.GetId())
resp2 := Tester.SetExecution(ctx, t, cond2, targets2)
response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence()
response.Result[1].Condition = cond2
response.Result[1].Targets = targets2
resp3 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[2], []string{targetResp.GetId()}, []string{})
response.Details.Timestamp = resp3.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp3.GetDetails().GetSequence()
cond3 := request.Queries[0].GetInConditionsQuery().GetConditions()[2]
targets3 := executionTargetsSingleTarget(targetResp.GetId())
resp3 := Tester.SetExecution(ctx, t, cond3, targets3)
response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate()
response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence()
response.Result[2].Condition = cond3
response.Result[2].Targets = targets3
response.Details.Timestamp = resp3.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp3.GetDetails().GetSequence()
return nil
},
req: &action.ListExecutionsRequest{
@@ -665,24 +758,77 @@ func TestServer_ListExecutions_Request(t *testing.T) {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession",
Targets: []string{targetResp.GetId()},
}, {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/CreateSession",
Targets: []string{targetResp.GetId()},
}, {
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/SetSession",
Targets: []string{targetResp.GetId()},
},
},
},
},
{
name: "list multiple conditions all types",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
targets := executionTargetsSingleTarget(targetResp.GetId())
for i, cond := range request.Queries[0].GetInConditionsQuery().GetConditions() {
resp := Tester.SetExecution(ctx, t, cond, targets)
response.Result[i].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[i].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[i].Condition = cond
response.Result[i].Targets = targets
// filled with info of last sequence
response.Details.Timestamp = resp.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp.GetDetails().GetSequence()
}
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_InConditionsQuery{
InConditionsQuery: &action.InConditionsQuery{
Conditions: []*action.Condition{
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}},
{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2beta.SessionService/GetSession"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2beta.SessionService"}}}},
{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}},
{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}},
{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"}}},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 10,
},
Result: []*action.Execution{
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
{Details: &object.Details{ResourceOwner: Tester.Instance.InstanceID()}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -699,20 +845,33 @@ func TestServer_ListExecutions_Request(t *testing.T) {
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, listErr, "Error: "+listErr.Error())
assert.Error(t, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(ttt, listErr)
assert.NoError(t, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
assert.Len(t, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
// as not sorted, all elements have to be checked
// workaround as oneof elements can only be checked with assert.EqualExportedValues()
if j, found := containExecution(got.Result, tt.want.Result[i]); found {
assert.EqualExportedValues(t, tt.want.Result[i], got.Result[j])
}
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func containExecution(executionList []*action.Execution, execution *action.Execution) (int, bool) {
for i, exec := range executionList {
if reflect.DeepEqual(exec.Details, execution.Details) {
return i, true
}
}
return 0, false
}
+1 -1
View File
@@ -66,5 +66,5 @@ func checkExecutionEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().Actions {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-141bwx3lef", "Errors.action.NotEnabled")
return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled")
}
+26 -21
View File
@@ -58,23 +58,26 @@ func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetReque
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
var targetType domain.TargetType
var url string
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := req.GetTargetType().(type) {
case *action.CreateTargetRequest_RestWebhook:
targetType = domain.TargetTypeWebhook
url = t.RestWebhook.GetUrl()
case *action.CreateTargetRequest_RestRequestResponse:
targetType = domain.TargetTypeRequestResponse
url = t.RestRequestResponse.GetUrl()
interruptOnError = t.RestWebhook.InterruptOnError
case *action.CreateTargetRequest_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.CreateTargetRequest_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: req.GetName(),
TargetType: targetType,
URL: url,
Endpoint: req.GetEndpoint(),
Timeout: req.GetTimeout().AsDuration(),
Async: req.GetIsAsync(),
InterruptOnError: req.GetInterruptOnError(),
InterruptOnError: interruptOnError,
}
}
@@ -86,22 +89,24 @@ func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarge
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetTargetId(),
},
Name: req.Name,
Name: req.Name,
Endpoint: req.Endpoint,
}
switch t := req.GetTargetType().(type) {
case *action.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.URL = gu.Ptr(t.RestWebhook.GetUrl())
case *action.UpdateTargetRequest_RestRequestResponse:
target.TargetType = gu.Ptr(domain.TargetTypeRequestResponse)
target.URL = gu.Ptr(t.RestRequestResponse.GetUrl())
if req.TargetType != nil {
switch t := req.GetTargetType().(type) {
case *action.UpdateTargetRequest_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.UpdateTargetRequest_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.UpdateTargetRequest_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if req.Timeout != nil {
target.Timeout = gu.Ptr(req.GetTimeout().AsDuration())
}
if req.ExecutionType != nil {
target.Async = gu.Ptr(req.GetIsAsync())
target.InterruptOnError = gu.Ptr(req.GetInterruptOnError())
}
return target
}
@@ -14,6 +14,7 @@ import (
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@@ -69,8 +70,8 @@ func TestServer_CreateTarget(t *testing.T) {
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{},
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
@@ -79,29 +80,25 @@ func TestServer_CreateTarget(t *testing.T) {
name: "empty timeout",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
ExecutionType: nil,
Timeout: nil,
},
wantErr: true,
},
{
name: "empty execution type, ok",
name: "async, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: nil,
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
@@ -111,19 +108,17 @@ func TestServer_CreateTarget(t *testing.T) {
},
},
{
name: "async execution, ok",
name: "webhook, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_IsAsync{
IsAsync: true,
},
},
want: &action.CreateTargetResponse{
Details: &object.Details{
@@ -133,20 +128,59 @@ func TestServer_CreateTarget(t *testing.T) {
},
},
{
name: "interrupt on error execution, ok",
name: "webhook, interrupt on error, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, interruptOnError, ok",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
@@ -186,7 +220,7 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
@@ -215,7 +249,7 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
@@ -235,16 +269,16 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com",
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
@@ -259,18 +293,14 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/new",
},
},
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
want: &action.UpdateTargetResponse{
@@ -283,7 +313,7 @@ func TestServer_UpdateTarget(t *testing.T) {
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId()
request.TargetId = targetID
return nil
},
@@ -301,17 +331,17 @@ func TestServer_UpdateTarget(t *testing.T) {
},
},
{
name: "change execution type, ok",
name: "change type async, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
ExecutionType: &action.UpdateTargetRequest_IsAsync{
IsAsync: true,
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
@@ -341,7 +371,7 @@ func TestServer_UpdateTarget(t *testing.T) {
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
+89 -52
View File
@@ -27,55 +27,64 @@ func Test_createTargetToCommand(t *testing.T) {
args: args{nil},
want: &command.AddTarget{
Name: "",
URL: "",
Endpoint: "",
Timeout: 0,
Async: false,
InterruptOnError: false,
},
},
{
name: "all fields (async webhook)",
name: "all fields (webhook)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/1",
},
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_IsAsync{
IsAsync: true,
},
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
URL: "https://example.com/hooks/1",
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (async)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeAsync,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
Async: true,
InterruptOnError: false,
},
},
{
name: "all fields (interrupting response)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
TargetType: &action.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com/hooks/1",
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeRequestResponse,
URL: "https://example.com/hooks/1",
TargetType: domain.TargetTypeCall,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
Async: false,
InterruptOnError: true,
},
},
@@ -105,80 +114,108 @@ func Test_updateTargetToCommand(t *testing.T) {
{
name: "all fields nil",
args: args{&action.UpdateTargetRequest{
Name: nil,
TargetType: nil,
Timeout: nil,
ExecutionType: nil,
Name: nil,
TargetType: nil,
Timeout: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
URL: nil,
Endpoint: nil,
Timeout: nil,
Async: nil,
InterruptOnError: nil,
},
},
{
name: "all fields empty",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
ExecutionType: nil,
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
URL: nil,
Endpoint: nil,
Timeout: gu.Ptr(0 * time.Second),
Async: nil,
InterruptOnError: nil,
},
},
{
name: "all fields (async webhook)",
name: "all fields (webhook)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/1",
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.UpdateTargetRequest_IsAsync{
IsAsync: true,
},
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
URL: gu.Ptr("https://example.com/hooks/1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (webhook interrupt)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeWebhook),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
{
name: "all fields (async)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeAsync),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(true),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (interrupting response)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
TargetType: &action.UpdateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com/hooks/1",
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.UpdateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.UpdateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeRequestResponse),
URL: gu.Ptr("https://example.com/hooks/1"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(false),
InterruptOnError: gu.Ptr(true),
},
},
@@ -0,0 +1,179 @@
package middleware
import (
"context"
"encoding/json"
"strings"
"github.com/zitadel/logging"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
"github.com/zitadel/zitadel/internal/query"
exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func ExecutionHandler(queries *query.Queries) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
requestTargets, responseTargets := queryTargets(ctx, queries, info.FullMethod)
// call targets otherwise return req
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req)
if err != nil {
return nil, err
}
response, err := handler(ctx, handledReq)
if err != nil {
return nil, err
}
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response)
}
}
func executeTargetsForRequest(ctx context.Context, targets []execution.Target, fullMethod string, req interface{}) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
// if no targets are found, return without any calls
if len(targets) == 0 {
return req, nil
}
ctxData := authz.GetCtxData(ctx)
info := &ContextInfoRequest{
FullMethod: fullMethod,
InstanceID: authz.GetInstance(ctx).InstanceID(),
ProjectID: ctxData.ProjectID,
OrgID: ctxData.OrgID,
UserID: ctxData.UserID,
Request: req,
}
return execution.CallTargets(ctx, targets, info)
}
func executeTargetsForResponse(ctx context.Context, targets []execution.Target, fullMethod string, req, resp interface{}) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
// if no targets are found, return without any calls
if len(targets) == 0 {
return resp, nil
}
ctxData := authz.GetCtxData(ctx)
info := &ContextInfoResponse{
FullMethod: fullMethod,
InstanceID: authz.GetInstance(ctx).InstanceID(),
ProjectID: ctxData.ProjectID,
OrgID: ctxData.OrgID,
UserID: ctxData.UserID,
Request: req,
Response: resp,
}
return execution.CallTargets(ctx, targets, info)
}
type ExecutionQueries interface {
TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*query.ExecutionTarget, err error)
}
func queryTargets(
ctx context.Context,
queries ExecutionQueries,
fullMethod string,
) ([]execution.Target, []execution.Target) {
ctx, span := tracing.NewSpan(ctx)
defer span.End()
targets, err := queries.TargetsByExecutionIDs(ctx,
idsForFullMethod(fullMethod, domain.ExecutionTypeRequest),
idsForFullMethod(fullMethod, domain.ExecutionTypeResponse),
)
requestTargets := make([]execution.Target, 0, len(targets))
responseTargets := make([]execution.Target, 0, len(targets))
if err != nil {
logging.WithFields("fullMethod", fullMethod).WithError(err).Info("unable to query targets")
return requestTargets, responseTargets
}
for _, target := range targets {
if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeRequest)) {
requestTargets = append(requestTargets, target)
} else if strings.HasPrefix(target.GetExecutionID(), exec_repo.IDAll(domain.ExecutionTypeResponse)) {
responseTargets = append(responseTargets, target)
}
}
return requestTargets, responseTargets
}
func idsForFullMethod(fullMethod string, executionType domain.ExecutionType) []string {
return []string{exec_repo.ID(executionType, fullMethod), exec_repo.ID(executionType, serviceFromFullMethod(fullMethod)), exec_repo.IDAll(executionType)}
}
func serviceFromFullMethod(s string) string {
parts := strings.Split(s, "/")
return parts[1]
}
var _ execution.ContextInfo = &ContextInfoRequest{}
type ContextInfoRequest struct {
FullMethod string `json:"fullMethod,omitempty"`
InstanceID string `json:"instanceID,omitempty"`
OrgID string `json:"orgID,omitempty"`
ProjectID string `json:"projectID,omitempty"`
UserID string `json:"userID,omitempty"`
Request interface{} `json:"request,omitempty"`
}
func (c *ContextInfoRequest) GetHTTPRequestBody() []byte {
data, err := json.Marshal(c)
if err != nil {
return nil
}
return data
}
func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error {
return json.Unmarshal(resp, c.Request)
}
func (c *ContextInfoRequest) GetContent() interface{} {
return c.Request
}
var _ execution.ContextInfo = &ContextInfoResponse{}
type ContextInfoResponse struct {
FullMethod string `json:"fullMethod,omitempty"`
InstanceID string `json:"instanceID,omitempty"`
OrgID string `json:"orgID,omitempty"`
ProjectID string `json:"projectID,omitempty"`
UserID string `json:"userID,omitempty"`
Request interface{} `json:"request,omitempty"`
Response interface{} `json:"response,omitempty"`
}
func (c *ContextInfoResponse) GetHTTPRequestBody() []byte {
data, err := json.Marshal(c)
if err != nil {
return nil
}
return data
}
func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error {
return json.Unmarshal(resp, c.Response)
}
func (c *ContextInfoResponse) GetContent() interface{} {
return c.Response
}
@@ -0,0 +1,778 @@
package middleware
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/execution"
)
var _ execution.Target = &mockExecutionTarget{}
type mockExecutionTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *mockExecutionTarget) SetEndpoint(endpoint string) {
e.Endpoint = endpoint
}
func (e *mockExecutionTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *mockExecutionTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *mockExecutionTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *mockExecutionTarget) GetTimeout() time.Duration {
return e.Timeout
}
func (e *mockExecutionTarget) GetTargetID() string {
return e.TargetID
}
func (e *mockExecutionTarget) GetExecutionID() string {
return e.ExecutionID
}
type mockContentRequest struct {
Content string
}
func newMockContentRequest(content string) *mockContentRequest {
return &mockContentRequest{
Content: content,
}
}
func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest {
return &ContextInfoRequest{
FullMethod: fullMethod,
Request: newMockContentRequest(request),
}
}
func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse {
return &ContextInfoResponse{
FullMethod: fullMethod,
Request: newMockContentRequest(request),
Response: newMockContentRequest(response),
}
}
func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
type target struct {
reqBody execution.ContextInfo
sleep time.Duration
statusCode int
respBody interface{}
}
type args struct {
ctx context.Context
executionTargets []execution.Target
targets []target
fullMethod string
req interface{}
}
type res struct {
want interface{}
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"target, executionTargets nil",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: nil,
req: newMockContentRequest("request"),
},
res{
want: newMockContentRequest("request"),
},
},
{
"target, executionTargets empty",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{},
req: newMockContentRequest("request"),
},
res{
want: newMockContentRequest("request"),
},
},
{
"target, not reachable",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, error without interrupt",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusBadRequest,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"target, interruptOnError",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusBadRequest,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, wrong request",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{reqBody: newMockContextInfoRequest("/service/method", "wrong")},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"target, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content1"),
},
},
{
"target async, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Second,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"target async, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeAsync,
Timeout: time.Minute,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"webhook, error",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
sleep: 0,
statusCode: http.StatusInternalServerError,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"webhook, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"webhook, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
want: newMockContentRequest("content"),
},
},
{
"with includes, interruptOnError",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target1",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target2",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target3",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content1"),
respBody: newMockContentRequest("content2"),
sleep: 0,
statusCode: http.StatusBadRequest,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content2"),
respBody: newMockContentRequest("content3"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
{
"with includes, timeout",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target1",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target2",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target3",
TargetType: domain.TargetTypeCall,
Timeout: time.Second,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest("content1"),
sleep: 0,
statusCode: http.StatusOK,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content1"),
respBody: newMockContentRequest("content2"),
sleep: 5 * time.Second,
statusCode: http.StatusBadRequest,
},
{
reqBody: newMockContextInfoRequest("/service/method", "content2"),
respBody: newMockContentRequest("content3"),
sleep: 5 * time.Second,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("content"),
},
res{
wantErr: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
closeFuncs := make([]func(), len(tt.args.targets))
for i, target := range tt.args.targets {
url, closeF := testServerCall(
target.reqBody,
target.sleep,
target.statusCode,
target.respBody,
)
et := tt.args.executionTargets[i].(*mockExecutionTarget)
et.SetEndpoint(url)
closeFuncs[i] = closeF
}
resp, err := executeTargetsForRequest(
tt.args.ctx,
tt.args.executionTargets,
tt.args.fullMethod,
tt.args.req,
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.want, resp)
for _, closeF := range closeFuncs {
closeF()
}
})
}
}
func testServerCall(
reqBody interface{},
sleep time.Duration,
statusCode int,
respBody interface{},
) (string, func()) {
handler := func(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(reqBody)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
sentBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if !reflect.DeepEqual(data, sentBody) {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if statusCode != http.StatusOK {
http.Error(w, "error", statusCode)
return
}
time.Sleep(sleep)
w.Header().Set("Content-Type", "application/json")
resp, err := json.Marshal(respBody)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
if _, err := io.WriteString(w, string(resp)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
return server.URL, server.Close
}
func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
type target struct {
reqBody execution.ContextInfo
sleep time.Duration
statusCode int
respBody interface{}
}
type args struct {
ctx context.Context
executionTargets []execution.Target
targets []target
fullMethod string
req interface{}
resp interface{}
}
type res struct {
want interface{}
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"target, executionTargets nil",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: nil,
req: newMockContentRequest("request"),
resp: newMockContentRequest("response"),
},
res{
want: newMockContentRequest("response"),
},
},
{
"target, executionTargets empty",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{},
req: newMockContentRequest("request"),
resp: newMockContentRequest("response"),
},
res{
want: newMockContentRequest("response"),
},
},
{
"target, empty response",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "request./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoRequest("/service/method", "content"),
respBody: newMockContentRequest(""),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: []byte{},
},
res{
wantErr: true,
},
},
{
"target, ok",
args{
ctx: context.Background(),
fullMethod: "/service/method",
executionTargets: []execution.Target{
&mockExecutionTarget{
InstanceID: "instance",
ExecutionID: "response./zitadel.session.v2beta.SessionService/SetSession",
TargetID: "target",
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
InterruptOnError: true,
},
},
targets: []target{
{
reqBody: newMockContextInfoResponse("/service/method", "request", "response"),
respBody: newMockContentRequest("response1"),
sleep: 0,
statusCode: http.StatusOK,
},
},
req: newMockContentRequest("request"),
resp: newMockContentRequest("response"),
},
res{
want: newMockContentRequest("response1"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
closeFuncs := make([]func(), len(tt.args.targets))
for i, target := range tt.args.targets {
url, closeF := testServerCall(
target.reqBody,
target.sleep,
target.statusCode,
target.respBody,
)
et := tt.args.executionTargets[i].(*mockExecutionTarget)
et.SetEndpoint(url)
closeFuncs[i] = closeF
}
resp, err := executeTargetsForResponse(
tt.args.ctx,
tt.args.executionTargets,
tt.args.fullMethod,
tt.args.req,
tt.args.resp,
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.want, resp)
for _, closeF := range closeFuncs {
closeF()
}
})
}
}
+1
View File
@@ -58,6 +58,7 @@ func CreateServer(
middleware.AuthorizationInterceptor(verifier, authConfig),
middleware.TranslationHandler(),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
middleware.ExecutionHandler(queries),
middleware.ValidationHandler(),
middleware.ServiceHandler(),
middleware.ActivityInterceptor(),
+37 -11
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"strings"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -9,6 +10,10 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
EventGroupSuffix = ".*"
)
type ExecutionAPICondition struct {
Method string
Service string
@@ -134,7 +139,11 @@ func (e *ExecutionEventCondition) ID() string {
return execution.ID(domain.ExecutionTypeEvent, e.Event)
}
if e.Group != "" {
return execution.ID(domain.ExecutionTypeEvent, e.Group)
group := e.Group
if !strings.HasSuffix(e.Group, EventGroupSuffix) {
group += EventGroupSuffix
}
return execution.ID(domain.ExecutionTypeEvent, group)
}
if e.All {
return execution.IDAll(domain.ExecutionTypeEvent)
@@ -168,25 +177,43 @@ func (c *Commands) SetExecutionEvent(ctx context.Context, cond *ExecutionEventCo
type SetExecution struct {
models.ObjectRoot
Targets []string
Includes []string
Targets []*execution.Target
}
func (t SetExecution) GetIncludes() []string {
includes := make([]string, 0)
for i := range t.Targets {
if t.Targets[i].Type == domain.ExecutionTargetTypeInclude {
includes = append(includes, t.Targets[i].Target)
}
}
return includes
}
func (t SetExecution) GetTargets() []string {
targets := make([]string, 0)
for i := range t.Targets {
if t.Targets[i].Type == domain.ExecutionTargetTypeTarget {
targets = append(targets, t.Targets[i].Target)
}
}
return targets
}
func (e *SetExecution) IsValid() error {
if len(e.Targets) == 0 && len(e.Includes) == 0 {
if len(e.Targets) == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-56bteot2uj", "Errors.Execution.NoTargets")
}
if len(e.Targets) > 0 && len(e.Includes) > 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-5zleae34r1", "Errors.Execution.Invalid")
}
return nil
}
func (e *SetExecution) Existing(c *Commands, ctx context.Context, resourceOwner string) error {
if len(e.Targets) > 0 && !c.existsTargetsByIDs(ctx, e.Targets, resourceOwner) {
targets := e.GetTargets()
if len(targets) > 0 && !c.existsTargetsByIDs(ctx, targets, resourceOwner) {
return zerrors.ThrowNotFound(nil, "COMMAND-17e8fq1ggk", "Errors.Target.NotFound")
}
if len(e.Includes) > 0 && !c.existsExecutionsByIDs(ctx, e.Includes, resourceOwner) {
includes := e.GetIncludes()
if len(includes) > 0 && !c.existsExecutionsByIDs(ctx, includes, resourceOwner) {
return zerrors.ThrowNotFound(nil, "COMMAND-slgj0l4cdz", "Errors.Execution.IncludeNotFound")
}
return nil
@@ -206,11 +233,10 @@ func (c *Commands) setExecution(ctx context.Context, set *SetExecution, resource
return nil, err
}
if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEvent(
if err := c.pushAppendAndReduce(ctx, wm, execution.NewSetEventV2(
ctx,
ExecutionAggregateFromWriteModel(&wm.WriteModel),
set.Targets,
set.Includes,
)); err != nil {
return nil, err
}
+13 -3
View File
@@ -10,12 +10,13 @@ import (
type ExecutionWriteModel struct {
eventstore.WriteModel
Targets []string
Includes []string
Targets []string
Includes []string
ExecutionTargets []*execution.Target
}
func (e *ExecutionWriteModel) Exists() bool {
return len(e.Targets) > 0 || len(e.Includes) > 0
return len(e.ExecutionTargets) > 0 || len(e.Includes) > 0 || len(e.Targets) > 0
}
func NewExecutionWriteModel(id string, resourceOwner string) *ExecutionWriteModel {
@@ -34,9 +35,12 @@ func (wm *ExecutionWriteModel) Reduce() error {
case *execution.SetEvent:
wm.Targets = e.Targets
wm.Includes = e.Includes
case *execution.SetEventV2:
wm.ExecutionTargets = e.Targets
case *execution.RemovedEvent:
wm.Targets = nil
wm.Includes = nil
wm.ExecutionTargets = nil
}
}
return wm.WriteModel.Reduce()
@@ -49,6 +53,7 @@ func (wm *ExecutionWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(execution.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(execution.SetEventType,
execution.SetEventV2Type,
execution.RemovedEventType).
Builder()
}
@@ -91,6 +96,10 @@ func (wm *ExecutionsExistWriteModel) Reduce() error {
if !slices.Contains(wm.existingIDs, e.Aggregate().ID) {
wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID)
}
case *execution.SetEventV2:
if !slices.Contains(wm.existingIDs, e.Aggregate().ID) {
wm.existingIDs = append(wm.existingIDs, e.Aggregate().ID)
}
case *execution.RemovedEvent:
i := slices.Index(wm.existingIDs, e.Aggregate().ID)
if i >= 0 {
@@ -108,6 +117,7 @@ func (wm *ExecutionsExistWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateTypes(execution.AggregateType).
AggregateIDs(wm.ids...).
EventTypes(execution.SetEventType,
execution.SetEventV2Type,
execution.RemovedEventType).
Builder()
}
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/execution"
)
@@ -32,10 +33,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -53,10 +56,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -65,10 +70,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -91,10 +98,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -112,10 +121,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -138,24 +149,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -174,17 +191,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -193,10 +214,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -214,17 +237,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -233,10 +260,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -254,24 +283,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -299,24 +334,30 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
),
@@ -334,10 +375,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -346,17 +389,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -385,10 +432,12 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution1", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
@@ -397,17 +446,21 @@ func TestCommandSide_executionsExistsWriteModel(t *testing.T) {
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution2", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
execution.NewSetEvent(context.Background(),
execution.NewSetEventV2(context.Background(),
execution.NewAggregate("execution3", "org1"),
[]string{"target"},
[]string{"include"},
[]*execution.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
),
),
eventFromEventPusher(
File diff suppressed because it is too large Load Diff
+11 -15
View File
@@ -16,9 +16,8 @@ type AddTarget struct {
Name string
TargetType domain.TargetType
URL string
Endpoint string
Timeout time.Duration
Async bool
InterruptOnError bool
}
@@ -29,9 +28,9 @@ func (a *AddTarget) IsValid() error {
if a.Timeout == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-39f35d8uri", "Errors.Target.NoTimeout")
}
_, err := url.Parse(a.URL)
if err != nil || a.URL == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
_, err := url.Parse(a.Endpoint)
if err != nil || a.Endpoint == "" {
return zerrors.ThrowInvalidArgument(err, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
}
return nil
@@ -65,9 +64,8 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
TargetAggregateFromWriteModel(&wm.WriteModel),
add.Name,
add.TargetType,
add.URL,
add.Endpoint,
add.Timeout,
add.Async,
add.InterruptOnError,
))
if err != nil {
@@ -84,9 +82,8 @@ type ChangeTarget struct {
Name *string
TargetType *domain.TargetType
URL *string
Endpoint *string
Timeout *time.Duration
Async *bool
InterruptOnError *bool
}
@@ -100,10 +97,10 @@ func (a *ChangeTarget) IsValid() error {
if a.Timeout != nil && *a.Timeout == 0 {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-08b39vdi57", "Errors.Target.NoTimeout")
}
if a.URL != nil {
_, err := url.Parse(*a.URL)
if err != nil || *a.URL == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
if a.Endpoint != nil {
_, err := url.Parse(*a.Endpoint)
if err != nil || *a.Endpoint == "" {
return zerrors.ThrowInvalidArgument(err, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
}
}
return nil
@@ -130,9 +127,8 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
TargetAggregateFromWriteModel(&existing.WriteModel),
change.Name,
change.TargetType,
change.URL,
change.Endpoint,
change.Timeout,
change.Async,
change.InterruptOnError)
if changedEvent == nil {
return writeModelToObjectDetails(&existing.WriteModel), nil
+7 -16
View File
@@ -15,9 +15,8 @@ type TargetWriteModel struct {
Name string
TargetType domain.TargetType
URL string
Endpoint string
Timeout time.Duration
Async bool
InterruptOnError bool
State domain.TargetState
@@ -39,9 +38,8 @@ func (wm *TargetWriteModel) Reduce() error {
case *target.AddedEvent:
wm.Name = e.Name
wm.TargetType = e.TargetType
wm.URL = e.URL
wm.Endpoint = e.Endpoint
wm.Timeout = e.Timeout
wm.Async = e.Async
wm.State = domain.TargetActive
case *target.ChangedEvent:
if e.Name != nil {
@@ -50,15 +48,12 @@ func (wm *TargetWriteModel) Reduce() error {
if e.TargetType != nil {
wm.TargetType = *e.TargetType
}
if e.URL != nil {
wm.URL = *e.URL
if e.Endpoint != nil {
wm.Endpoint = *e.Endpoint
}
if e.Timeout != nil {
wm.Timeout = *e.Timeout
}
if e.Async != nil {
wm.Async = *e.Async
}
if e.InterruptOnError != nil {
wm.InterruptOnError = *e.InterruptOnError
}
@@ -86,9 +81,8 @@ func (wm *TargetWriteModel) NewChangedEvent(
agg *eventstore.Aggregate,
name *string,
targetType *domain.TargetType,
url *string,
endpoint *string,
timeout *time.Duration,
async *bool,
interruptOnError *bool,
) *target.ChangedEvent {
changes := make([]target.Changes, 0)
@@ -98,15 +92,12 @@ func (wm *TargetWriteModel) NewChangedEvent(
if targetType != nil && wm.TargetType != *targetType {
changes = append(changes, target.ChangeTargetType(*targetType))
}
if url != nil && wm.URL != *url {
changes = append(changes, target.ChangeURL(*url))
if endpoint != nil && wm.Endpoint != *endpoint {
changes = append(changes, target.ChangeEndpoint(*endpoint))
}
if timeout != nil && wm.Timeout != *timeout {
changes = append(changes, target.ChangeTimeout(*timeout))
}
if async != nil && wm.Async != *async {
changes = append(changes, target.ChangeAsync(*async))
}
if interruptOnError != nil && wm.InterruptOnError != *interruptOnError {
changes = append(changes, target.ChangeInterruptOnError(*interruptOnError))
}
@@ -20,7 +20,6 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent {
"https://example.com",
time.Second,
false,
false,
)
}
+96 -158
View File
@@ -19,7 +19,7 @@ import (
func TestCommands_AddTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@@ -41,7 +41,7 @@ func TestCommands_AddTarget(t *testing.T) {
{
"no resourceowner, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
@@ -55,12 +55,12 @@ func TestCommands_AddTarget(t *testing.T) {
{
"no name, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@@ -69,50 +69,50 @@ func TestCommands_AddTarget(t *testing.T) {
{
"no timeout, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no url, error",
"no Endpoint, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
Timeout: time.Second,
URL: "",
Name: "name",
Timeout: time.Second,
Endpoint: "",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"no parsable url, error",
"no parsable Endpoint, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
add: &AddTarget{
Name: "name",
Timeout: time.Second,
URL: "://",
Name: "name",
Timeout: time.Second,
Endpoint: "://",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@@ -121,18 +121,17 @@ func TestCommands_AddTarget(t *testing.T) {
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
),
),
),
@@ -142,11 +141,11 @@ func TestCommands_AddTarget(t *testing.T) {
ctx: context.Background(),
add: &AddTarget{
Name: "name",
URL: "https://example.com",
Endpoint: "https://example.com",
Timeout: time.Second,
TargetType: domain.TargetTypeWebhook,
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsPreconditionFailed,
@@ -155,16 +154,10 @@ func TestCommands_AddTarget(t *testing.T) {
{
"already existing",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
eventFromEventPusher(
targetAddEvent("target", "instance"),
),
),
),
@@ -176,9 +169,9 @@ func TestCommands_AddTarget(t *testing.T) {
Name: "name",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
URL: "https://example.com",
Endpoint: "https://example.com",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorAlreadyExists,
@@ -187,18 +180,10 @@ func TestCommands_AddTarget(t *testing.T) {
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
idGenerator: mock.ExpectID(t, "id1"),
@@ -209,32 +194,28 @@ func TestCommands_AddTarget(t *testing.T) {
Name: "name",
TargetType: domain.TargetTypeWebhook,
Timeout: time.Second,
URL: "https://example.com",
Endpoint: "https://example.com",
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
{
"push full ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
expectPush(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
time.Second,
true,
true,
),
func() eventstore.Command {
event := targetAddEvent("id1", "instance")
event.InterruptOnError = true
return event
}(),
),
),
idGenerator: mock.ExpectID(t, "id1"),
@@ -244,17 +225,16 @@ func TestCommands_AddTarget(t *testing.T) {
add: &AddTarget{
Name: "name",
TargetType: domain.TargetTypeWebhook,
URL: "https://example.com",
Endpoint: "https://example.com",
Timeout: time.Second,
Async: true,
InterruptOnError: true,
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
id: "id1",
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
@@ -262,7 +242,7 @@ func TestCommands_AddTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
details, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
@@ -282,7 +262,7 @@ func TestCommands_AddTarget(t *testing.T) {
func TestCommands_ChangeTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -302,7 +282,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"resourceowner missing, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
@@ -316,12 +296,12 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@@ -330,14 +310,14 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"name empty, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
Name: gu.Ptr(""),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@@ -346,46 +326,46 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"timeout empty, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
Timeout: gu.Ptr(time.Duration(0)),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"url empty, error",
"Endpoint empty, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
URL: gu.Ptr(""),
Endpoint: gu.Ptr(""),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
"url not parsable, error",
"Endpoint not parsable, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
change: &ChangeTarget{
URL: gu.Ptr("://"),
Endpoint: gu.Ptr("://"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@@ -394,7 +374,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
},
@@ -406,7 +386,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
Name: gu.Ptr("name"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsNotFound,
@@ -415,18 +395,10 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"no changes",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("target", "instance"),
),
),
),
@@ -439,35 +411,27 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
TargetType: gu.Ptr(domain.TargetTypeWebhook),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
{
"unique constraint failed, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("target", "instance"),
),
),
expectPushFailed(
zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"),
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
[]target.Changes{
target.ChangeName("name", "name2"),
},
@@ -483,7 +447,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
Name: gu.Ptr("name2"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsPreconditionFailed,
@@ -492,23 +456,15 @@ func TestCommands_ChangeTarget(t *testing.T) {
{
"push ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
expectPush(
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
[]target.Changes{
target.ChangeName("name", "name2"),
},
@@ -524,40 +480,31 @@ func TestCommands_ChangeTarget(t *testing.T) {
},
Name: gu.Ptr("name2"),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
{
"push full ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
expectPush(
target.NewChangedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
[]target.Changes{
target.ChangeName("name", "name2"),
target.ChangeURL("https://example2.com"),
target.ChangeTargetType(domain.TargetTypeRequestResponse),
target.ChangeTimeout(time.Second),
target.ChangeAsync(true),
target.ChangeEndpoint("https://example2.com"),
target.ChangeTargetType(domain.TargetTypeCall),
target.ChangeTimeout(10 * time.Second),
target.ChangeInterruptOnError(true),
},
),
@@ -571,17 +518,16 @@ func TestCommands_ChangeTarget(t *testing.T) {
AggregateID: "id1",
},
Name: gu.Ptr("name2"),
URL: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeRequestResponse),
Timeout: gu.Ptr(time.Second),
Async: gu.Ptr(true),
Endpoint: gu.Ptr("https://example2.com"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
@@ -589,7 +535,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
details, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -607,7 +553,7 @@ func TestCommands_ChangeTarget(t *testing.T) {
func TestCommands_DeleteTarget(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
}
type args struct {
ctx context.Context
@@ -627,12 +573,12 @@ func TestCommands_DeleteTarget(t *testing.T) {
{
"id missing, error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
},
args{
ctx: context.Background(),
id: "",
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsErrorInvalidArgument,
@@ -641,14 +587,14 @@ func TestCommands_DeleteTarget(t *testing.T) {
{
"not found, error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
},
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
err: zerrors.IsNotFound,
@@ -657,23 +603,15 @@ func TestCommands_DeleteTarget(t *testing.T) {
{
"remove ok",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
target.NewAddedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
"name",
domain.TargetTypeWebhook,
"https://example.com",
0,
false,
false,
),
targetAddEvent("id1", "instance"),
),
),
expectPush(
target.NewRemovedEvent(context.Background(),
target.NewAggregate("id1", "org1"),
target.NewAggregate("id1", "instance"),
"name",
),
),
@@ -682,11 +620,11 @@ func TestCommands_DeleteTarget(t *testing.T) {
args{
ctx: context.Background(),
id: "id1",
resourceOwner: "org1",
resourceOwner: "instance",
},
res{
details: &domain.ObjectDetails{
ResourceOwner: "org1",
ResourceOwner: "instance",
},
},
},
@@ -694,7 +632,7 @@ func TestCommands_DeleteTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
details, err := c.DeleteTarget(tt.args.ctx, tt.args.id, tt.args.resourceOwner)
if tt.res.err == nil {
+14
View File
@@ -31,3 +31,17 @@ func (e ExecutionType) String() string {
}
return ""
}
type ExecutionTargetType uint
func (s ExecutionTargetType) Valid() bool {
return s < executionTargetTypeStateCount
}
const (
ExecutionTargetTypeUnspecified ExecutionTargetType = iota
ExecutionTargetTypeInclude
ExecutionTargetTypeTarget
executionTargetTypeStateCount
)
+2 -1
View File
@@ -4,7 +4,8 @@ type TargetType uint
const (
TargetTypeWebhook TargetType = iota
TargetTypeRequestResponse
TargetTypeCall
TargetTypeAsync
)
type TargetState int32
+122
View File
@@ -0,0 +1,122 @@
package execution
import (
"bytes"
"context"
"io"
"net/http"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type ContextInfo interface {
GetHTTPRequestBody() []byte
GetContent() interface{}
SetHTTPResponseBody([]byte) error
}
type Target interface {
GetTargetID() string
IsInterruptOnError() bool
GetEndpoint() string
GetTargetType() domain.TargetType
GetTimeout() time.Duration
}
// CallTargets call a list of targets in order with handling of error and responses
func CallTargets(
ctx context.Context,
targets []Target,
info ContextInfo,
) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
for _, target := range targets {
// call the type of target
resp, err := CallTarget(ctx, target, info)
// handle error if interrupt is set
if err != nil && target.IsInterruptOnError() {
return nil, err
}
if len(resp) > 0 {
// error in unmarshalling
if err := info.SetHTTPResponseBody(resp); err != nil {
return nil, err
}
}
}
return info.GetContent(), nil
}
type ContextInfoRequest interface {
GetHTTPRequestBody() []byte
}
// CallTarget call the desired type of target with handling of responses
func CallTarget(
ctx context.Context,
target Target,
info ContextInfoRequest,
) (res []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer span.EndWithError(err)
switch target.GetTargetType() {
// get request, ignore response and return request and error for handling in list of targets
case domain.TargetTypeWebhook:
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
// get request, return response and error
case domain.TargetTypeCall:
return call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody())
case domain.TargetTypeAsync:
go func(target Target, info ContextInfoRequest) {
if _, err := call(ctx, target.GetEndpoint(), target.GetTimeout(), info.GetHTTPRequestBody()); err != nil {
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
}
}(target, info)
return nil, nil
default:
return nil, zerrors.ThrowInternal(nil, "EXEC-auqnansr2m", "Errors.Execution.Unknown")
}
}
// webhook call a webhook, ignore the response but return the errror
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte) error {
_, err := call(ctx, url, timeout, body)
return err
}
// call function to do a post HTTP request to a desired url with timeout
func call(ctx context.Context, url string, timeout time.Duration, body []byte) (_ []byte, err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, span := tracing.NewSpan(ctx)
defer func() {
cancel()
span.EndWithError(err)
}()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Check for success between 200 and 299, redirect 300 to 399 is handled by the client, return error with statusCode >= 400
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
return io.ReadAll(resp.Body)
}
return nil, zerrors.ThrowUnknown(nil, "EXEC-dra6yamk98", "Errors.Execution.Failed")
}
+347
View File
@@ -0,0 +1,347 @@
package execution
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/domain"
)
var _ Target = &mockTarget{}
type mockTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *mockTarget) GetTargetID() string {
return e.TargetID
}
func (e *mockTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *mockTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *mockTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *mockTarget) GetTimeout() time.Duration {
return e.Timeout
}
func Test_Call(t *testing.T) {
type args struct {
ctx context.Context
timeout time.Duration
sleep time.Duration
method string
body []byte
respBody []byte
statusCode int
}
type res struct {
body []byte
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"not ok status",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusBadRequest,
},
res{
wantErr: true,
},
},
{
"timeout",
args{
ctx: context.Background(),
timeout: time.Second,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
},
res{
wantErr: true,
},
},
{
"ok",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
},
res{
body: []byte("{\"response\": \"values\"}"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
respBody, err := testServerCall(t,
tt.args.method,
tt.args.body,
tt.args.sleep,
tt.args.statusCode,
tt.args.respBody,
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
)
if tt.res.wantErr {
assert.Error(t, err)
assert.Nil(t, respBody)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.res.body, respBody)
}
})
}
}
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
return func(url string) ([]byte, error) {
return call(ctx, url, timeout, body)
}
}
func testCallTarget(ctx context.Context,
target *mockTarget,
info ContextInfoRequest,
) func(string) ([]byte, error) {
return func(url string) (r []byte, err error) {
target.Endpoint = url
return CallTarget(ctx, target, info)
}
}
func testServerCall(
t *testing.T,
method string,
body []byte,
timeout time.Duration,
statusCode int,
respBody []byte,
call func(string) ([]byte, error),
) ([]byte, error) {
handler := func(w http.ResponseWriter, r *http.Request) {
checkRequest(t, r, method, body)
if statusCode != http.StatusOK {
http.Error(w, "error", statusCode)
return
}
time.Sleep(timeout)
w.Header().Set("Content-Type", "application/json")
if _, err := io.WriteString(w, string(respBody)); err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
return call(server.URL)
}
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
sentBody, err := io.ReadAll(sent.Body)
require.NoError(t, err)
require.Equal(t, expectedBody, sentBody)
require.Equal(t, method, sent.Method)
}
var _ ContextInfoRequest = &mockContextInfoRequest{}
type request struct {
Request string `json:"request"`
}
type mockContextInfoRequest struct {
Request *request `json:"request"`
}
func newMockContextInfoRequest(s string) *mockContextInfoRequest {
return &mockContextInfoRequest{&request{s}}
}
func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte {
data, _ := json.Marshal(c)
return data
}
func (c *mockContextInfoRequest) GetContent() []byte {
data, _ := json.Marshal(c.Request)
return data
}
func Test_CallTarget(t *testing.T) {
type args struct {
ctx context.Context
target *mockTarget
sleep time.Duration
info ContextInfoRequest
method string
body []byte
respBody []byte
statusCode int
}
type res struct {
body []byte
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"unknown targettype, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: 4,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
},
},
{
"webhook, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
},
},
{
"webhook, ok",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeWebhook,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
res{
body: nil,
},
},
{
"request response, error",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusInternalServerError,
},
res{
wantErr: true,
},
},
{
"request response, ok",
args{
ctx: context.Background(),
sleep: time.Second,
method: http.MethodPost,
info: newMockContextInfoRequest("content1"),
target: &mockTarget{
TargetType: domain.TargetTypeCall,
Timeout: time.Minute,
},
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
respBody: []byte("{\"request\":\"content2\"}"),
statusCode: http.StatusOK,
},
res{
body: []byte("{\"request\":\"content2\"}"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
respBody, err := testServerCall(t,
tt.args.method,
tt.args.body,
tt.args.sleep,
tt.args.statusCode,
tt.args.respBody,
testCallTarget(tt.args.ctx, tt.args.target, tt.args.info),
)
if tt.res.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.body, respBody)
})
}
}
+5 -3
View File
@@ -65,7 +65,9 @@ func AssertListDetails[D ListDetailsMsg](t testing.TB, expected, actual D) {
assert.Equal(t, wantDetails.GetTotalResult(), gotDetails.GetTotalResult())
gotCD := gotDetails.GetTimestamp().AsTime()
wantCD := time.Now()
assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute))
if wantDetails.GetTimestamp() != nil {
gotCD := gotDetails.GetTimestamp().AsTime()
wantCD := time.Now()
assert.WithinRange(t, gotCD, wantCD.Add(-time.Minute), wantCD.Add(time.Minute))
}
}
+30 -31
View File
@@ -522,39 +522,32 @@ func (s *Tester) CreateProjectMembership(t *testing.T, ctx context.Context, proj
require.NoError(t, err)
}
func (s *Tester) CreateTarget(ctx context.Context, t *testing.T) *action.CreateTargetResponse {
req := &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse {
nameSet := fmt.Sprint(time.Now().UnixNano() + 1)
if name != "" {
nameSet = name
}
target, err := s.Client.ActionV3.CreateTarget(ctx, req)
require.NoError(t, err)
return target
}
func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T, name string, async bool, interrupt bool) *action.CreateTargetResponse {
req := &action.CreateTargetRequest{
Name: name,
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
Name: nameSet,
Endpoint: endpoint,
Timeout: durationpb.New(10 * time.Second),
}
if async {
req.ExecutionType = &action.CreateTargetRequest_IsAsync{
IsAsync: true,
switch ty {
case domain.TargetTypeWebhook:
req.TargetType = &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: interrupt,
},
}
}
if interrupt {
req.ExecutionType = &action.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
case domain.TargetTypeCall:
req.TargetType = &action.CreateTargetRequest_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: interrupt,
},
}
case domain.TargetTypeAsync:
req.TargetType = &action.CreateTargetRequest_RestAsync{
RestAsync: &action.SetRESTAsync{},
}
}
target, err := s.Client.ActionV3.CreateTarget(ctx, req)
@@ -562,16 +555,22 @@ func (s *Tester) CreateTargetWithNameAndType(ctx context.Context, t *testing.T,
return target
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string, includes []string) *action.SetExecutionResponse {
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Targets: targets,
Includes: includes,
})
require.NoError(t, err)
return target
}
func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
_, err := s.Client.ActionV3.DeleteExecution(ctx, &action.DeleteExecutionRequest{
Condition: cond,
})
require.NoError(t, err)
}
func (s *Tester) CreateUserSchema(ctx context.Context, t *testing.T) *schema.CreateUserSchemaResponse {
return s.CreateUserSchemaWithType(ctx, t, fmt.Sprint(time.Now().UnixNano()+1))
}
+311 -79
View File
@@ -3,7 +3,10 @@ package query
import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"errors"
"time"
sq "github.com/Masterminds/squirrel"
@@ -11,6 +14,8 @@ import (
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query/projection"
exec "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -23,18 +28,10 @@ var (
name: projection.ExecutionIDCol,
table: executionTable,
}
ExecutionColumnCreationDate = Column{
name: projection.ExecutionCreationDateCol,
table: executionTable,
}
ExecutionColumnChangeDate = Column{
name: projection.ExecutionChangeDateCol,
table: executionTable,
}
ExecutionColumnResourceOwner = Column{
name: projection.ExecutionResourceOwnerCol,
table: executionTable,
}
ExecutionColumnInstanceID = Column{
name: projection.ExecutionInstanceIDCol,
table: executionTable,
@@ -43,14 +40,33 @@ var (
name: projection.ExecutionSequenceCol,
table: executionTable,
}
ExecutionColumnTargets = Column{
name: projection.ExecutionTargetsCol,
table: executionTable,
executionTargetsTable = table{
name: projection.ExecutionTable + "_" + projection.ExecutionTargetSuffix,
instanceIDCol: projection.ExecutionTargetInstanceIDCol,
}
ExecutionColumnIncludes = Column{
name: projection.ExecutionIncludesCol,
table: executionTable,
executionTargetsTableAlias = executionTargetsTable.setAlias("execution_targets")
ExecutionTargetsColumnInstanceID = Column{
name: projection.ExecutionTargetInstanceIDCol,
table: executionTargetsTableAlias,
}
ExecutionTargetsColumnExecutionID = Column{
name: projection.ExecutionTargetExecutionIDCol,
table: executionTargetsTableAlias,
}
executionTargetsListCol = Column{
name: "targets",
table: executionTargetsTableAlias,
}
)
var (
//go:embed execution_targets.sql
executionTargetsQuery string
//go:embed targets_by_execution_id.sql
TargetsByExecutionIDQuery string
//go:embed targets_by_execution_ids.sql
TargetsByExecutionIDsQuery string
)
type Executions struct {
@@ -66,8 +82,7 @@ type Execution struct {
ID string
domain.ObjectDetails
Targets database.TextArray[string]
Includes database.TextArray[string]
Targets []*exec.Target
}
type ExecutionSearchQueries struct {
@@ -108,84 +123,301 @@ func NewExecutionTypeSearchQuery(t domain.ExecutionType) (SearchQuery, error) {
return NewTextQuery(ExecutionColumnID, t.String(), TextStartsWith)
}
func NewExecutionTargetSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(ExecutionColumnTargets, value, TextListContains)
func NewTargetSearchQuery(target string) (SearchQuery, error) {
data, err := targetItemJSONB(domain.ExecutionTargetTypeTarget, target)
if err != nil {
return nil, err
}
return NewListContains(executionTargetsListCol, data)
}
func NewExecutionIncludeSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(ExecutionColumnIncludes, value, TextListContains)
func NewIncludeSearchQuery(include string) (SearchQuery, error) {
data, err := targetItemJSONB(domain.ExecutionTargetTypeInclude, include)
if err != nil {
return nil, err
}
return NewListContains(executionTargetsListCol, data)
}
func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) {
return sq.Select(
ExecutionColumnID.identifier(),
ExecutionColumnChangeDate.identifier(),
ExecutionColumnResourceOwner.identifier(),
ExecutionColumnSequence.identifier(),
ExecutionColumnTargets.identifier(),
ExecutionColumnIncludes.identifier(),
countColumn.identifier(),
).From(executionTable.identifier()).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Executions, error) {
executions := make([]*Execution, 0)
var count uint64
for rows.Next() {
execution := new(Execution)
err := rows.Scan(
&execution.ID,
&execution.EventDate,
&execution.ResourceOwner,
&execution.Sequence,
&execution.Targets,
&execution.Includes,
&count,
)
if err != nil {
return nil, err
}
executions = append(executions, execution)
}
// marshall executionTargets into the same JSONB structure as in the SQL queries
func targetItemJSONB(t domain.ExecutionTargetType, targetItem string) ([]byte, error) {
var target *executionTarget
switch t {
case domain.ExecutionTargetTypeTarget:
target = &executionTarget{Target: targetItem}
case domain.ExecutionTargetTypeInclude:
target = &executionTarget{Include: targetItem}
case domain.ExecutionTargetTypeUnspecified:
return nil, nil
default:
return nil, nil
}
return json.Marshal([]*executionTarget{target})
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-72xfx5jlj7", "Errors.Query.CloseRows")
}
// TargetsByExecutionID query list of targets for best match of a list of IDs, for example:
// [ "request/zitadel.action.v3alpha.ActionService/GetTargetByID",
// "request/zitadel.action.v3alpha.ActionService",
// "request" ]
func (q *Queries) TargetsByExecutionID(ctx context.Context, ids []string) (execution []*ExecutionTarget, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
return &Executions{
Executions: executions,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
instanceID := authz.GetInstance(ctx).InstanceID()
if instanceID == "" {
return nil, nil
}
err = q.client.QueryContext(ctx,
func(rows *sql.Rows) error {
execution, err = scanExecutionTargets(rows)
return err
},
TargetsByExecutionIDQuery,
instanceID,
database.TextArray[string](ids),
)
return execution, err
}
// TargetsByExecutionIDs query list of targets for best matches of 2 separate lists of IDs, combined for performance, for example:
// [ "request/zitadel.action.v3alpha.ActionService/GetTargetByID",
// "request/zitadel.action.v3alpha.ActionService",
// "request" ]
// and
// [ "response/zitadel.action.v3alpha.ActionService/GetTargetByID",
// "response/zitadel.action.v3alpha.ActionService",
// "response" ]
func (q *Queries) TargetsByExecutionIDs(ctx context.Context, ids1, ids2 []string) (execution []*ExecutionTarget, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
instanceID := authz.GetInstance(ctx).InstanceID()
if instanceID == "" {
return nil, nil
}
err = q.client.QueryContext(ctx,
func(rows *sql.Rows) error {
execution, err = scanExecutionTargets(rows)
return err
},
TargetsByExecutionIDsQuery,
instanceID,
database.TextArray[string](ids1),
database.TextArray[string](ids2),
)
return execution, err
}
func prepareExecutionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(row *sql.Row) (*Execution, error)) {
return sq.Select(
ExecutionColumnInstanceID.identifier(),
ExecutionColumnID.identifier(),
ExecutionColumnChangeDate.identifier(),
ExecutionColumnResourceOwner.identifier(),
ExecutionColumnSequence.identifier(),
ExecutionColumnTargets.identifier(),
ExecutionColumnIncludes.identifier(),
executionTargetsListCol.identifier(),
).From(executionTable.identifier()).
Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " +
ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " +
ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(),
).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Execution, error) {
execution := new(Execution)
err := row.Scan(
&execution.ID,
&execution.EventDate,
&execution.ResourceOwner,
&execution.Sequence,
&execution.Targets,
&execution.Includes,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal")
scanExecution
}
func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) {
return sq.Select(
ExecutionColumnInstanceID.identifier(),
ExecutionColumnID.identifier(),
ExecutionColumnChangeDate.identifier(),
ExecutionColumnSequence.identifier(),
executionTargetsListCol.identifier(),
countColumn.identifier(),
).From(executionTable.identifier()).
Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " +
ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " +
ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(),
).
PlaceholderFormat(sq.Dollar),
scanExecutions
}
type executionTarget struct {
Position int `json:"position,omitempty"`
Include string `json:"include,omitempty"`
Target string `json:"target,omitempty"`
}
func scanExecution(row *sql.Row) (*Execution, error) {
execution := new(Execution)
targets := make([]byte, 0)
err := row.Scan(
&execution.ResourceOwner,
&execution.ID,
&execution.EventDate,
&execution.Sequence,
&targets,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound")
}
return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal")
}
executionTargets := make([]*executionTarget, 0)
if err := json.Unmarshal(targets, &executionTargets); err != nil {
return nil, err
}
execution.Targets = make([]*exec.Target, len(executionTargets))
for i := range executionTargets {
if executionTargets[i].Target != "" {
execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: executionTargets[i].Target}
}
if executionTargets[i].Include != "" {
execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: executionTargets[i].Include}
}
}
return execution, nil
}
func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) {
executionTargets := make([]*executionTarget, 0)
if err := json.Unmarshal(data, &executionTargets); err != nil {
return nil, err
}
targets := make([]*exec.Target, len(executionTargets))
// position starts with 1
for _, item := range executionTargets {
if item.Target != "" {
targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target}
}
if item.Include != "" {
targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include}
}
}
return targets, nil
}
func scanExecutions(rows *sql.Rows) (*Executions, error) {
executions := make([]*Execution, 0)
var count uint64
for rows.Next() {
execution := new(Execution)
targets := make([]byte, 0)
err := rows.Scan(
&execution.ResourceOwner,
&execution.ID,
&execution.EventDate,
&execution.Sequence,
&targets,
&count,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-tbrmno85vp", "Errors.Execution.NotFound")
}
return execution, nil
return nil, zerrors.ThrowInternal(err, "QUERY-tyw2ydsj84", "Errors.Internal")
}
execution.Targets, err = executionTargetsUnmarshal(targets)
if err != nil {
return nil, err
}
executions = append(executions, execution)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-yhka3fs3mw", "Errors.Query.CloseRows")
}
return &Executions{
Executions: executions,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
type ExecutionTarget struct {
InstanceID string
ExecutionID string
TargetID string
TargetType domain.TargetType
Endpoint string
Timeout time.Duration
InterruptOnError bool
}
func (e *ExecutionTarget) GetExecutionID() string {
return e.ExecutionID
}
func (e *ExecutionTarget) GetTargetID() string {
return e.TargetID
}
func (e *ExecutionTarget) IsInterruptOnError() bool {
return e.InterruptOnError
}
func (e *ExecutionTarget) GetEndpoint() string {
return e.Endpoint
}
func (e *ExecutionTarget) GetTargetType() domain.TargetType {
return e.TargetType
}
func (e *ExecutionTarget) GetTimeout() time.Duration {
return e.Timeout
}
func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) {
targets := make([]*ExecutionTarget, 0)
for rows.Next() {
target := new(ExecutionTarget)
var (
instanceID = &sql.NullString{}
executionID = &sql.NullString{}
targetID = &sql.NullString{}
targetType = &sql.NullInt32{}
endpoint = &sql.NullString{}
timeout = &sql.NullInt64{}
interruptOnError = &sql.NullBool{}
)
err := rows.Scan(
executionID,
instanceID,
targetID,
targetType,
endpoint,
timeout,
interruptOnError,
)
if err != nil {
return nil, err
}
target.InstanceID = instanceID.String
target.ExecutionID = executionID.String
target.TargetID = targetID.String
target.TargetType = domain.TargetType(targetType.Int32)
target.Endpoint = endpoint.String
target.Timeout = time.Duration(timeout.Int64)
target.InterruptOnError = interruptOnError.Bool
targets = append(targets, target)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-37ardr0pki", "Errors.Query.CloseRows")
}
return targets, nil
}
+11
View File
@@ -0,0 +1,11 @@
SELECT instance_id,
execution_id,
JSONB_AGG(
JSON_OBJECT(
'position' : position,
'include' : include,
'target' : target_id
)
) as targets
FROM projections.executions1_targets
GROUP BY instance_id, execution_id
+55 -39
View File
@@ -8,44 +8,56 @@ import (
"regexp"
"testing"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
exec "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
prepareExecutionsStmt = `SELECT projections.executions.id,` +
` projections.executions.change_date,` +
` projections.executions.resource_owner,` +
` projections.executions.sequence,` +
` projections.executions.targets,` +
` projections.executions.includes,` +
prepareExecutionsStmt = `SELECT projections.executions1.instance_id,` +
` projections.executions1.id,` +
` projections.executions1.change_date,` +
` projections.executions1.sequence,` +
` execution_targets.targets,` +
` COUNT(*) OVER ()` +
` FROM projections.executions`
` FROM projections.executions1` +
` JOIN (` +
`SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` +
` FROM projections.executions1_targets` +
` GROUP BY instance_id, execution_id` +
`)` +
` AS execution_targets` +
` ON execution_targets.instance_id = projections.executions1.instance_id` +
` AND execution_targets.execution_id = projections.executions1.id`
prepareExecutionsCols = []string{
"instance_id",
"id",
"change_date",
"resource_owner",
"sequence",
"targets",
"includes",
"count",
}
prepareExecutionStmt = `SELECT projections.executions.id,` +
` projections.executions.change_date,` +
` projections.executions.resource_owner,` +
` projections.executions.sequence,` +
` projections.executions.targets,` +
` projections.executions.includes` +
` FROM projections.executions`
prepareExecutionStmt = `SELECT projections.executions1.instance_id,` +
` projections.executions1.id,` +
` projections.executions1.change_date,` +
` projections.executions1.sequence,` +
` execution_targets.targets` +
` FROM projections.executions1` +
` JOIN (` +
`SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` +
` FROM projections.executions1_targets` +
` GROUP BY instance_id, execution_id` +
`)` +
` AS execution_targets` +
` ON execution_targets.instance_id = projections.executions1.instance_id` +
` AND execution_targets.execution_id = projections.executions1.id`
prepareExecutionCols = []string{
"instance_id",
"id",
"change_date",
"resource_owner",
"sequence",
"targets",
"includes",
}
)
@@ -81,12 +93,11 @@ func Test_ExecutionPrepares(t *testing.T) {
prepareExecutionsCols,
[][]driver.Value{
{
"ro",
"id",
testNow,
"ro",
uint64(20211109),
database.TextArray[string]{"target"},
database.TextArray[string]{"include"},
[]byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`),
},
},
),
@@ -103,8 +114,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211109,
},
Targets: database.TextArray[string]{"target"},
Includes: database.TextArray[string]{"include"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
},
},
},
@@ -118,20 +131,18 @@ func Test_ExecutionPrepares(t *testing.T) {
prepareExecutionsCols,
[][]driver.Value{
{
"ro",
"id-1",
testNow,
"ro",
uint64(20211109),
database.TextArray[string]{"target1"},
database.TextArray[string]{"include1"},
[]byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`),
},
{
"ro",
"id-2",
testNow,
"ro",
uint64(20211110),
database.TextArray[string]{"target2"},
database.TextArray[string]{"include2"},
[]byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`),
},
},
),
@@ -148,8 +159,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211109,
},
Targets: database.TextArray[string]{"target1"},
Includes: database.TextArray[string]{"include1"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
},
{
ID: "id-2",
@@ -158,8 +171,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211110,
},
Targets: database.TextArray[string]{"target2"},
Includes: database.TextArray[string]{"include2"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
},
},
},
},
@@ -207,12 +222,11 @@ func Test_ExecutionPrepares(t *testing.T) {
regexp.QuoteMeta(prepareExecutionStmt),
prepareExecutionCols,
[]driver.Value{
"ro",
"id",
testNow,
"ro",
uint64(20211109),
database.TextArray[string]{"target"},
database.TextArray[string]{"include"},
[]byte(`[{"position" : 1, "target" : "target"}, {"position" : 2, "include" : "include"}]`),
},
),
},
@@ -223,8 +237,10 @@ func Test_ExecutionPrepares(t *testing.T) {
ResourceOwner: "ro",
Sequence: 20211109,
},
Targets: database.TextArray[string]{"target"},
Includes: database.TextArray[string]{"include"},
Targets: []*exec.Target{
{Type: domain.ExecutionTargetTypeTarget, Target: "target"},
{Type: domain.ExecutionTargetTypeInclude, Target: "include"},
},
},
},
{
+86 -28
View File
@@ -3,6 +3,7 @@ package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
@@ -11,15 +12,19 @@ import (
)
const (
ExecutionTable = "projections.executions"
ExecutionIDCol = "id"
ExecutionCreationDateCol = "creation_date"
ExecutionChangeDateCol = "change_date"
ExecutionResourceOwnerCol = "resource_owner"
ExecutionInstanceIDCol = "instance_id"
ExecutionSequenceCol = "sequence"
ExecutionTargetsCol = "targets"
ExecutionIncludesCol = "includes"
ExecutionTable = "projections.executions1"
ExecutionIDCol = "id"
ExecutionCreationDateCol = "creation_date"
ExecutionChangeDateCol = "change_date"
ExecutionInstanceIDCol = "instance_id"
ExecutionSequenceCol = "sequence"
ExecutionTargetSuffix = "targets"
ExecutionTargetExecutionIDCol = "execution_id"
ExecutionTargetInstanceIDCol = "instance_id"
ExecutionTargetPositionCol = "position"
ExecutionTargetTargetIDCol = "target_id"
ExecutionTargetIncludeCol = "include"
)
type executionProjection struct{}
@@ -33,19 +38,28 @@ func (*executionProjection) Name() string {
}
func (*executionProjection) Init() *old_handler.Check {
return handler.NewTableCheck(
return handler.NewMultiTableCheck(
handler.NewTable([]*handler.InitColumn{
handler.NewColumn(ExecutionIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionCreationDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(ExecutionChangeDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(ExecutionResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(ExecutionTargetsCol, handler.ColumnTypeTextArray, handler.Nullable()),
handler.NewColumn(ExecutionIncludesCol, handler.ColumnTypeTextArray, handler.Nullable()),
handler.NewColumn(ExecutionInstanceIDCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(ExecutionInstanceIDCol, ExecutionIDCol),
),
handler.NewSuffixedTable([]*handler.InitColumn{
handler.NewColumn(ExecutionTargetInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionTargetExecutionIDCol, handler.ColumnTypeText),
handler.NewColumn(ExecutionTargetPositionCol, handler.ColumnTypeInt64),
handler.NewColumn(ExecutionTargetIncludeCol, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(ExecutionTargetTargetIDCol, handler.ColumnTypeText, handler.Nullable()),
},
handler.NewPrimaryKey(ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol, ExecutionTargetPositionCol),
ExecutionTargetSuffix,
handler.WithForeignKey(handler.NewForeignKey("execution", []string{ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol}, []string{ExecutionInstanceIDCol, ExecutionIDCol})),
handler.WithIndex(handler.NewIndex("execution", []string{ExecutionTargetInstanceIDCol, ExecutionTargetExecutionIDCol})),
),
)
}
@@ -55,7 +69,7 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer {
Aggregate: exec.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: exec.SetEventType,
Event: exec.SetEventV2Type,
Reduce: p.reduceExecutionSet,
},
{
@@ -77,21 +91,65 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer {
}
func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*exec.SetEvent](event)
e, err := assertEvent[*exec.SetEventV2](event)
if err != nil {
return nil, err
}
columns := []handler.Column{
handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionIDCol, e.Aggregate().ID),
handler.NewCol(ExecutionResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(ExecutionCreationDateCol, handler.OnlySetValueOnInsert(ExecutionTable, e.CreationDate())),
handler.NewCol(ExecutionChangeDateCol, e.CreationDate()),
handler.NewCol(ExecutionSequenceCol, e.Sequence()),
handler.NewCol(ExecutionTargetsCol, e.Targets),
handler.NewCol(ExecutionIncludesCol, e.Includes),
stmts := []func(eventstore.Event) handler.Exec{
handler.AddUpsertStatement(
[]handler.Column{
handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionIDCol, e.Aggregate().ID),
},
[]handler.Column{
handler.NewCol(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionIDCol, e.Aggregate().ID),
handler.NewCol(ExecutionCreationDateCol, handler.OnlySetValueOnInsert(ExecutionTable, e.CreationDate())),
handler.NewCol(ExecutionChangeDateCol, e.CreationDate()),
handler.NewCol(ExecutionSequenceCol, e.Sequence()),
},
),
// cleanup execution targets to re-insert them
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(ExecutionTargetExecutionIDCol, e.Aggregate().ID),
},
handler.WithTableSuffix(ExecutionTargetSuffix),
),
}
return handler.NewUpsertStatement(e, columns[0:2], columns), nil
if len(e.Targets) > 0 {
for i, target := range e.Targets {
var targetStr, includeStr string
switch target.Type {
case domain.ExecutionTargetTypeTarget:
targetStr = target.Target
case domain.ExecutionTargetTypeInclude:
includeStr = target.Target
case domain.ExecutionTargetTypeUnspecified:
continue
default:
continue
}
stmts = append(stmts,
handler.AddCreateStatement(
[]handler.Column{
handler.NewCol(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCol(ExecutionTargetExecutionIDCol, e.Aggregate().ID),
handler.NewCol(ExecutionTargetPositionCol, i+1),
handler.NewCol(ExecutionTargetIncludeCol, includeStr),
handler.NewCol(ExecutionTargetTargetIDCol, targetStr),
},
handler.WithTableSuffix(ExecutionTargetSuffix),
),
)
}
}
return handler.NewMultiStatement(e, stmts...), nil
}
func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) {
@@ -99,8 +157,8 @@ func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*h
if err != nil {
return nil, err
}
return handler.NewDeleteStatement(
e,
return handler.NewDeleteStatement(e,
[]handler.Condition{
handler.NewCond(ExecutionInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(ExecutionIDCol, e.Aggregate().ID),
+33 -9
View File
@@ -25,11 +25,11 @@ func TestExecutionProjection_reduces(t *testing.T) {
args: args{
event: getEvent(
testEvent(
exec.SetEventType,
exec.SetEventV2Type,
exec.AggregateType,
[]byte(`{"targets": ["target"], "includes": ["include"]}`),
[]byte(`{"targets": [{"type":2,"target":"target"},{"type":1,"target":"include"}]}`),
),
eventstore.GenericEventMapper[exec.SetEvent],
eventstore.GenericEventMapper[exec.SetEventV2],
),
},
reduce: (&executionProjection{}).reduceExecutionSet,
@@ -39,16 +39,40 @@ func TestExecutionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.executions (instance_id, id, resource_owner, creation_date, change_date, sequence, targets, includes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, id) DO UPDATE SET (resource_owner, creation_date, change_date, sequence, targets, includes) = (EXCLUDED.resource_owner, projections.executions.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.targets, EXCLUDED.includes)",
expectedStmt: "INSERT INTO projections.executions1 (instance_id, id, creation_date, change_date, sequence) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (instance_id, id) DO UPDATE SET (creation_date, change_date, sequence) = (projections.executions1.creation_date, EXCLUDED.change_date, EXCLUDED.sequence)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
"ro-id",
anyArg{},
anyArg{},
uint64(15),
[]string{"target"},
[]string{"include"},
},
},
{
expectedStmt: "DELETE FROM projections.executions1_targets WHERE (instance_id = $1) AND (execution_id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
},
},
{
expectedStmt: "INSERT INTO projections.executions1_targets (instance_id, execution_id, position, include, target_id) VALUES ($1, $2, $3, $4, $5)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
1,
"",
"target",
},
},
{
expectedStmt: "INSERT INTO projections.executions1_targets (instance_id, execution_id, position, include, target_id) VALUES ($1, $2, $3, $4, $5)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
2,
"include",
"",
},
},
},
@@ -74,7 +98,7 @@ func TestExecutionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1) AND (id = $2)",
expectedStmt: "DELETE FROM projections.executions1 WHERE (instance_id = $1) AND (id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
@@ -103,7 +127,7 @@ func TestExecutionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.executions WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.executions1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
+1 -1
View File
@@ -269,8 +269,8 @@ func newProjectionsList() {
RestrictionsProjection,
SystemFeatureProjection,
InstanceFeatureProjection,
ExecutionProjection,
TargetProjection,
ExecutionProjection,
UserSchemaProjection,
}
}
+8 -14
View File
@@ -11,7 +11,7 @@ import (
)
const (
TargetTable = "projections.targets"
TargetTable = "projections.targets1"
TargetIDCol = "id"
TargetCreationDateCol = "creation_date"
TargetChangeDateCol = "change_date"
@@ -20,9 +20,8 @@ const (
TargetSequenceCol = "sequence"
TargetNameCol = "name"
TargetTargetType = "target_type"
TargetURLCol = "url"
TargetEndpointCol = "endpoint"
TargetTimeoutCol = "timeout"
TargetAsyncCol = "async"
TargetInterruptOnErrorCol = "interrupt_on_error"
)
@@ -47,10 +46,9 @@ func (*targetProjection) Init() *old_handler.Check {
handler.NewColumn(TargetTargetType, handler.ColumnTypeEnum),
handler.NewColumn(TargetSequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(TargetNameCol, handler.ColumnTypeText),
handler.NewColumn(TargetURLCol, handler.ColumnTypeText, handler.Default("")),
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64, handler.Default(0)),
handler.NewColumn(TargetAsyncCol, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(TargetEndpointCol, handler.ColumnTypeText),
handler.NewColumn(TargetTimeoutCol, handler.ColumnTypeInt64),
handler.NewColumn(TargetInterruptOnErrorCol, handler.ColumnTypeBool),
},
handler.NewPrimaryKey(TargetInstanceIDCol, TargetIDCol),
),
@@ -103,10 +101,9 @@ func (p *targetProjection) reduceTargetAdded(event eventstore.Event) (*handler.S
handler.NewCol(TargetChangeDateCol, e.CreationDate()),
handler.NewCol(TargetSequenceCol, e.Sequence()),
handler.NewCol(TargetNameCol, e.Name),
handler.NewCol(TargetURLCol, e.URL),
handler.NewCol(TargetEndpointCol, e.Endpoint),
handler.NewCol(TargetTargetType, e.TargetType),
handler.NewCol(TargetTimeoutCol, e.Timeout),
handler.NewCol(TargetAsyncCol, e.Async),
handler.NewCol(TargetInterruptOnErrorCol, e.InterruptOnError),
},
), nil
@@ -128,15 +125,12 @@ func (p *targetProjection) reduceTargetChanged(event eventstore.Event) (*handler
if e.TargetType != nil {
values = append(values, handler.NewCol(TargetTargetType, *e.TargetType))
}
if e.URL != nil {
values = append(values, handler.NewCol(TargetURLCol, *e.URL))
if e.Endpoint != nil {
values = append(values, handler.NewCol(TargetEndpointCol, *e.Endpoint))
}
if e.Timeout != nil {
values = append(values, handler.NewCol(TargetTimeoutCol, *e.Timeout))
}
if e.Async != nil {
values = append(values, handler.NewCol(TargetAsyncCol, *e.Async))
}
if e.InterruptOnError != nil {
values = append(values, handler.NewCol(TargetInterruptOnErrorCol, *e.InterruptOnError))
}
+6 -8
View File
@@ -29,7 +29,7 @@ func TestTargetProjection_reduces(t *testing.T) {
testEvent(
target.AddedEventType,
target.AggregateType,
[]byte(`{"name": "name", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
[]byte(`{"name": "name", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
),
eventstore.GenericEventMapper[target.AddedEvent],
),
@@ -41,7 +41,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.targets (instance_id, resource_owner, id, creation_date, change_date, sequence, name, url, target_type, timeout, async, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
expectedStmt: "INSERT INTO projections.targets1 (instance_id, resource_owner, id, creation_date, change_date, sequence, name, endpoint, target_type, timeout, interrupt_on_error) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
expectedArgs: []interface{}{
"instance-id",
"ro-id",
@@ -54,7 +54,6 @@ func TestTargetProjection_reduces(t *testing.T) {
domain.TargetTypeWebhook,
3 * time.Second,
true,
true,
},
},
},
@@ -68,7 +67,7 @@ func TestTargetProjection_reduces(t *testing.T) {
testEvent(
target.ChangedEventType,
target.AggregateType,
[]byte(`{"name": "name2", "targetType":0, "url":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
[]byte(`{"name": "name2", "targetType":0, "endpoint":"https://example.com", "timeout": 3000000000, "async": true, "interruptOnError": true}`),
),
eventstore.GenericEventMapper[target.ChangedEvent],
),
@@ -80,7 +79,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.targets SET (change_date, sequence, resource_owner, name, target_type, url, timeout, async, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8, $9) WHERE (instance_id = $10) AND (id = $11)",
expectedStmt: "UPDATE projections.targets1 SET (change_date, sequence, resource_owner, name, target_type, endpoint, timeout, interrupt_on_error) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (instance_id = $9) AND (id = $10)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -90,7 +89,6 @@ func TestTargetProjection_reduces(t *testing.T) {
"https://example.com",
3 * time.Second,
true,
true,
"instance-id",
"agg-id",
},
@@ -118,7 +116,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1) AND (id = $2)",
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1) AND (id = $2)",
expectedArgs: []interface{}{
"instance-id",
"agg-id",
@@ -147,7 +145,7 @@ func TestTargetProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.targets WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.targets1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
+25 -6
View File
@@ -478,31 +478,31 @@ func (q *SubSelect) comp() sq.Sqlizer {
return selectQuery
}
type ListQuery struct {
type listQuery struct {
Column Column
Data interface{}
Compare ListComparison
}
func NewListQuery(column Column, value interface{}, compare ListComparison) (*ListQuery, error) {
func NewListQuery(column Column, value interface{}, compare ListComparison) (*listQuery, error) {
if compare < 0 || compare >= listCompareMax {
return nil, ErrInvalidCompare
}
if column.isZero() {
return nil, ErrMissingColumn
}
return &ListQuery{
return &listQuery{
Column: column,
Data: value,
Compare: compare,
}, nil
}
func (q *ListQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
func (q *listQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query.Where(q.comp())
}
func (q *ListQuery) comp() sq.Sqlizer {
func (q *listQuery) comp() sq.Sqlizer {
if q.Compare != ListIn {
return nil
}
@@ -517,7 +517,7 @@ func (q *ListQuery) comp() sq.Sqlizer {
return sq.Eq{q.Column.identifier(): q.Data}
}
func (q *ListQuery) Col() Column {
func (q *listQuery) Col() Column {
return q.Column
}
@@ -720,6 +720,25 @@ type listContains struct {
args interface{}
}
func NewListContains(c Column, value interface{}) (*listContains, error) {
return &listContains{
col: c,
args: value,
}, nil
}
func (q *listContains) Col() Column {
return q.col
}
func (q *listContains) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
return query.Where(q.comp())
}
func (q *listContains) ToSql() (string, []interface{}, error) {
return q.col.identifier() + " @> ? ", []interface{}{q.args}, nil
}
func (q *listContains) comp() sq.Sqlizer {
return q
}
+4 -4
View File
@@ -521,7 +521,7 @@ func TestNewListQuery(t *testing.T) {
tests := []struct {
name string
args args
want *ListQuery
want *listQuery
wantErr func(error) bool
}{
{
@@ -575,7 +575,7 @@ func TestNewListQuery(t *testing.T) {
data: []interface{}{"hurst"},
compare: ListIn,
},
want: &ListQuery{
want: &listQuery{
Column: testCol,
Data: []interface{}{"hurst"},
Compare: ListIn,
@@ -588,7 +588,7 @@ func TestNewListQuery(t *testing.T) {
data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}},
compare: ListIn,
},
want: &ListQuery{
want: &listQuery{
Column: testCol,
Data: &SubSelect{Column: testCol, Queries: []SearchQuery{&textQuery{testCol, "horst1", TextEquals}}},
Compare: ListIn,
@@ -751,7 +751,7 @@ func TestListQuery_comp(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &ListQuery{
s := &listQuery{
Column: tt.fields.Column,
Data: tt.fields.Data,
Compare: tt.fields.Compare,
+4 -13
View File
@@ -52,17 +52,13 @@ var (
table: targetTable,
}
TargetColumnURL = Column{
name: projection.TargetURLCol,
name: projection.TargetEndpointCol,
table: targetTable,
}
TargetColumnTimeout = Column{
name: projection.TargetTimeoutCol,
table: targetTable,
}
TargetColumnAsync = Column{
name: projection.TargetAsyncCol,
table: targetTable,
}
TargetColumnInterruptOnError = Column{
name: projection.TargetInterruptOnErrorCol,
table: targetTable,
@@ -84,9 +80,8 @@ type Target struct {
Name string
TargetType domain.TargetType
URL string
Endpoint string
Timeout time.Duration
Async bool
InterruptOnError bool
}
@@ -138,7 +133,6 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
TargetColumnTargetType.identifier(),
TargetColumnTimeout.identifier(),
TargetColumnURL.identifier(),
TargetColumnAsync.identifier(),
TargetColumnInterruptOnError.identifier(),
countColumn.identifier(),
).From(targetTable.identifier()).
@@ -156,8 +150,7 @@ func prepareTargetsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&target.Name,
&target.TargetType,
&target.Timeout,
&target.URL,
&target.Async,
&target.Endpoint,
&target.InterruptOnError,
&count,
)
@@ -190,7 +183,6 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild
TargetColumnTargetType.identifier(),
TargetColumnTimeout.identifier(),
TargetColumnURL.identifier(),
TargetColumnAsync.identifier(),
TargetColumnInterruptOnError.identifier(),
).From(targetTable.identifier()).
PlaceholderFormat(sq.Dollar),
@@ -204,8 +196,7 @@ func prepareTargetQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuild
&target.Name,
&target.TargetType,
&target.Timeout,
&target.URL,
&target.Async,
&target.Endpoint,
&target.InterruptOnError,
)
if err != nil {
+54 -42
View File
@@ -14,18 +14,17 @@ import (
)
var (
prepareTargetsStmt = `SELECT projections.targets.id,` +
` projections.targets.change_date,` +
` projections.targets.resource_owner,` +
` projections.targets.sequence,` +
` projections.targets.name,` +
` projections.targets.target_type,` +
` projections.targets.timeout,` +
` projections.targets.url,` +
` projections.targets.async,` +
` projections.targets.interrupt_on_error,` +
prepareTargetsStmt = `SELECT projections.targets1.id,` +
` projections.targets1.change_date,` +
` projections.targets1.resource_owner,` +
` projections.targets1.sequence,` +
` projections.targets1.name,` +
` projections.targets1.target_type,` +
` projections.targets1.timeout,` +
` projections.targets1.endpoint,` +
` projections.targets1.interrupt_on_error,` +
` COUNT(*) OVER ()` +
` FROM projections.targets`
` FROM projections.targets1`
prepareTargetsCols = []string{
"id",
"change_date",
@@ -34,23 +33,21 @@ var (
"name",
"target_type",
"timeout",
"url",
"async",
"endpoint",
"interrupt_on_error",
"count",
}
prepareTargetStmt = `SELECT projections.targets.id,` +
` projections.targets.change_date,` +
` projections.targets.resource_owner,` +
` projections.targets.sequence,` +
` projections.targets.name,` +
` projections.targets.target_type,` +
` projections.targets.timeout,` +
` projections.targets.url,` +
` projections.targets.async,` +
` projections.targets.interrupt_on_error` +
` FROM projections.targets`
prepareTargetStmt = `SELECT projections.targets1.id,` +
` projections.targets1.change_date,` +
` projections.targets1.resource_owner,` +
` projections.targets1.sequence,` +
` projections.targets1.name,` +
` projections.targets1.target_type,` +
` projections.targets1.timeout,` +
` projections.targets1.endpoint,` +
` projections.targets1.interrupt_on_error` +
` FROM projections.targets1`
prepareTargetCols = []string{
"id",
"change_date",
@@ -59,8 +56,7 @@ var (
"name",
"target_type",
"timeout",
"url",
"async",
"endpoint",
"interrupt_on_error",
}
)
@@ -106,7 +102,6 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
true,
},
},
),
@@ -126,8 +121,7 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: true,
Endpoint: "https://example.com",
InterruptOnError: true,
},
},
@@ -151,7 +145,6 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
false,
},
{
"id-2",
@@ -163,14 +156,24 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
false,
true,
},
{
"id-3",
testNow,
"ro",
uint64(20211110),
"target-name3",
domain.TargetTypeAsync,
1 * time.Second,
"https://example.com",
false,
},
},
),
},
object: &Targets{
SearchResponse: SearchResponse{
Count: 2,
Count: 3,
},
Targets: []*Target{
{
@@ -183,9 +186,8 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name1",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: true,
InterruptOnError: false,
Endpoint: "https://example.com",
InterruptOnError: true,
},
{
ID: "id-2",
@@ -197,9 +199,21 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name2",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: false,
InterruptOnError: true,
Endpoint: "https://example.com",
InterruptOnError: false,
},
{
ID: "id-3",
ObjectDetails: domain.ObjectDetails{
EventDate: testNow,
ResourceOwner: "ro",
Sequence: 20211110,
},
Name: "target-name3",
TargetType: domain.TargetTypeAsync,
Timeout: 1 * time.Second,
Endpoint: "https://example.com",
InterruptOnError: false,
},
},
},
@@ -256,7 +270,6 @@ func Test_TargetPrepares(t *testing.T) {
1 * time.Second,
"https://example.com",
true,
false,
},
),
},
@@ -270,9 +283,8 @@ func Test_TargetPrepares(t *testing.T) {
Name: "target-name",
TargetType: domain.TargetTypeWebhook,
Timeout: 1 * time.Second,
URL: "https://example.com",
Async: true,
InterruptOnError: false,
Endpoint: "https://example.com",
InterruptOnError: true,
},
},
{
@@ -0,0 +1,40 @@
WITH RECURSIVE
dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
AS (SELECT execution_id
, instance_id
, ARRAY [position]
, "include"
, "target_id"
FROM matched_targets_and_includes
UNION ALL
SELECT e.execution_id
, p.instance_id
, e.position || p.position
, p."include"
, p."target_id"
FROM dissolved_execution_targets e
JOIN projections.executions1_targets p
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
AND e.include = p.execution_id),
matched AS (SELECT *
FROM projections.executions1
WHERE instance_id = $1
AND id = ANY($2)
ORDER BY id DESC
LIMIT 1),
matched_targets_and_includes AS (SELECT pos.*
FROM matched m
JOIN
projections.executions1_targets pos
ON m.id = pos.execution_id
AND m.instance_id = pos.instance_id
ORDER BY execution_id,
position)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
FROM dissolved_execution_targets e
JOIN projections.targets1 t
ON e.instance_id = t.instance_id
AND e.target_id = t.id
WHERE "include" = ''
ORDER BY position DESC;
@@ -0,0 +1,47 @@
WITH RECURSIVE
dissolved_execution_targets(execution_id, instance_id, position, "include", "target_id")
AS (SELECT execution_id
, instance_id
, ARRAY [position]
, "include"
, "target_id"
FROM matched_targets_and_includes
UNION ALL
SELECT e.execution_id
, p.instance_id
, e.position || p.position
, p."include"
, p."target_id"
FROM dissolved_execution_targets e
JOIN projections.executions1_targets p
ON e.instance_id = p.instance_id
AND e.include IS NOT NULL
AND e.include = p.execution_id),
matched AS ((SELECT *
FROM projections.executions1
WHERE instance_id = $1
AND id = ANY($2)
ORDER BY id DESC
LIMIT 1)
UNION ALL
(SELECT *
FROM projections.executions1
WHERE instance_id = $1
AND id = ANY($3)
ORDER BY id DESC
LIMIT 1)),
matched_targets_and_includes AS (SELECT pos.*
FROM matched m
JOIN
projections.executions1_targets pos
ON m.id = pos.execution_id
AND m.instance_id = pos.instance_id
ORDER BY execution_id,
position)
select e.execution_id, e.instance_id, e.target_id, t.target_type, t.endpoint, t.timeout, t.interrupt_on_error
FROM dissolved_execution_targets e
JOIN projections.targets1 t
ON e.instance_id = t.instance_id
AND e.target_id = t.id
WHERE "include" = ''
ORDER BY position DESC;
+4 -1
View File
@@ -23,7 +23,10 @@ func NewAggregate(aggrID, instanceID string) *eventstore.Aggregate {
}
func ID(executionType domain.ExecutionType, value string) string {
return strings.Join([]string{executionType.String(), value}, ".")
if strings.HasPrefix(value, "/") {
return strings.Join([]string{executionType.String(), value}, "")
}
return strings.Join([]string{executionType.String(), value}, "/")
}
func IDAll(executionType domain.ExecutionType) string {
@@ -4,5 +4,6 @@ import "github.com/zitadel/zitadel/internal/eventstore"
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SetEventType, eventstore.GenericEventMapper[SetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, SetEventV2Type, eventstore.GenericEventMapper[SetEventV2])
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent])
}
+31 -8
View File
@@ -3,12 +3,14 @@ package execution
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
eventTypePrefix eventstore.EventType = "execution."
SetEventType = eventTypePrefix + "set"
SetEventV2Type = eventTypePrefix + "v2.set"
RemovedEventType = eventTypePrefix + "removed"
)
@@ -31,18 +33,39 @@ func (e *SetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewSetEvent(
type SetEventV2 struct {
*eventstore.BaseEvent `json:"-"`
Targets []*Target `json:"targets"`
}
func (e *SetEventV2) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *SetEventV2) Payload() any {
return e
}
func (e *SetEventV2) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
type Target struct {
Type domain.ExecutionTargetType `json:"type"`
Target string `json:"target"`
}
func NewSetEventV2(
ctx context.Context,
aggregate *eventstore.Aggregate,
targets []string,
includes []string,
) *SetEvent {
return &SetEvent{
targets []*Target,
) *SetEventV2 {
return &SetEventV2{
BaseEvent: eventstore.NewBaseEventForPush(
ctx, aggregate, SetEventType,
ctx, aggregate, SetEventV2Type,
),
Targets: targets,
Includes: includes,
Targets: targets,
}
}
+6 -15
View File
@@ -20,9 +20,8 @@ type AddedEvent struct {
Name string `json:"name"`
TargetType domain.TargetType `json:"targetType"`
URL string `json:"url"`
Endpoint string `json:"endpoint"`
Timeout time.Duration `json:"timeout"`
Async bool `json:"async"`
InterruptOnError bool `json:"interruptOnError"`
}
@@ -43,16 +42,15 @@ func NewAddedEvent(
aggregate *eventstore.Aggregate,
name string,
targetType domain.TargetType,
url string,
endpoint string,
timeout time.Duration,
async bool,
interruptOnError bool,
) *AddedEvent {
return &AddedEvent{
*eventstore.NewBaseEventForPush(
ctx, aggregate, AddedEventType,
),
name, targetType, url, timeout, async, interruptOnError}
name, targetType, endpoint, timeout, interruptOnError}
}
type ChangedEvent struct {
@@ -60,9 +58,8 @@ type ChangedEvent struct {
Name *string `json:"name,omitempty"`
TargetType *domain.TargetType `json:"targetType,omitempty"`
URL *string `json:"url,omitempty"`
Endpoint *string `json:"endpoint,omitempty"`
Timeout *time.Duration `json:"timeout,omitempty"`
Async *bool `json:"async,omitempty"`
InterruptOnError *bool `json:"interruptOnError,omitempty"`
oldName string
@@ -119,9 +116,9 @@ func ChangeTargetType(targetType domain.TargetType) func(event *ChangedEvent) {
}
}
func ChangeURL(url string) func(event *ChangedEvent) {
func ChangeEndpoint(endpoint string) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.URL = &url
e.Endpoint = &endpoint
}
}
@@ -131,12 +128,6 @@ func ChangeTimeout(timeout time.Duration) func(event *ChangedEvent) {
}
}
func ChangeAsync(async bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.Async = &async
}
}
func ChangeInterruptOnError(interruptOnError bool) func(event *ChangedEvent) {
return func(e *ChangedEvent) {
e.InterruptOnError = &interruptOnError
+1
View File
@@ -484,6 +484,7 @@ Errors:
NotActive: Действието не е активно
NotInactive: Действието не е неактивно
MaxAllowed: Не са разрешени допълнителни активни действия
NotEnabled: Функцията „Действие“ не е активирана
Flow:
FlowTypeMissing: Липсва FlowType
Empty: Потокът вече е празен
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: Akce není aktivní
NotInactive: Akce není neaktivní
MaxAllowed: Není dovoleno více aktivních akcí
NotEnabled: Funkce "Akce" není povolena
Flow:
FlowTypeMissing: Chybí typ toku
Empty: Tok je již prázdný
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: Action ist nicht aktiv
NotInactive: Action ist nicht inaktiv
MaxAllowed: Keine weitere aktiven Actions mehr erlaubt
NotEnabled: Function "Action" ist nicht aktiviert
Flow:
FlowTypeMissing: FlowType fehlt
Empty: Flow ist bereits leer
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: Action is not active
NotInactive: Action is not inactive
MaxAllowed: No additional active Actions allowed
NotEnabled: Feature "Action" is not enabled
Flow:
FlowTypeMissing: FlowType missing
Empty: Flow is already empty
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: La acción no está activa
NotInactive: La acción no está inactiva
MaxAllowed: No hay acciones adicionales activas permitidas
NotEnabled: La función "Acción" no está habilitada
Flow:
FlowTypeMissing: Falta el tipo de flujo
Empty: El flujo ya está vacío
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: L'action n'est pas active
NotInactive: L'action n'est pas inactive
MaxAllowed: Aucune action active supplémentaire n'est autorisée
NotEnabled: La fonctionnalité "Action" n'est pas activée
Flow:
FlowTypeMissing: FlowType missing
Empty: Le flux est déjà vide
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: L'azione non è attiva
NotInactive: L'azione non è inattiva
MaxAllowed: Non sono permesse altre azioni attive
NotEnabled: La funzione "Azione" non è abilitata
Flow:
FlowTypeMissing: FlowType mancante
Empty: Flow è già vuoto
+1
View File
@@ -459,6 +459,7 @@ Errors:
NotActive: アクションはアクティブではありません
NotInactive: アクションは非アクティブではありません
MaxAllowed: 追加のアクティブアクションは許可されていません
NotEnabled: 機能「アクション」が有効になっていません
Flow:
FlowTypeMissing: フロータイプがありません
Empty: フローはすでに空です
+1
View File
@@ -469,6 +469,7 @@ Errors:
NotActive: Акцијата не е активна
NotInactive: Акцијата не е неактивна
MaxAllowed: Не се дозволени дополнителни активни акции
NotEnabled: Функцијата „Акција“ не е овозможена
Flow:
FlowTypeMissing: FlowType не е наведен
Empty: Flow е веќе празен
+1
View File
@@ -469,6 +469,7 @@ Errors:
NotActive: Actie is niet actief
NotInactive: Actie is niet inactief
MaxAllowed: Geen extra actieve acties toegestaan
NotEnabled: Functie "Actie" is niet ingeschakeld
Flow:
FlowTypeMissing: FlowType ontbreekt
Empty: Flow is al leeg
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: Działanie nie jest aktywne
NotInactive: Działanie nie jest dezaktywowane
MaxAllowed: Nie dopuszcza się dodatkowych aktywnych działań.
NotEnabled: Funkcja „Akcja” nie jest włączona
Flow:
FlowTypeMissing: Typ przepływu brakuje
Empty: Przepływ jest już pusty
+1
View File
@@ -469,6 +469,7 @@ Errors:
NotActive: A ação não está ativa
NotInactive: A ação não está inativa
MaxAllowed: Não são permitidas ações adicionais ativas
NotEnabled: O recurso "Ação" não está ativado
Flow:
FlowTypeMissing: O tipo de fluxo está faltando
Empty: O fluxo já está vazio
+1
View File
@@ -463,6 +463,7 @@ Errors:
NotActive: Действие не активно
NotInactive: Действие не является неактивным
MaxAllowed: Дополнительные активные действия запрещены
NotEnabled: Функция «Действие» не включена
Flow:
FlowTypeMissing: Тип процесса отсутствует
Empty: Процесс уже пуст
+1
View File
@@ -470,6 +470,7 @@ Errors:
NotActive: 动作不是启用状态
NotInactive: 动作不是停用状态
MaxAllowed: 不允许额外的动作
NotEnabled: 未启用“操作”功能
Flow:
FlowTypeMissing: 缺少身份认证流程类型
Empty: 身份认证流程为空