From 1c5ecba42a8ffb5b366db9c5ca28a7170cdd33a1 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Sat, 4 May 2024 11:55:57 +0200 Subject: [PATCH] feat: add action v2 execution on requests and responses (#7637) * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: add execution of targets to grpc calls * feat: split request and response logic to handle the different context information * feat: split request and response logic to handle the different context information * fix: integration test * fix: import alias * fix: refactor execution package * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * fix: refactor execution interceptor integration and unit tests * docs: basic documentation for executions and targets * fix: change order for interceptors * fix: merge back origin/main * fix: change target definition command and query side (#7735) * fix: change target definition command and query side * fix: correct refactoring name changes * fix: correct refactoring name changes * fix: changing execution defintion with target list and type * fix: changing execution definition with target list and type * fix: add back search queries for target and include * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * fix: projections change for execution with targets suffix table * docs: add example to actions v2 * docs: add example to actions v2 * fix: correct integration tests on query for executions * fix: add separate event for execution v2 as content changed * fix: add separate event for execution v2 as content changed * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes --------- Co-authored-by: adlerhurst * fix: added review comment changes * fix: added review comment changes * Update internal/api/grpc/server/middleware/execution_interceptor.go Co-authored-by: Silvan * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes * fix: added review comment changes --------- Co-authored-by: adlerhurst Co-authored-by: Elio Bischof --- docs/docs/apis/actionsv2/execution-local.md | 139 +++ docs/docs/apis/actionsv2/introduction.md | 167 +++ docs/docs/concepts/features/actions.md | 2 +- docs/docs/concepts/features/actions_v2.md | 40 + docs/sidebars.js | 9 + internal/api/grpc/action/v3alpha/execution.go | 111 +- .../v3alpha/execution_integration_test.go | 143 ++- .../execution_target_integration_test.go | 323 +++++ internal/api/grpc/action/v3alpha/query.go | 116 +- .../action/v3alpha/query_integration_test.go | 323 +++-- internal/api/grpc/action/v3alpha/server.go | 2 +- internal/api/grpc/action/v3alpha/target.go | 47 +- .../action/v3alpha/target_integration_test.go | 120 +- .../api/grpc/action/v3alpha/target_test.go | 141 ++- .../middleware/execution_interceptor.go | 179 +++ .../middleware/execution_interceptor_test.go | 778 ++++++++++++ internal/api/grpc/server/server.go | 1 + internal/command/action_v2_execution.go | 48 +- internal/command/action_v2_execution_model.go | 16 +- .../command/action_v2_execution_model_test.go | 209 ++-- internal/command/action_v2_execution_test.go | 1047 +++++++---------- internal/command/action_v2_target.go | 26 +- internal/command/action_v2_target_model.go | 23 +- .../command/action_v2_target_model_test.go | 1 - internal/command/action_v2_target_test.go | 254 ++-- internal/domain/execution.go | 14 + internal/domain/target.go | 3 +- internal/execution/execution.go | 122 ++ internal/execution/execution_test.go | 347 ++++++ internal/integration/assert.go | 8 +- internal/integration/client.go | 61 +- internal/query/execution.go | 390 ++++-- internal/query/execution_targets.sql | 11 + internal/query/execution_test.go | 94 +- internal/query/projection/execution.go | 114 +- internal/query/projection/execution_test.go | 42 +- internal/query/projection/projection.go | 2 +- internal/query/projection/target.go | 22 +- internal/query/projection/target_test.go | 14 +- internal/query/search_query.go | 31 +- internal/query/search_query_test.go | 8 +- internal/query/target.go | 17 +- internal/query/target_test.go | 96 +- internal/query/targets_by_execution_id.sql | 40 + internal/query/targets_by_execution_ids.sql | 47 + internal/repository/execution/aggregate.go | 5 +- internal/repository/execution/eventstore.go | 1 + internal/repository/execution/execution.go | 39 +- internal/repository/target/target.go | 21 +- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/mk.yaml | 1 + internal/static/i18n/nl.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/pt.yaml | 1 + internal/static/i18n/ru.yaml | 1 + internal/static/i18n/zh.yaml | 1 + .../action/v3alpha/action_service.proto | 45 +- proto/zitadel/action/v3alpha/execution.proto | 28 +- proto/zitadel/action/v3alpha/query.proto | 2 +- proto/zitadel/action/v3alpha/target.proto | 46 +- 67 files changed, 4395 insertions(+), 1554 deletions(-) create mode 100644 docs/docs/apis/actionsv2/execution-local.md create mode 100644 docs/docs/apis/actionsv2/introduction.md create mode 100644 docs/docs/concepts/features/actions_v2.md create mode 100644 internal/api/grpc/action/v3alpha/execution_target_integration_test.go create mode 100644 internal/api/grpc/server/middleware/execution_interceptor.go create mode 100644 internal/api/grpc/server/middleware/execution_interceptor_test.go create mode 100644 internal/execution/execution.go create mode 100644 internal/execution/execution_test.go create mode 100644 internal/query/execution_targets.sql create mode 100644 internal/query/targets_by_execution_id.sql create mode 100644 internal/query/targets_by_execution_ids.sql diff --git a/docs/docs/apis/actionsv2/execution-local.md b/docs/docs/apis/actionsv2/execution-local.md new file mode 100644 index 000000000..3f0ccc0fe --- /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 000000000..16adaac42 --- /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 c3c18b061..21626d237 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 000000000..3c7822904 --- /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 ddb73badd..48eda263d 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 dde4727c2..58a36cff2 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 752e9b546..4ca1b97d6 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 000000000..30afb1af6 --- /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 582510b0b..095eaa797 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 31b5a8025..b083eda5a 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 dfd813a5a..952a555d2 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 1a01ad226..c57d5b607 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 2d45bed6d..8b143fddb 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 f630a6c71..23e33ad9b 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 000000000..ec4eee17d --- /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 000000000..bbc87c374 --- /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 4f0a6140b..ef4c271bf 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 164830d6e..422e053da 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 0dbeb3d87..c53992856 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 4c74a146e..1c0f535d0 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 6a40eadf5..c8f91f49b 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 1bd2a6207..913bfb229 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 60f70c40e..24dd76c80 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 4c0f31c8e..8042da23b 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 8600ffabf..ef60baae4 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 08f215bf0..eb83a0578 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 83ab85478..1e08f91cf 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 000000000..abb2153fc --- /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 000000000..2d891148d --- /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 d5b7353aa..225b3399b 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 eb95134bc..cb22e5d55 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 5a8a7e0f7..ff501f820 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") + scanExecution +} + +func prepareExecutionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(rows *sql.Rows) (*Executions, error)) { + return sq.Select( + ExecutionColumnInstanceID.identifier(), + ExecutionColumnID.identifier(), + ExecutionColumnChangeDate.identifier(), + ExecutionColumnSequence.identifier(), + executionTargetsListCol.identifier(), + countColumn.identifier(), + ).From(executionTable.identifier()). + Join("(" + executionTargetsQuery + ") AS " + executionTargetsTableAlias.alias + " ON " + + ExecutionTargetsColumnInstanceID.identifier() + " = " + ExecutionColumnInstanceID.identifier() + " AND " + + ExecutionTargetsColumnExecutionID.identifier() + " = " + ExecutionColumnID.identifier(), + ). + PlaceholderFormat(sq.Dollar), + scanExecutions +} + +type executionTarget struct { + Position int `json:"position,omitempty"` + Include string `json:"include,omitempty"` + Target string `json:"target,omitempty"` +} + +func scanExecution(row *sql.Row) (*Execution, error) { + execution := new(Execution) + targets := make([]byte, 0) + + err := row.Scan( + &execution.ResourceOwner, + &execution.ID, + &execution.EventDate, + &execution.Sequence, + &targets, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-qzn1xycesh", "Errors.Execution.NotFound") + } + return nil, zerrors.ThrowInternal(err, "QUERY-f8sjvm4tb8", "Errors.Internal") + } + + executionTargets := make([]*executionTarget, 0) + if err := json.Unmarshal(targets, &executionTargets); err != nil { + return nil, err + } + + execution.Targets = make([]*exec.Target, len(executionTargets)) + for i := range executionTargets { + if executionTargets[i].Target != "" { + execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: executionTargets[i].Target} + } + if executionTargets[i].Include != "" { + execution.Targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: executionTargets[i].Include} + } + } + + return execution, nil +} + +func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) { + executionTargets := make([]*executionTarget, 0) + if err := json.Unmarshal(data, &executionTargets); err != nil { + return nil, err + } + + targets := make([]*exec.Target, len(executionTargets)) + // position starts with 1 + for _, item := range executionTargets { + if item.Target != "" { + targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target} + } + if item.Include != "" { + targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include} + } + } + return targets, nil +} + +func scanExecutions(rows *sql.Rows) (*Executions, error) { + executions := make([]*Execution, 0) + var count uint64 + + for rows.Next() { + execution := new(Execution) + targets := make([]byte, 0) + + err := rows.Scan( + &execution.ResourceOwner, + &execution.ID, + &execution.EventDate, + &execution.Sequence, + &targets, + &count, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, zerrors.ThrowNotFound(err, "QUERY-tbrmno85vp", "Errors.Execution.NotFound") } - return execution, nil + return nil, zerrors.ThrowInternal(err, "QUERY-tyw2ydsj84", "Errors.Internal") + } + + execution.Targets, err = executionTargetsUnmarshal(targets) + if err != nil { + return nil, err + } + executions = append(executions, execution) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-yhka3fs3mw", "Errors.Query.CloseRows") + } + + return &Executions{ + Executions: executions, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil +} + +type ExecutionTarget struct { + InstanceID string + ExecutionID string + TargetID string + TargetType domain.TargetType + Endpoint string + Timeout time.Duration + InterruptOnError bool +} + +func (e *ExecutionTarget) GetExecutionID() string { + return e.ExecutionID +} +func (e *ExecutionTarget) GetTargetID() string { + return e.TargetID +} +func (e *ExecutionTarget) IsInterruptOnError() bool { + return e.InterruptOnError +} +func (e *ExecutionTarget) GetEndpoint() string { + return e.Endpoint +} +func (e *ExecutionTarget) GetTargetType() domain.TargetType { + return e.TargetType +} +func (e *ExecutionTarget) GetTimeout() time.Duration { + return e.Timeout +} + +func scanExecutionTargets(rows *sql.Rows) ([]*ExecutionTarget, error) { + targets := make([]*ExecutionTarget, 0) + for rows.Next() { + target := new(ExecutionTarget) + + var ( + instanceID = &sql.NullString{} + executionID = &sql.NullString{} + targetID = &sql.NullString{} + targetType = &sql.NullInt32{} + endpoint = &sql.NullString{} + timeout = &sql.NullInt64{} + interruptOnError = &sql.NullBool{} + ) + + err := rows.Scan( + executionID, + instanceID, + targetID, + targetType, + endpoint, + timeout, + interruptOnError, + ) + + if err != nil { + return nil, err } + + target.InstanceID = instanceID.String + target.ExecutionID = executionID.String + target.TargetID = targetID.String + target.TargetType = domain.TargetType(targetType.Int32) + target.Endpoint = endpoint.String + target.Timeout = time.Duration(timeout.Int64) + target.InterruptOnError = interruptOnError.Bool + + targets = append(targets, target) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-37ardr0pki", "Errors.Query.CloseRows") + } + + return targets, nil } diff --git a/internal/query/execution_targets.sql b/internal/query/execution_targets.sql new file mode 100644 index 000000000..32257f4a1 --- /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 20058b1ae..b989d539a 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 350a27ec2..9001fcd3b 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 537e8e058..27d6e8925 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 0f8b2518a..de7b6135b 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 af801002a..7b7c46d25 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 1ba0c9379..30067c664 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 b4944a8f2..868df84fe 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 c64b2c131..19c1dbcf4 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 ecb60dca7..c5d8f893a 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 d1003fc8d..61a61e9e4 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 000000000..6b564104e --- /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 000000000..c1361d932 --- /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 973bb1785..eea14f846 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 bf4e7bc79..2d9300021 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 4c1a85a6b..a6851b449 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 2d50857cb..85e3ae702 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 aa6223fd0..b1ba504c2 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 cc35a3165..ab303086b 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 c36ea20c4..a5cfd521c 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 bec1ac2a7..fc1ef19ac 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 89eca6f58..fff4bf468 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 308dea53a..500d3f322 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 9537a5b79..ce1b04b98 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 130e89d55..2e980e492 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 293afdd84..eceb252c7 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 d0afcfc07..a03d9e177 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 381ab9f6a..7d54595a2 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 9f0d94d9c..4e1603639 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 172f2bee0..5b3e46e6b 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 171807a1a..9b6b30b14 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 4c3727632..da174b45d 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 c8b85f50a..6f2447118 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 26093305b..a32caacfb 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 a034c58ac..92dda32bb 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