diff --git a/docs/docs/apis/actionsv2/execution-local.md b/docs/docs/apis/actionsv2/execution-local.md new file mode 100644 index 00000000000..3f0ccc0fe0d --- /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 00000000000..16adaac4230 --- /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 c3c18b0611c..21626d237c5 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 00000000000..3c782290450 --- /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 ddb73baddef..48eda263dad 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 dde4727c2ee..58a36cff226 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 752e9b546af..4ca1b97d6f3 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 00000000000..30afb1af6ff --- /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 582510b0bf3..095eaa7973d 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 31b5a8025a8..b083eda5a46 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 dfd813a5ad5..952a555d24f 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 1a01ad22606..c57d5b607f4 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 2d45bed6d84..8b143fddb80 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 f630a6c7157..23e33ad9be9 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 00000000000..ec4eee17d2b --- /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 00000000000..bbc87c374fb --- /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 4f0a6140bc0..ef4c271bf5b 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 164830d6e51..422e053daff 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 0dbeb3d8740..c53992856e1 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 4c74a146e80..1c0f535d094 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 6a40eadf5e4..c8f91f49b20 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 1bd2a620708..913bfb2299a 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 60f70c40e0e..24dd76c80a9 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 4c0f31c8e0b..8042da23b19 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 8600ffabf10..ef60baae490 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 08f215bf080..eb83a0578c8 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 83ab85478ee..1e08f91cf04 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 00000000000..abb2153fc2a --- /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 00000000000..2d891148df7 --- /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 d5b7353aa26..225b3399b42 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 eb95134bc38..cb22e5d5529 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 5a8a7e0f799..ff501f8201a 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 00000000000..32257f4a1fb --- /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 20058b1ae00..b989d539a05 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 350a27ec27a..9001fcd3ba2 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 537e8e0586e..27d6e892581 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 0f8b2518a6a..de7b6135bd2 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 af801002a96..7b7c46d2574 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 1ba0c9379dc..30067c6640c 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 b4944a8f2d6..868df84fe9c 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 c64b2c131c5..19c1dbcf41b 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 ecb60dca792..c5d8f893ade 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 d1003fc8d85..61a61e9e45f 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 00000000000..6b564104e5d --- /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 00000000000..c1361d9320b --- /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 973bb178541..eea14f8468d 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 bf4e7bc79f0..2d930002178 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 4c1a85a6b62..a6851b44954 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 2d50857cba8..85e3ae70230 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 aa6223fd051..b1ba504c26d 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 cc35a3165ab..ab303086b5b 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 c36ea20c46d..a5cfd521cdb 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 bec1ac2a72a..fc1ef19ac06 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 89eca6f58ea..fff4bf468d1 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 308dea53af7..500d3f32251 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 9537a5b7900..ce1b04b9827 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 130e89d55f8..2e980e49255 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 293afdd8436..eceb252c710 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 d0afcfc0724..a03d9e177a9 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 381ab9f6ad1..7d54595a2f7 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 9f0d94d9c40..4e160363951 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 172f2bee017..5b3e46e6bd3 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 171807a1a80..9b6b30b14f3 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 4c372763266..da174b45d0b 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 c8b85f50a03..6f244711854 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 26093305bcc..a32caacfbaa 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 a034c58acee..92dda32bbb7 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