feat(v3alpha): write actions (#8225)

# Which Problems Are Solved

The current v3alpha actions APIs don't exactly adhere to the [new
resources API
design](https://zitadel.com/docs/apis/v3#standard-resources).

# How the Problems Are Solved

- **Breaking**: The current v3alpha actions APIs are removed. This is
breaking.
- **Resource Namespace**: New v3alpha actions APIs for targets and
executions are added under the namespace /resources.
- **Feature Flag**: New v3alpha actions APIs still have to be activated
using the actions feature flag
- **Reduced Executions Overhead**: Executions are managed similar to
settings according to the new API design: an empty list of targets
basically makes an execution a Noop. So a single method, SetExecution is
enough to cover all use cases. Noop executions are not returned in
future search requests.
- **Compatibility**: The executions created with previous v3alpha APIs
are still available to be managed with the new executions API.

# Additional Changes

- Removed integration tests which test executions but rely on readable
targets. They are added again with #8169

# Additional Context

Closes #8168
This commit is contained in:
Elio Bischof
2024-07-31 14:42:12 +02:00
committed by GitHub
parent a1d24353db
commit cc3ec1e2a7
50 changed files with 2822 additions and 5570 deletions

View File

@@ -0,0 +1,137 @@
package action
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
settings_object "github.com/zitadel/zitadel/internal/api/grpc/settings/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
reqTargets := req.GetExecution().GetTargets()
targets := make([]*execution.Target, len(reqTargets))
for i, target := range reqTargets {
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: targets,
}
owner := &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: authz.GetInstance(ctx).InstanceID(),
}
var err error
var details *domain.ObjectDetails
switch t := req.GetCondition().GetConditionType().(type) {
case *action.Condition_Request:
cond := executionConditionFromRequest(t.Request)
details, err = s.command.SetExecutionRequest(ctx, cond, set, owner.Id)
case *action.Condition_Response:
cond := executionConditionFromResponse(t.Response)
details, err = s.command.SetExecutionResponse(ctx, cond, set, owner.Id)
case *action.Condition_Event:
cond := executionConditionFromEvent(t.Event)
details, err = s.command.SetExecutionEvent(ctx, cond, set, owner.Id)
case *action.Condition_Function:
details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, owner.Id)
default:
err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid")
}
if err != nil {
return nil, err
}
return &action.SetExecutionResponse{
Details: settings_object.DomainToDetailsPb(details, owner),
}, 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
default:
return "", zerrors.ThrowInvalidArgument(nil, "ACTION-9BBob", "Errors.Execution.ConditionInvalid")
}
}
func (s *Server) ListExecutionFunctions(_ context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) {
return &action.ListExecutionFunctionsResponse{
Functions: s.ListActionFunctions(),
}, nil
}
func (s *Server) ListExecutionMethods(_ context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) {
return &action.ListExecutionMethodsResponse{
Methods: s.ListGRPCMethods(),
}, nil
}
func (s *Server) ListExecutionServices(_ context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) {
return &action.ListExecutionServicesResponse{
Services: s.ListGRPCServices(),
}, 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(),
}
}

View File

@@ -0,0 +1,805 @@
//go:build integration
package action_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
settings_object "github.com/zitadel/zitadel/pkg/grpc/settings/object/v3alpha"
)
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, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "service, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "NotExistingService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "service, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "all, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// 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, "", "https://notexisting", domain.TargetTypeWebhook, false)
executionCond := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_All{
All: true,
},
},
},
}
Tester.SetExecution(CTX, t,
executionCond,
executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
)
circularExecutionService := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
}
Tester.SetExecution(CTX, t,
circularExecutionService,
executionTargetsSingleInclude(executionCond),
)
circularExecutionMethod := &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
}
Tester.SetExecution(CTX, t,
circularExecutionMethod,
executionTargetsSingleInclude(circularExecutionService),
)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "method, circular error",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: circularExecutionService,
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(circularExecutionMethod),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "service, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleInclude(executionCond),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Response(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.NotExistingService/List",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "method, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/ListSessions",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "service, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "NotExistingService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "service, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_Service{
Service: "zitadel.session.v2beta.SessionService",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "all, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Event(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
/*
//TODO event existing check
{
name: "event, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "event, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Event{
Event: "xxx",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
/*
// TODO:
{
name: "group, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Targets: []string{targetResp.GetId()},
},
wantErr: true,
},
*/
{
name: "group, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_Group{
Group: "xxx",
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
{
name: "all, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Event{
Event: &action.EventExecution{
Condition: &action.EventExecution_All{
All: true,
},
},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}
func TestServer_SetExecution_Function(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.SetExecutionRequest
want *action.SetExecutionResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{
Condition: &action.ResponseExecution_All{All: true},
},
},
},
},
wantErr: true,
},
{
name: "no condition, error",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Response{
Response: &action.ResponseExecution{},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "function, not existing",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "xxx"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
wantErr: true,
},
{
name: "function, ok",
ctx: CTX,
req: &action.SetExecutionRequest{
Condition: &action.Condition{
ConditionType: &action.Condition_Function{
Function: &action.FunctionExecution{Name: "Action.Flow.Type.ExternalAuthentication.Action.TriggerType.PostAuthentication"},
},
},
Execution: &action.Execution{
Targets: executionTargetsSingleTarget(targetResp.GetDetails().GetId()),
},
},
want: &action.SetExecutionResponse{
Details: &settings_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Client.SetExecution(tt.ctx, tt.req)
got, err := Client.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertSettingsDetails(t, tt.want.Details, got.Details)
// cleanup to not impact other requests
Tester.DeleteExecution(tt.ctx, t, tt.req.GetCondition())
})
}
}

View File

@@ -0,0 +1,70 @@
package action
import (
"context"
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var _ action.ZITADELActionsServer = (*Server)(nil)
type Server struct {
action.UnimplementedZITADELActionsServer
command *command.Commands
query *query.Queries
ListActionFunctions func() []string
ListGRPCMethods func() []string
ListGRPCServices func() []string
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
listActionFunctions func() []string,
listGRPCMethods func() []string,
listGRPCServices func() []string,
) *Server {
return &Server{
command: command,
query: query,
ListActionFunctions: listActionFunctions,
ListGRPCMethods: listGRPCMethods,
ListGRPCServices: listGRPCServices,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
action.RegisterZITADELActionsServer(grpcServer, s)
}
func (s *Server) AppName() string {
return action.ZITADELActions_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ZITADELActions_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ZITADELActions_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterZITADELActionsHandler
}
func checkExecutionEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().Actions {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "ACTION-8o6pvqfjhs", "Errors.Action.NotEnabled")
}

View File

@@ -0,0 +1,69 @@
//go:build integration
package action_test
import (
"context"
"os"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
var (
CTX context.Context
Tester *integration.Tester
Client action.ZITADELActionsClient
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
Client = Tester.Client.ActionV3
CTX, _ = Tester.WithAuthorization(ctx, integration.IAMOwner), errCtx
return m.Run()
}())
}
func ensureFeatureEnabled(t *testing.T) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(t, err)
if f.Actions.GetEnabled() {
return
}
_, err = Tester.Client.FeatureV2.SetInstanceFeatures(CTX, &feature.SetInstanceFeaturesRequest{
Actions: gu.Ptr(true),
})
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t,
func(ttt *assert.CollectT) {
f, err := Tester.Client.FeatureV2.GetInstanceFeatures(CTX, &feature.GetInstanceFeaturesRequest{
Inheritance: true,
})
require.NoError(ttt, err)
if f.Actions.GetEnabled() {
return
}
},
retryDuration,
100*time.Millisecond,
"timed out waiting for ensuring instance feature")
}

View File

@@ -0,0 +1,121 @@
package action
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
add := createTargetToCommand(req)
instance := targetOwnerInstance(ctx)
details, err := s.command.AddTarget(ctx, add, instance.Id)
if err != nil {
return nil, err
}
return &action.CreateTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, add.AggregateID),
}, nil
}
func (s *Server) PatchTarget(ctx context.Context, req *action.PatchTargetRequest) (*action.PatchTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
instance := targetOwnerInstance(ctx)
details, err := s.command.ChangeTarget(ctx, patchTargetToCommand(req), instance.Id)
if err != nil {
return nil, err
}
return &action.PatchTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()),
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
instance := targetOwnerInstance(ctx)
details, err := s.command.DeleteTarget(ctx, req.GetId(), instance.Id)
if err != nil {
return nil, err
}
return &action.DeleteTargetResponse{
Details: resource_object.DomainToDetailsPb(details, instance, req.GetId()),
}, nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
reqTarget := req.GetTarget()
var (
targetType domain.TargetType
interruptOnError bool
)
switch t := reqTarget.GetTargetType().(type) {
case *action.Target_RestWebhook:
targetType = domain.TargetTypeWebhook
interruptOnError = t.RestWebhook.InterruptOnError
case *action.Target_RestCall:
targetType = domain.TargetTypeCall
interruptOnError = t.RestCall.InterruptOnError
case *action.Target_RestAsync:
targetType = domain.TargetTypeAsync
}
return &command.AddTarget{
Name: reqTarget.GetName(),
TargetType: targetType,
Endpoint: reqTarget.GetEndpoint(),
Timeout: reqTarget.GetTimeout().AsDuration(),
InterruptOnError: interruptOnError,
}
}
func patchTargetToCommand(req *action.PatchTargetRequest) *command.ChangeTarget {
reqTarget := req.GetTarget()
if reqTarget == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetId(),
},
Name: reqTarget.Name,
Endpoint: reqTarget.Endpoint,
}
if reqTarget.TargetType != nil {
switch t := reqTarget.GetTargetType().(type) {
case *action.PatchTarget_RestWebhook:
target.TargetType = gu.Ptr(domain.TargetTypeWebhook)
target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError)
case *action.PatchTarget_RestCall:
target.TargetType = gu.Ptr(domain.TargetTypeCall)
target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError)
case *action.PatchTarget_RestAsync:
target.TargetType = gu.Ptr(domain.TargetTypeAsync)
target.InterruptOnError = gu.Ptr(false)
}
}
if reqTarget.Timeout != nil {
target.Timeout = gu.Ptr(reqTarget.GetTimeout().AsDuration())
}
return target
}
func targetOwnerInstance(ctx context.Context) *object.Owner {
return &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: authz.GetInstance(ctx).InstanceID(),
}
}

View File

@@ -0,0 +1,447 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/require"
"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"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func TestServer_CreateTarget(t *testing.T) {
ensureFeatureEnabled(t)
tests := []struct {
name string
ctx context.Context
req *action.Target
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &action.Target{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: nil,
},
wantErr: true,
},
{
name: "async, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "webhook, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "webhook, interrupt on error, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: false,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "call, interruptOnError, ok",
ctx: CTX,
req: &action.Target{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
Endpoint: "https://example.com",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
func TestServer_PatchTarget(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
req *action.PatchTargetRequest
}
tests := []struct {
name string
prepare func(request *action.PatchTargetRequest) error
args args
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.PatchTargetRequest) error {
request.Id = "notexisting"
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change url, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Endpoint: gu.Ptr("https://example.com/hooks/new"),
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
Timeout: durationpb.New(20 * time.Second),
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type async, ok",
prepare: func(request *action.PatchTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetDetails().GetId()
request.Id = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.PatchTargetRequest{
Target: &action.PatchTarget{
TargetType: &action.PatchTarget_RestAsync{
RestAsync: &action.SetRESTAsync{},
},
},
},
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
// We want to have the same response no matter how often we call the function
Client.PatchTarget(tt.args.ctx, tt.args.req)
got, err := Client.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t, "", "https://example.com", domain.TargetTypeWebhook, false)
tests := []struct {
name string
ctx context.Context
req *action.DeleteTargetRequest
want *resource_object.Details
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: CTX,
req: &action.DeleteTargetRequest{
Id: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: CTX,
req: &action.DeleteTargetRequest{
Id: target.GetDetails().GetId(),
},
want: &resource_object.Details{
ChangeDate: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertResourceDetails(t, tt.want, got.Details)
})
}
}

View File

@@ -0,0 +1,229 @@
package action
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.Target
}
tests := []struct {
name string
args args
want *command.AddTarget
}{
{
name: "nil",
args: args{nil},
want: &command.AddTarget{
Name: "",
Endpoint: "",
Timeout: 0,
InterruptOnError: false,
},
},
{
name: "all fields (webhook)",
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeWebhook,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: false,
},
},
{
name: "all fields (async)",
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_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,
InterruptOnError: false,
},
},
{
name: "all fields (interrupting response)",
args: args{&action.Target{
Name: "target 1",
Endpoint: "https://example.com/hooks/1",
TargetType: &action.Target_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.AddTarget{
Name: "target 1",
TargetType: domain.TargetTypeCall,
Endpoint: "https://example.com/hooks/1",
Timeout: 10 * time.Second,
InterruptOnError: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(&action.CreateTargetRequest{Target: tt.args.req})
assert.Equal(t, tt.want, got)
})
}
}
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.PatchTarget
}
tests := []struct {
name string
args args
want *command.ChangeTarget
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields nil",
args: args{&action.PatchTarget{
Name: nil,
TargetType: nil,
Timeout: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
Endpoint: nil,
Timeout: nil,
InterruptOnError: nil,
},
},
{
name: "all fields empty",
args: args{&action.PatchTarget{
Name: gu.Ptr(""),
TargetType: nil,
Timeout: durationpb.New(0),
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
Endpoint: nil,
Timeout: gu.Ptr(0 * time.Second),
InterruptOnError: nil,
},
},
{
name: "all fields (webhook)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
InterruptOnError: false,
},
},
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(false),
},
},
{
name: "all fields (webhook interrupt)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_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.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_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),
InterruptOnError: gu.Ptr(false),
},
},
{
name: "all fields (interrupting response)",
args: args{&action.PatchTarget{
Name: gu.Ptr("target 1"),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
TargetType: &action.PatchTarget_RestCall{
RestCall: &action.SetRESTCall{
InterruptOnError: true,
},
},
Timeout: durationpb.New(10 * time.Second),
}},
want: &command.ChangeTarget{
Name: gu.Ptr("target 1"),
TargetType: gu.Ptr(domain.TargetTypeCall),
Endpoint: gu.Ptr("https://example.com/hooks/1"),
Timeout: gu.Ptr(10 * time.Second),
InterruptOnError: gu.Ptr(true),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := patchTargetToCommand(&action.PatchTargetRequest{Target: tt.args.req})
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,21 @@
package object
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails, owner *object.Owner, id string) *resources_object.Details {
details := &resources_object.Details{
Id: id,
Sequence: objectDetail.Sequence,
Owner: owner,
}
if !objectDetail.EventDate.IsZero() {
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
}
return details
}