diff --git a/docs/docs/apis/actionsv2/execution-local.md b/docs/docs/apis/actionsv2/execution-local.md new file mode 100644 index 0000000000..3f0ccc0fe0 --- /dev/null +++ b/docs/docs/apis/actionsv2/execution-local.md @@ -0,0 +1,139 @@ +--- +title: Actions v2 example execution locally +--- + +In this guide, you will create a ZITADEL execution and target. After a user is created through the API, the target is called. + +## Prerequisites + +Before you start, make sure you have everything set up correctly. + +- You need to be at least a ZITADEL [_IAM_OWNER_](/guides/manage/console/managers) +- Your ZITADEL instance needs to have the actions feature enabled. + +## Start example target + +To start a simple HTTP server locally, which receives the webhook call, the following code example can be used: + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +// webhook HandleFunc to read the request body and then print out the contents +func webhook(w http.ResponseWriter, req *http.Request) { + // read the body content + sentBody, err := io.ReadAll(req.Body) + if err != nil { + // if there was an error while reading the body return an error + http.Error(w, "error", http.StatusInternalServerError) + return + } + // print out the read content + fmt.Println(string(sentBody)) +} + +func main() { + // handle the HTTP call under "/webhook" + http.HandleFunc("/webhook", webhook) + + // start an HTTP server with the before defined function to handle the endpoint under "http://localhost:8090" + http.ListenAndServe(":8090", nil) +} +``` + +What happens here is only a target which prints out the received request, which could also be handled with a different logic. + +## Create target + +As you see in the example above the target is created with HTTP and port '8090' and if we want to use it as webhook, the target can be created as follows: + +[Create a target](/apis/resources/action_service_v3/action-service-create-target) + +```shell +curl -L -X POST 'https://$CUSTOM-DOMAIN/v3alpha/targets' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "name": "local webhook", + "restWebhook": { + "interruptOnError": true + }, + "endpoint": "http://localhost:8090/webhook", + "timeout": "10s" +}' +``` + +Save the returned ID to set in the execution. + +## Set execution + +To call the target just created before, with the intention to print the request used for user creation by the user V2 API, we define an execution with a method condition. + +[Set an execution](/apis/resources/action_service_v3/action-service-set-execution) + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v3alpha/executions' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "condition": { + "request": { + "method": "/zitadel.user.v2beta.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + } + ] +}' +``` + +## Example call + +Now on every call on `/zitadel.user.v2beta.UserService/AddHumanUser` the local server prints out the received body of the request: + +```shell +curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/users/human' \ +-H 'Content-Type: application/json' \ +-H 'Accept: application/json' \ +-H 'Authorization: Bearer ' \ +--data-raw '{ + "profile": { + "givenName": "Example_given", + "familyName": "Example_family" + }, + "email": { + "email": "example@example.com" + } +}' +``` + +Should print out something like, also described under [Sent information Request](./introduction#sent-information-request): +```shell +{ + "fullMethod": "/zitadel.user.v2beta.UserService/AddHumanUser", + "instanceID": "262851882718855632", + "orgID": "262851882718921168", + "projectID": "262851882719052240", + "userID": "262851882718986704", + "request": { + "profile": { + "given_name": "Example_given", + "family_name": "Example_family" + }, + "email": { + "email": "example@example.com" + } + } +} +``` + + diff --git a/docs/docs/apis/actionsv2/introduction.md b/docs/docs/apis/actionsv2/introduction.md new file mode 100644 index 0000000000..16adaac423 --- /dev/null +++ b/docs/docs/apis/actionsv2/introduction.md @@ -0,0 +1,167 @@ +--- +title: Actions V2 +--- + +This page describes the options you have when defining ZITADEL Actions V2. + +## Endpoints + +ZITADEL sends an HTTP Post request to the endpoint set as Target, the received request than can be edited and send back or custom processes can be handled. + +### Sent information Request + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": "full request of the call" +} +``` + +### Sent information Response + +The information sent to the Endpoint is structured as JSON: + +```json +{ + "fullMethod": "full method of the GRPC call", + "instanceID": "instanceID of the called instance", + "orgID": "ID of the organization related to the calling context", + "projectID": "ID of the project related to the used application", + "userID": "ID of the calling user", + "request": "full request of the call", + "response": "full response of the call" +} +``` + +## Target + +The Target describes how ZITADEL interacts with the Endpoint. + +There are different types of Targets: + +- `Webhook`, the call handles the status code but response is irrelevant, can be InterruptOnError +- `Call`, the call handles the status code and response, can be InterruptOnError +- `Async`, the call handles neither status code nor response, but can be called in parallel with other Targets + +`InterruptOnError` means that the Execution gets interrupted if any of the calls return with a status code >= 400, and the next Target will not be called anymore. + +The API documentation to create a target can be found [here](/apis/resources/action_service_v3/action-service-create-target) + +## Execution + +ZITADEL decides on specific conditions if one or more Targets have to be called. +The Execution resource contains 2 parts, the condition and the called targets. + +The condition can be defined for 4 types of processes: + +- `Requests`, before a request is processed by ZITADEL +- `Responses`, before a response is sent back to the application +- `Functions`, handling specific functionality in the logic of ZITADEL +- `Events`, after a specific event happened and was stored in ZITADEL + +The API documentation to set an Execution can be found [here](/apis/resources/action_service_v3/action-service-set-execution) + +### Condition Best Match + +As the conditions can be defined on different levels, ZITADEL tries to find out which Execution is the best match. +This means that for example if you have an Execution defined on `all requests`, on the service `zitadel.user.v2beta.UserService` and on `/zitadel.user.v2beta.UserService/AddHumanUser`, +ZITADEL would with a call on the `/zitadel.user.v2beta.UserService/AddHumanUser` use the Executions with the following priority: + +1. `/zitadel.user.v2beta.UserService/AddHumanUser` +2. `zitadel.user.v2beta.UserService` +3. `all` + +If you then have a call on `/zitadel.user.v2beta.UserService/UpdateHumanUser` the following priority would be found: + +1. `zitadel.user.v2beta.UserService` +2. `all` + +And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. + +### Targets and Includes + +An execution can not only contain a list of Targets, but also Includes. +The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. + +If you define 2 Executions as follows: + +```json +{ + "condition": { + "request": { + "service": "zitadel.user.v2beta.UserService" + } + }, + "targets": [ + { + "target": "" + } + ] +} +``` + +```json +{ + "condition": { + "request": { + "method": "/zitadel.user.v2beta.UserService/AddHumanUser" + } + }, + "targets": [ + { + "target": "" + }, + { + "include": { + "request": { + "service": "zitadel.user.v2beta.UserService" + } + } + } + ] +} +``` + +The called Targets on "/zitadel.user.v2beta.UserService/AddHumanUser" would be, in order: + +1. `` +2. `` + +### Condition for Requests and Responses + +For Request and Response there are 3 levels the condition can be defined: + +- `Method`, handling a request or response of a specific GRPC full method, which includes the service name and method of the ZITADEL API +- `Service`, handling any request or response under a service of the ZITADEL API +- `All`, handling any request or response under the ZITADEL API + +The available conditions can be found under: +- [All available Methods](/apis/resources/action_service_v3/action-service-list-execution-methods), for example `/zitadel.user.v2beta.UserService/AddHumanUser` +- [All available Services](/apis/resources/action_service_v3/action-service-list-execution-services), for example `zitadel.user.v2beta.UserService` + +### Condition for Functions + +Replace the current Actions with the following flows: + +- [Internal Authentication](../actions/internal-authentication) +- [External Authentication](../actions/external-authentication) +- [Complement Token](../actions/complement-token) +- [Customize SAML Response](../actions/customize-samlresponse) + +The available conditions can be found under [all available Functions](/apis/resources/action_service_v3/action-service-list-execution-functions). + +### Condition for Events + +For event there are 3 levels the condition can be defined: + +- Event, handling a specific event +- Group, handling a specific group of events +- All, handling any event in ZITADEL + +The concept of events can be found under [Events](/concepts/architecture/software#events) \ No newline at end of file diff --git a/docs/docs/concepts/features/actions.md b/docs/docs/concepts/features/actions.md index c3c18b0611..21626d237c 100644 --- a/docs/docs/concepts/features/actions.md +++ b/docs/docs/concepts/features/actions.md @@ -7,7 +7,7 @@ By using ZITADEL actions, you can manipulate ZITADELs behavior on specific Event This is useful when you have special business requirements that ZITADEL doesn't support out-of-the-box. :::info -We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. +We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. ::: ## Why actions? diff --git a/docs/docs/concepts/features/actions_v2.md b/docs/docs/concepts/features/actions_v2.md new file mode 100644 index 0000000000..3c78229045 --- /dev/null +++ b/docs/docs/concepts/features/actions_v2.md @@ -0,0 +1,40 @@ +--- +title: ZITADEL Actions v2 +sidebar_label: Actions v2 +--- + +By using ZITADEL actions V2, you can manipulate ZITADELs behavior on specific API calls, events or functions. +This is useful when you have special business requirements that ZITADEL doesn't support out-of-the-box. + +:::info +We're working on Actions continuously. In the [roadmap](https://zitadel.com/roadmap), you see how we are planning to expand and improve it. Please tell us about your needs and help us prioritize further fixes and features. +::: + +## Why actions? +ZITADEL can't anticipate and solve every possible business rule and integration requirements from all ZITADEL users. Here are some examples: +- A business requires domain specific data validation before a user can be created or authenticated. +- A business needs to automate tasks. Roles should be assigned to users based on their ADFS 2016+ groups. +- A business needs to store metadata on a user that is used for integrating applications. +- A business needs to restrict the users who are allowed to register to a certain organization by their email domains. + +With actions, ZITADEL provides a way to solve such problems. + +## How it works +There are 3 components necessary: +- Endpoint, an external endpoint with the desired logic, can be whatever is necessary as long as it can receive an HTTP Post request. +- Target, a resource in ZITADEL with all necessary information how to trigger an endpoint +- Execution, a resource in ZITADEL with the information when to trigger which targets + +The process is that ZITADEL decides at certain points that with a defined Execution a call to the defined Target(s) is triggered, +so that everybody can implement their custom behaviour for as many processes as possible. + +Possible conditions for the Execution: +- Request, to react to or manipulate requests to ZITADEL, for example add information to newly created users +- Response, to react to or manipulate responses to ZITADEL, for example to provision newly created users to other systems +- Function, to react to different functionality in ZITADEL, replaces [Actions](/concepts/features/actions) +- Event, to create to different events which get created in ZITADEL, for example to inform somebody if a user gets locked + +## Further reading + +- [Actions v2 example execution locally](/apis/actionsv2/execution-local) +- [Actions v2 reference](/apis/actionsv2/introduction#action) \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index ddb73badde..48eda263da 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -795,6 +795,15 @@ module.exports = { "apis/actions/objects", ], }, + { + type: "category", + label: "Actions V2", + collapsed: false, + items: [ + "apis/actionsv2/introduction", + "apis/actionsv2/execution-local", + ], + }, { type: "doc", label: "gRPC Status Codes", diff --git a/internal/api/grpc/action/v3alpha/execution.go b/internal/api/grpc/action/v3alpha/execution.go index dde4727c2e..58a36cff22 100644 --- a/internal/api/grpc/action/v3alpha/execution.go +++ b/internal/api/grpc/action/v3alpha/execution.go @@ -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(), + } +} diff --git a/internal/api/grpc/action/v3alpha/execution_integration_test.go b/internal/api/grpc/action/v3alpha/execution_integration_test.go index 752e9b546a..4ca1b97d6f 100644 --- a/internal/api/grpc/action/v3alpha/execution_integration_test.go +++ b/internal/api/grpc/action/v3alpha/execution_integration_test.go @@ -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"}, }, }, }, diff --git a/internal/api/grpc/action/v3alpha/execution_target_integration_test.go b/internal/api/grpc/action/v3alpha/execution_target_integration_test.go new file mode 100644 index 0000000000..30afb1af6f --- /dev/null +++ b/internal/api/grpc/action/v3alpha/execution_target_integration_test.go @@ -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 +} diff --git a/internal/api/grpc/action/v3alpha/query.go b/internal/api/grpc/action/v3alpha/query.go index 582510b0bf..095eaa7973 100644 --- a/internal/api/grpc/action/v3alpha/query.go +++ b/internal/api/grpc/action/v3alpha/query.go @@ -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, "/")}}} +} diff --git a/internal/api/grpc/action/v3alpha/query_integration_test.go b/internal/api/grpc/action/v3alpha/query_integration_test.go index 31b5a8025a..b083eda5a4 100644 --- a/internal/api/grpc/action/v3alpha/query_integration_test.go +++ b/internal/api/grpc/action/v3alpha/query_integration_test.go @@ -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 +} diff --git a/internal/api/grpc/action/v3alpha/server.go b/internal/api/grpc/action/v3alpha/server.go index dfd813a5ad..952a555d24 100644 --- a/internal/api/grpc/action/v3alpha/server.go +++ b/internal/api/grpc/action/v3alpha/server.go @@ -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") } diff --git a/internal/api/grpc/action/v3alpha/target.go b/internal/api/grpc/action/v3alpha/target.go index 1a01ad2260..c57d5b607f 100644 --- a/internal/api/grpc/action/v3alpha/target.go +++ b/internal/api/grpc/action/v3alpha/target.go @@ -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 } diff --git a/internal/api/grpc/action/v3alpha/target_integration_test.go b/internal/api/grpc/action/v3alpha/target_integration_test.go index 2d45bed6d8..8b143fddb8 100644 --- a/internal/api/grpc/action/v3alpha/target_integration_test.go +++ b/internal/api/grpc/action/v3alpha/target_integration_test.go @@ -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 diff --git a/internal/api/grpc/action/v3alpha/target_test.go b/internal/api/grpc/action/v3alpha/target_test.go index f630a6c715..23e33ad9be 100644 --- a/internal/api/grpc/action/v3alpha/target_test.go +++ b/internal/api/grpc/action/v3alpha/target_test.go @@ -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), }, }, diff --git a/internal/api/grpc/server/middleware/execution_interceptor.go b/internal/api/grpc/server/middleware/execution_interceptor.go new file mode 100644 index 0000000000..ec4eee17d2 --- /dev/null +++ b/internal/api/grpc/server/middleware/execution_interceptor.go @@ -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 +} diff --git a/internal/api/grpc/server/middleware/execution_interceptor_test.go b/internal/api/grpc/server/middleware/execution_interceptor_test.go new file mode 100644 index 0000000000..bbc87c374f --- /dev/null +++ b/internal/api/grpc/server/middleware/execution_interceptor_test.go @@ -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() + } + }) + } +} diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 4f0a6140bc..ef4c271bf5 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -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(), diff --git a/internal/command/action_v2_execution.go b/internal/command/action_v2_execution.go index 164830d6e5..422e053daf 100644 --- a/internal/command/action_v2_execution.go +++ b/internal/command/action_v2_execution.go @@ -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 } diff --git a/internal/command/action_v2_execution_model.go b/internal/command/action_v2_execution_model.go index 0dbeb3d874..c53992856e 100644 --- a/internal/command/action_v2_execution_model.go +++ b/internal/command/action_v2_execution_model.go @@ -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() } diff --git a/internal/command/action_v2_execution_model_test.go b/internal/command/action_v2_execution_model_test.go index 4c74a146e8..1c0f535d09 100644 --- a/internal/command/action_v2_execution_model_test.go +++ b/internal/command/action_v2_execution_model_test.go @@ -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( diff --git a/internal/command/action_v2_execution_test.go b/internal/command/action_v2_execution_test.go index 6a40eadf5e..c8f91f49b2 100644 --- a/internal/command/action_v2_execution_test.go +++ b/internal/command/action_v2_execution_test.go @@ -19,10 +19,9 @@ func existsMock(exists bool) func(method string) bool { return exists } } - func TestCommands_SetExecutionRequest(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore grpcMethodExists func(method string) bool grpcServiceExists func(method string) bool } @@ -45,7 +44,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -60,13 +59,13 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -75,7 +74,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -85,7 +84,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -94,7 +93,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -105,7 +104,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -114,7 +113,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -125,83 +124,16 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, }, }, - { - "target and include, error", - fields{ - eventstore: eventstoreExpect(t), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{ - Targets: []string{"invalid"}, - Includes: []string{"invalid"}, - }, - resourceOwner: "org1", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "push failed, error", - fields{ - eventstore: eventstoreExpect(t, - expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), - ), - expectPushFailed( - zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.valid", "org1"), - []string{"target"}, - nil, - ), - ), - ), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "valid", - "", - false, - }, - set: &SetExecution{ - Targets: []string{"target"}, - }, - resourceOwner: "org1", - }, - res{ - err: zerrors.IsPreconditionFailed, - }, - }, { "method not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(false), }, args{ @@ -212,9 +144,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -223,7 +157,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "service not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcServiceExists: existsMock(false), }, args{ @@ -234,9 +168,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -245,25 +181,25 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -277,38 +213,40 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -322,38 +260,40 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -366,20 +306,22 @@ func TestCommands_SetExecutionRequest(t *testing.T) { true, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push not found, method include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), grpcMethodExists: existsMock(true), @@ -392,9 +334,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -403,21 +347,23 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, method include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.include", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/include", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), - nil, - []string{"request.include"}, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, ), ), ), @@ -431,20 +377,22 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push not found, service include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), grpcServiceExists: existsMock(true), @@ -457,9 +405,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -468,21 +418,23 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, service include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.include", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/include", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), - nil, - []string{"request.include"}, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, ), ), ), @@ -496,20 +448,22 @@ func TestCommands_SetExecutionRequest(t *testing.T) { false, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push not found, all include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -521,9 +475,11 @@ func TestCommands_SetExecutionRequest(t *testing.T) { true, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -532,21 +488,23 @@ func TestCommands_SetExecutionRequest(t *testing.T) { { "push ok, all include", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.include", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/include", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request", "org1"), - nil, - []string{"request.include"}, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, ), ), ), @@ -559,13 +517,15 @@ func TestCommands_SetExecutionRequest(t *testing.T) { true, }, set: &SetExecution{ - Includes: []string{"request.include"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeInclude, Target: "request/include"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -573,7 +533,7 @@ func TestCommands_SetExecutionRequest(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), GrpcMethodExisting: tt.fields.grpcMethodExists, GrpcServiceExisting: tt.fields.grpcServiceExists, } @@ -593,7 +553,7 @@ func TestCommands_SetExecutionRequest(t *testing.T) { func TestCommands_SetExecutionResponse(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore grpcMethodExists func(method string) bool grpcServiceExists func(method string) bool } @@ -616,7 +576,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -631,13 +591,13 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -646,7 +606,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -656,7 +616,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -665,7 +625,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -676,7 +636,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -685,7 +645,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(true), }, args{ @@ -696,30 +656,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "target and include, error", - fields{ - eventstore: eventstoreExpect(t), - grpcMethodExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionAPICondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{ - Targets: []string{"invalid"}, - Includes: []string{"invalid"}, - }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -728,24 +665,24 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -759,9 +696,11 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -770,7 +709,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "method not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcMethodExists: existsMock(false), }, args{ @@ -781,9 +720,11 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -792,7 +733,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "service not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), grpcServiceExists: existsMock(false), }, args{ @@ -803,9 +744,11 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -814,25 +757,25 @@ func TestCommands_SetExecutionResponse(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), + target.NewAggregate("target", "instance"), "name", domain.TargetTypeWebhook, "https://example.com", time.Second, true, - true, ), ), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -846,38 +789,31 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -891,38 +827,31 @@ func TestCommands_SetExecutionResponse(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -935,13 +864,15 @@ func TestCommands_SetExecutionResponse(t *testing.T) { true, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -949,7 +880,7 @@ func TestCommands_SetExecutionResponse(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), GrpcMethodExisting: tt.fields.grpcMethodExists, GrpcServiceExisting: tt.fields.grpcServiceExists, } @@ -969,7 +900,7 @@ func TestCommands_SetExecutionResponse(t *testing.T) { func TestCommands_SetExecutionEvent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore eventExists func(string) bool eventGroupExists func(string) bool } @@ -992,7 +923,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1007,13 +938,13 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionEventCondition{}, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1022,7 +953,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1032,7 +963,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1041,7 +972,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventExists: existsMock(true), }, args{ @@ -1052,7 +983,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1061,7 +992,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventExists: existsMock(true), }, args{ @@ -1072,30 +1003,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{}, - resourceOwner: "org1", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "target and include, error", - fields{ - eventstore: eventstoreExpect(t), - eventExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: &ExecutionEventCondition{ - "notvalid", - "", - false, - }, - set: &SetExecution{ - Targets: []string{"invalid"}, - Includes: []string{"invalid"}, - }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1104,26 +1012,17 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1137,9 +1036,11 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1148,7 +1049,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "event not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventExists: existsMock(false), }, args{ @@ -1159,9 +1060,11 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1170,7 +1073,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "group not found, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), eventGroupExists: existsMock(false), }, args{ @@ -1181,9 +1084,11 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1192,25 +1097,16 @@ func TestCommands_SetExecutionEvent(t *testing.T) { { "push ok, event target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.event", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1224,38 +1120,31 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, group target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.group", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/group.*", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1269,38 +1158,31 @@ func TestCommands_SetExecutionEvent(t *testing.T) { false, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1313,13 +1195,15 @@ func TestCommands_SetExecutionEvent(t *testing.T) { true, }, set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -1327,7 +1211,7 @@ func TestCommands_SetExecutionEvent(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), EventExisting: tt.fields.eventExists, EventGroupExisting: tt.fields.eventGroupExists, } @@ -1347,7 +1231,7 @@ func TestCommands_SetExecutionEvent(t *testing.T) { func TestCommands_SetExecutionFunction(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore actionFunctionExists func(string) bool } type args struct { @@ -1369,7 +1253,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(true), }, args{ @@ -1385,13 +1269,13 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: "", set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1400,14 +1284,14 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "empty executionType, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(true), }, args{ ctx: context.Background(), cond: "function", set: &SetExecution{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1416,33 +1300,14 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "empty target, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(true), }, args{ ctx: context.Background(), cond: "function", set: &SetExecution{}, - resourceOwner: "org1", - }, - res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - "target and include, error", - fields{ - eventstore: eventstoreExpect(t), - actionFunctionExists: existsMock(true), - }, - args{ - ctx: context.Background(), - cond: "function", - set: &SetExecution{ - Targets: []string{"invalid"}, - Includes: []string{"invalid"}, - }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1451,26 +1316,17 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1480,9 +1336,11 @@ func TestCommands_SetExecutionFunction(t *testing.T) { ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1490,7 +1348,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { }, { "push error, function target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), actionFunctionExists: existsMock(true), @@ -1499,9 +1357,11 @@ func TestCommands_SetExecutionFunction(t *testing.T) { ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1510,16 +1370,18 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "push error, function not existing", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), actionFunctionExists: existsMock(false), }, args{ ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1528,25 +1390,16 @@ func TestCommands_SetExecutionFunction(t *testing.T) { { "push ok, function target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( - eventFromEventPusher( - target.NewAddedEvent(context.Background(), - target.NewAggregate("target", "org1"), - "name", - domain.TargetTypeWebhook, - "https://example.com", - time.Second, - true, - true, - ), - ), + targetAddEvent("target", "instance"), ), expectPush( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), @@ -1556,13 +1409,15 @@ func TestCommands_SetExecutionFunction(t *testing.T) { ctx: context.Background(), cond: "function", set: &SetExecution{ - Targets: []string{"target"}, + Targets: []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -1570,7 +1425,7 @@ func TestCommands_SetExecutionFunction(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), ActionFunctionExisting: tt.fields.actionFunctionExists, } details, err := c.SetExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.set, tt.args.resourceOwner) @@ -1589,7 +1444,7 @@ func TestCommands_SetExecutionFunction(t *testing.T) { func TestCommands_DeleteExecutionRequest(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1609,7 +1464,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1623,12 +1478,12 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1637,7 +1492,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1646,7 +1501,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "notvalid", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1655,20 +1510,21 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request.valid", "org1"), + execution.NewAggregate("request/valid", "instance"), ), ), ), @@ -1680,7 +1536,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1689,7 +1545,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "not found, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1700,7 +1556,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1709,19 +1565,20 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request.method", "org1"), + execution.NewAggregate("request/method", "instance"), ), ), ), @@ -1733,30 +1590,31 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request.service", "org1"), + execution.NewAggregate("request/service", "instance"), ), ), ), @@ -1768,30 +1626,31 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "service", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("request", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("request", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("request", "org1"), + execution.NewAggregate("request", "instance"), ), ), ), @@ -1803,11 +1662,11 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -1815,7 +1674,7 @@ func TestCommands_DeleteExecutionRequest(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.DeleteExecutionRequest(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { @@ -1833,7 +1692,7 @@ func TestCommands_DeleteExecutionRequest(t *testing.T) { func TestCommands_DeleteExecutionResponse(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -1853,7 +1712,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1867,12 +1726,12 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionAPICondition{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1881,7 +1740,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "no valid cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -1890,7 +1749,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "notvalid", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -1899,20 +1758,21 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response.valid", "org1"), + execution.NewAggregate("response/valid", "instance"), ), ), ), @@ -1924,7 +1784,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -1933,7 +1793,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "not found, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1944,7 +1804,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -1953,19 +1813,20 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { { "push ok, method target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.method", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/method", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response.method", "org1"), + execution.NewAggregate("response/method", "instance"), ), ), ), @@ -1977,30 +1838,31 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, service target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response.service", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response/service", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response.service", "org1"), + execution.NewAggregate("response/service", "instance"), ), ), ), @@ -2012,30 +1874,31 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "service", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push ok, all target", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("response", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("response", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("response", "org1"), + execution.NewAggregate("response", "instance"), ), ), ), @@ -2047,11 +1910,11 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -2059,7 +1922,7 @@ func TestCommands_DeleteExecutionResponse(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.DeleteExecutionResponse(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { @@ -2077,7 +1940,7 @@ func TestCommands_DeleteExecutionResponse(t *testing.T) { func TestCommands_DeleteExecutionEvent(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -2097,7 +1960,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -2111,12 +1974,12 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: &ExecutionEventCondition{}, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -2125,20 +1988,21 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), + execution.NewAggregate("event/valid", "instance"), ), ), ), @@ -2150,7 +2014,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -2159,7 +2023,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push error, not existing", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2170,7 +2034,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2179,7 +2043,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push error, event", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2190,7 +2054,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2199,19 +2063,20 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push ok, event", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/valid", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event.valid", "org1"), + execution.NewAggregate("event/valid", "instance"), ), ), ), @@ -2223,18 +2088,18 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push error, group", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2245,7 +2110,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "valid", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2254,19 +2119,20 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push ok, group", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event.group", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event/group", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event.group", "org1"), + execution.NewAggregate("event/group.*", "instance"), ), ), ), @@ -2278,18 +2144,18 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "group", false, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, { "push error, all", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -2300,7 +2166,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2309,19 +2175,20 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { { "push ok, all", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("event", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("event", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("event", "org1"), + execution.NewAggregate("event", "instance"), ), ), ), @@ -2333,11 +2200,11 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { "", true, }, - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -2345,7 +2212,7 @@ func TestCommands_DeleteExecutionEvent(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.DeleteExecutionEvent(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { @@ -2363,7 +2230,7 @@ func TestCommands_DeleteExecutionEvent(t *testing.T) { func TestCommands_DeleteExecutionFunction(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -2383,7 +2250,7 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "no resourceowner, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), @@ -2397,12 +2264,12 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "no cond, error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), }, args{ ctx: context.Background(), cond: "", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsErrorInvalidArgument, @@ -2411,20 +2278,21 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "push failed, error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPushFailed( zerrors.ThrowPreconditionFailed(nil, "id", "name already exists"), execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), + execution.NewAggregate("function/function", "instance"), ), ), ), @@ -2432,7 +2300,7 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { args{ ctx: context.Background(), cond: "function", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsPreconditionFailed, @@ -2441,14 +2309,14 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "push error, not existing", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, args{ ctx: context.Background(), cond: "function", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ err: zerrors.IsNotFound, @@ -2457,19 +2325,20 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { { "push ok, function", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( - execution.NewSetEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), - []string{"target"}, - nil, + execution.NewSetEventV2(context.Background(), + execution.NewAggregate("function/function", "instance"), + []*execution.Target{ + {Type: domain.ExecutionTargetTypeTarget, Target: "target"}, + }, ), ), ), expectPush( execution.NewRemovedEvent(context.Background(), - execution.NewAggregate("function.function", "org1"), + execution.NewAggregate("function/function", "instance"), ), ), ), @@ -2477,11 +2346,11 @@ func TestCommands_DeleteExecutionFunction(t *testing.T) { args{ ctx: context.Background(), cond: "function", - resourceOwner: "org1", + resourceOwner: "instance", }, res{ details: &domain.ObjectDetails{ - ResourceOwner: "org1", + ResourceOwner: "instance", }, }, }, @@ -2489,7 +2358,7 @@ func TestCommands_DeleteExecutionFunction(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.DeleteExecutionFunction(tt.args.ctx, tt.args.cond, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/action_v2_target.go b/internal/command/action_v2_target.go index 1bd2a62070..913bfb2299 100644 --- a/internal/command/action_v2_target.go +++ b/internal/command/action_v2_target.go @@ -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 diff --git a/internal/command/action_v2_target_model.go b/internal/command/action_v2_target_model.go index 60f70c40e0..24dd76c80a 100644 --- a/internal/command/action_v2_target_model.go +++ b/internal/command/action_v2_target_model.go @@ -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)) } diff --git a/internal/command/action_v2_target_model_test.go b/internal/command/action_v2_target_model_test.go index 4c0f31c8e0..8042da23b1 100644 --- a/internal/command/action_v2_target_model_test.go +++ b/internal/command/action_v2_target_model_test.go @@ -20,7 +20,6 @@ func targetAddEvent(aggID, resourceOwner string) *target.AddedEvent { "https://example.com", time.Second, false, - false, ) } diff --git a/internal/command/action_v2_target_test.go b/internal/command/action_v2_target_test.go index 8600ffabf1..ef60baae49 100644 --- a/internal/command/action_v2_target_test.go +++ b/internal/command/action_v2_target_test.go @@ -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 { diff --git a/internal/domain/execution.go b/internal/domain/execution.go index 08f215bf08..eb83a0578c 100644 --- a/internal/domain/execution.go +++ b/internal/domain/execution.go @@ -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 +) diff --git a/internal/domain/target.go b/internal/domain/target.go index 83ab85478e..1e08f91cf0 100644 --- a/internal/domain/target.go +++ b/internal/domain/target.go @@ -4,7 +4,8 @@ type TargetType uint const ( TargetTypeWebhook TargetType = iota - TargetTypeRequestResponse + TargetTypeCall + TargetTypeAsync ) type TargetState int32 diff --git a/internal/execution/execution.go b/internal/execution/execution.go new file mode 100644 index 0000000000..abb2153fc2 --- /dev/null +++ b/internal/execution/execution.go @@ -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") +} diff --git a/internal/execution/execution_test.go b/internal/execution/execution_test.go new file mode 100644 index 0000000000..2d891148df --- /dev/null +++ b/internal/execution/execution_test.go @@ -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) + }) + } +} diff --git a/internal/integration/assert.go b/internal/integration/assert.go index d5b7353aa2..225b3399b4 100644 --- a/internal/integration/assert.go +++ b/internal/integration/assert.go @@ -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)) + } } diff --git a/internal/integration/client.go b/internal/integration/client.go index eb95134bc3..cb22e5d552 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -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)) } diff --git a/internal/query/execution.go b/internal/query/execution.go index 5a8a7e0f79..ff501f8201 100644 --- a/internal/query/execution.go +++ b/internal/query/execution.go @@ -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") - } - return execution, nil - } + 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 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 } diff --git a/internal/query/execution_targets.sql b/internal/query/execution_targets.sql new file mode 100644 index 0000000000..32257f4a1f --- /dev/null +++ b/internal/query/execution_targets.sql @@ -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 \ No newline at end of file diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go index 20058b1ae0..b989d539a0 100644 --- a/internal/query/execution_test.go +++ b/internal/query/execution_test.go @@ -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"}, + }, }, }, { diff --git a/internal/query/projection/execution.go b/internal/query/projection/execution.go index 350a27ec27..9001fcd3ba 100644 --- a/internal/query/projection/execution.go +++ b/internal/query/projection/execution.go @@ -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), diff --git a/internal/query/projection/execution_test.go b/internal/query/projection/execution_test.go index 537e8e0586..27d6e89258 100644 --- a/internal/query/projection/execution_test.go +++ b/internal/query/projection/execution_test.go @@ -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", }, diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 0f8b2518a6..de7b6135bd 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -269,8 +269,8 @@ func newProjectionsList() { RestrictionsProjection, SystemFeatureProjection, InstanceFeatureProjection, - ExecutionProjection, TargetProjection, + ExecutionProjection, UserSchemaProjection, } } diff --git a/internal/query/projection/target.go b/internal/query/projection/target.go index af801002a9..7b7c46d257 100644 --- a/internal/query/projection/target.go +++ b/internal/query/projection/target.go @@ -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)) } diff --git a/internal/query/projection/target_test.go b/internal/query/projection/target_test.go index 1ba0c9379d..30067c6640 100644 --- a/internal/query/projection/target_test.go +++ b/internal/query/projection/target_test.go @@ -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", }, diff --git a/internal/query/search_query.go b/internal/query/search_query.go index b4944a8f2d..868df84fe9 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -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 +} diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index c64b2c131c..19c1dbcf41 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -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, diff --git a/internal/query/target.go b/internal/query/target.go index ecb60dca79..c5d8f893ad 100644 --- a/internal/query/target.go +++ b/internal/query/target.go @@ -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 { diff --git a/internal/query/target_test.go b/internal/query/target_test.go index d1003fc8d8..61a61e9e45 100644 --- a/internal/query/target_test.go +++ b/internal/query/target_test.go @@ -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, }, }, { diff --git a/internal/query/targets_by_execution_id.sql b/internal/query/targets_by_execution_id.sql new file mode 100644 index 0000000000..6b564104e5 --- /dev/null +++ b/internal/query/targets_by_execution_id.sql @@ -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; diff --git a/internal/query/targets_by_execution_ids.sql b/internal/query/targets_by_execution_ids.sql new file mode 100644 index 0000000000..c1361d9320 --- /dev/null +++ b/internal/query/targets_by_execution_ids.sql @@ -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; diff --git a/internal/repository/execution/aggregate.go b/internal/repository/execution/aggregate.go index 973bb17854..eea14f8468 100644 --- a/internal/repository/execution/aggregate.go +++ b/internal/repository/execution/aggregate.go @@ -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 { diff --git a/internal/repository/execution/eventstore.go b/internal/repository/execution/eventstore.go index bf4e7bc79f..2d93000217 100644 --- a/internal/repository/execution/eventstore.go +++ b/internal/repository/execution/eventstore.go @@ -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]) } diff --git a/internal/repository/execution/execution.go b/internal/repository/execution/execution.go index 4c1a85a6b6..a6851b4495 100644 --- a/internal/repository/execution/execution.go +++ b/internal/repository/execution/execution.go @@ -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, } } diff --git a/internal/repository/target/target.go b/internal/repository/target/target.go index 2d50857cba..85e3ae7023 100644 --- a/internal/repository/target/target.go +++ b/internal/repository/target/target.go @@ -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 diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index aa6223fd05..b1ba504c26 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -484,6 +484,7 @@ Errors: NotActive: Действието не е активно NotInactive: Действието не е неактивно MaxAllowed: Не са разрешени допълнителни активни действия + NotEnabled: Функцията „Действие“ не е активирана Flow: FlowTypeMissing: Липсва FlowType Empty: Потокът вече е празен diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index cc35a3165a..ab303086b5 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -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ý diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index c36ea20c46..a5cfd521cd 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -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 diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index bec1ac2a72..fc1ef19ac0 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -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 diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 89eca6f58e..fff4bf468d 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -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 diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 308dea53af..500d3f3225 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -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 diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 9537a5b790..ce1b04b982 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -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 diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 130e89d55f..2e980e4925 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -459,6 +459,7 @@ Errors: NotActive: アクションはアクティブではありません NotInactive: アクションは非アクティブではありません MaxAllowed: 追加のアクティブアクションは許可されていません + NotEnabled: 機能「アクション」が有効になっていません Flow: FlowTypeMissing: フロータイプがありません Empty: フローはすでに空です diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 293afdd843..eceb252c71 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -469,6 +469,7 @@ Errors: NotActive: Акцијата не е активна NotInactive: Акцијата не е неактивна MaxAllowed: Не се дозволени дополнителни активни акции + NotEnabled: Функцијата „Акција“ не е овозможена Flow: FlowTypeMissing: FlowType не е наведен Empty: Flow е веќе празен diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index d0afcfc072..a03d9e177a 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -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 diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 381ab9f6ad..7d54595a2f 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -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 diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 9f0d94d9c4..4e16036395 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -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 diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 172f2bee01..5b3e46e6bd 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -463,6 +463,7 @@ Errors: NotActive: Действие не активно NotInactive: Действие не является неактивным MaxAllowed: Дополнительные активные действия запрещены + NotEnabled: Функция «Действие» не включена Flow: FlowTypeMissing: Тип процесса отсутствует Empty: Процесс уже пуст diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 171807a1a8..9b6b30b14f 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -470,6 +470,7 @@ Errors: NotActive: 动作不是启用状态 NotInactive: 动作不是停用状态 MaxAllowed: 不允许额外的动作 + NotEnabled: 未启用“操作”功能 Flow: FlowTypeMissing: 缺少身份认证流程类型 Empty: 身份认证流程为空 diff --git a/proto/zitadel/action/v3alpha/action_service.proto b/proto/zitadel/action/v3alpha/action_service.proto index 4c37276326..da174b45d0 100644 --- a/proto/zitadel/action/v3alpha/action_service.proto +++ b/proto/zitadel/action/v3alpha/action_service.proto @@ -423,22 +423,26 @@ message CreateTargetRequest { option (validate.required) = true; SetRESTWebhook rest_webhook = 2; - SetRESTRequestResponse rest_request_response = 3; + SetRESTCall rest_call = 3; + SetRESTAsync rest_async = 4; } // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 4 [ + google.protobuf.Duration timeout = 5 [ (validate.rules).duration = {gt: {seconds: 0}, required: true}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"10s\""; } ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 5; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 6; - } + string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"https://example.com/hooks/ip_check\""; + } + ]; } message CreateTargetResponse { @@ -472,21 +476,24 @@ message UpdateTargetRequest { // or its target URL. oneof target_type { SetRESTWebhook rest_webhook = 3; - SetRESTRequestResponse rest_request_response = 4; + SetRESTCall rest_call = 4; + SetRESTAsync rest_async = 5; } // Optionally change the timeout, which defines the duration until ZITADEL cancels the execution. - optional google.protobuf.Duration timeout = 5 [ + optional google.protobuf.Duration timeout = 6 [ (validate.rules).duration = {gt: {seconds: 0}}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"10s\""; } ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 6; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 7; - } + + optional string endpoint = 7 [ + (validate.rules).string = {max_len: 1000, uri: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 1000, + example: "\"https://example.com/hooks/ip_check\""; + } + ]; } message UpdateTargetResponse { @@ -554,10 +561,8 @@ message GetTargetByIDResponse { message SetExecutionRequest { // Defines the condition type and content of the condition for execution. Condition condition = 1; - // Defines the execution targets which are defined as a different resource, which are called in the defined conditions. - repeated string targets = 2; - // Defines other executions as included with the same condition-types. - repeated string includes = 3; + // Ordered list of targets/includes called during the execution. + repeated zitadel.action.v3alpha.ExecutionTargetType targets = 2; } message SetExecutionResponse { diff --git a/proto/zitadel/action/v3alpha/execution.proto b/proto/zitadel/action/v3alpha/execution.proto index c8b85f50a0..6f24471185 100644 --- a/proto/zitadel/action/v3alpha/execution.proto +++ b/proto/zitadel/action/v3alpha/execution.proto @@ -14,17 +14,20 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; message Execution { - string execution_id = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"request.zitadel.session.v2beta.SessionService\""; - } - ]; + Condition Condition = 1; // Details provide some base information (such as the last change date) of the target. zitadel.object.v2beta.Details details = 2; - // Targets which are called in the defined conditions. - repeated string targets = 3; - // Included executions with the same condition-types. - repeated string includes = 4; + // List of ordered list of targets/includes called during the execution. + repeated ExecutionTargetType targets = 3; +} + +message ExecutionTargetType { + oneof type { + // Unique identifier of existing target to call. + string target = 1; + // Unique identifier of existing execution to include targets of. + Condition include = 2; + } } message Condition { @@ -37,7 +40,7 @@ message Condition { // Condition-type to execute on response if a request on the defined API point happens. ResponseExecution response = 2; // Condition-type to execute if function is used, replaces actions v1. - string function = 3; + FunctionExecution function = 3; // Condition-type to execute if an event is created in the system. EventExecution event = 4; } @@ -95,6 +98,11 @@ message ResponseExecution { } } +// Executed on the specified function +message FunctionExecution { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1000}]; +} + message EventExecution{ // Condition for the event execution, only one possible. oneof condition{ diff --git a/proto/zitadel/action/v3alpha/query.proto b/proto/zitadel/action/v3alpha/query.proto index 26093305bc..a32caacfba 100644 --- a/proto/zitadel/action/v3alpha/query.proto +++ b/proto/zitadel/action/v3alpha/query.proto @@ -43,7 +43,7 @@ message TargetQuery { message IncludeQuery { // Defines the include to query for. - string include = 1 [ + Condition include = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "the id of the include" example: "\"request.zitadel.session.v2beta.SessionService\""; diff --git a/proto/zitadel/action/v3alpha/target.proto b/proto/zitadel/action/v3alpha/target.proto index a034c58ace..92dda32bbb 100644 --- a/proto/zitadel/action/v3alpha/target.proto +++ b/proto/zitadel/action/v3alpha/target.proto @@ -13,30 +13,21 @@ import "zitadel/protoc_gen_zitadel/v2/options.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha;action"; +// Wait for response but response body is ignored, status is checked, call is sent as post. message SetRESTWebhook { - string url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; } -message SetRESTRequestResponse { - string url = 1 [ - (validate.rules).string = {min_len: 1, max_len: 1000, uri: true}, - (google.api.field_behavior) = REQUIRED, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1, - max_length: 1000, - example: "\"https://example.com/hooks/ip_check\""; - } - ]; +// Wait for response and response body is used, status is checked, call is sent as post. +message SetRESTCall { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; } +// Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. +message SetRESTAsync {} + message Target { // ID is the read-only unique identifier of the target. string target_id = 1 [ @@ -56,18 +47,19 @@ message Target { // Defines the target type and how the response of the target is treated. oneof target_type { SetRESTWebhook rest_webhook = 4; - SetRESTRequestResponse rest_request_response = 5; + SetRESTCall rest_call = 5; + SetRESTAsync rest_async = 6; } // Timeout defines the duration until ZITADEL cancels the execution. - google.protobuf.Duration timeout = 6 [ + google.protobuf.Duration timeout = 7 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"10s\""; } ]; - oneof execution_type { - // Set the execution to run asynchronously. - bool is_async = 7; - // Define if any error stops the whole execution. By default the process continues as normal. - bool interrupt_on_error = 8; - } + + string endpoint = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\""; + } + ]; } \ No newline at end of file