fix: add action v2 execution to features (#7597)

* fix: add action v2 execution to features

* fix: add action v2 execution to features

* fix: add action v2 execution to features

* fix: update internal/command/instance_features_model.go

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>

* fix: merge back main

* fix: merge back main

* fix: rename feature and service

* fix: rename feature and service

* fix: review changes

* fix: review changes

---------

Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
Stefan Benz
2024-04-09 19:21:21 +02:00
committed by GitHub
parent 6a51c4b0f5
commit 6dcdef0268
52 changed files with 1069 additions and 737 deletions

View File

@@ -0,0 +1,132 @@
package action
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
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 (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
set := &command.SetExecution{
Targets: req.GetTargets(),
Includes: req.GetIncludes(),
}
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(),
}
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(),
}
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(),
}
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())
if err != nil {
return nil, err
}
}
return &action.SetExecutionResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteExecution(ctx context.Context, req *action.DeleteExecutionRequest) (*action.DeleteExecutionResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
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(),
}
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(),
}
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(),
}
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())
if err != nil {
return nil, err
}
}
return &action.DeleteExecutionResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,298 @@
package action
import (
"context"
"google.golang.org/protobuf/types/known/durationpb"
"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/query"
"github.com/zitadel/zitadel/internal/zerrors"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
)
func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
queries, err := listTargetsRequestToModel(req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchTargets(ctx, queries)
if err != nil {
return nil, err
}
return &action.ListTargetsResponse{
Result: targetsToPb(resp.Targets),
Details: object.ToListDetails(resp.SearchResponse),
}, nil
}
func listTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := targetQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
return &query.TargetSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func targetFieldNameToSortingColumn(field action.TargetFieldName) query.Column {
switch field {
case action.TargetFieldName_FIELD_NAME_UNSPECIFIED:
return query.TargetColumnID
case action.TargetFieldName_FIELD_NAME_ID:
return query.TargetColumnID
case action.TargetFieldName_FIELD_NAME_CREATION_DATE:
return query.TargetColumnCreationDate
case action.TargetFieldName_FIELD_NAME_CHANGE_DATE:
return query.TargetColumnChangeDate
case action.TargetFieldName_FIELD_NAME_NAME:
return query.TargetColumnName
case action.TargetFieldName_FIELD_NAME_TARGET_TYPE:
return query.TargetColumnTargetType
case action.TargetFieldName_FIELD_NAME_URL:
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:
return query.TargetColumnID
}
}
func targetQueriesToQuery(queries []*action.TargetSearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = targetQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func targetQueryToQuery(query *action.TargetSearchQuery) (query.SearchQuery, error) {
switch q := query.Query.(type) {
case *action.TargetSearchQuery_TargetNameQuery:
return targetNameQueryToQuery(q.TargetNameQuery)
case *action.TargetSearchQuery_InTargetIdsQuery:
return targetInTargetIdsQueryToQuery(q.InTargetIdsQuery)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func targetNameQueryToQuery(q *action.TargetNameQuery) (query.SearchQuery, error) {
return query.NewTargetNameSearchQuery(object.TextMethodToQuery(q.Method), q.GetTargetName())
}
func targetInTargetIdsQueryToQuery(q *action.InTargetIDsQuery) (query.SearchQuery, error) {
return query.NewTargetInIDsSearchQuery(q.GetTargetIds())
}
func (s *Server) GetTargetByID(ctx context.Context, req *action.GetTargetByIDRequest) (_ *action.GetTargetByIDResponse, err error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
resp, err := s.query.GetTargetByID(ctx, req.GetTargetId())
if err != nil {
return nil, err
}
return &action.GetTargetByIDResponse{
Target: targetToPb(resp),
}, nil
}
func targetsToPb(targets []*query.Target) []*action.Target {
t := make([]*action.Target, len(targets))
for i, target := range targets {
t[i] = targetToPb(target)
}
return t
}
func targetToPb(t *query.Target) *action.Target {
target := &action.Target{
Details: object.DomainToDetailsPb(&t.ObjectDetails),
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}
}
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}}
default:
target.TargetType = nil
}
return target
}
func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
queries, err := listExecutionsRequestToModel(req)
if err != nil {
return nil, err
}
resp, err := s.query.SearchExecutions(ctx, queries)
if err != nil {
return nil, err
}
return &action.ListExecutionsResponse{
Result: executionsToPb(resp.Executions),
Details: object.ToListDetails(resp.SearchResponse),
}, nil
}
func listExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) {
offset, limit, asc := object.ListQueryToQuery(req.Query)
queries, err := executionQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
return &query.ExecutionSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func executionQueriesToQuery(queries []*action.SearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = executionQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func executionQueryToQuery(searchQuery *action.SearchQuery) (query.SearchQuery, error) {
switch q := searchQuery.Query.(type) {
case *action.SearchQuery_InConditionsQuery:
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())
default:
return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionTypeToQuery(q *action.ExecutionTypeQuery) (query.SearchQuery, error) {
switch q.ExecutionType {
case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
case action.ExecutionType_EXECUTION_TYPE_REQUEST:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest)
case action.ExecutionType_EXECUTION_TYPE_RESPONSE:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse)
case action.ExecutionType_EXECUTION_TYPE_EVENT:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent)
case action.ExecutionType_EXECUTION_TYPE_FUNCTION:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction)
default:
return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified)
}
}
func inConditionsQueryToQuery(q *action.InConditionsQuery) (query.SearchQuery, error) {
values := make([]string, len(q.GetConditions()))
for i, condition := range q.GetConditions() {
id, err := conditionToID(condition)
if err != nil {
return nil, err
}
values[i] = id
}
return query.NewExecutionInIDsSearchQuery(values)
}
func conditionToID(q *action.Condition) (string, error) {
switch t := q.GetConditionType().(type) {
case *action.Condition_Request:
cond := &command.ExecutionAPICondition{
Method: t.Request.GetMethod(),
Service: t.Request.GetService(),
All: t.Request.GetAll(),
}
return cond.ID(domain.ExecutionTypeRequest), nil
case *action.Condition_Response:
cond := &command.ExecutionAPICondition{
Method: t.Response.GetMethod(),
Service: t.Response.GetService(),
All: t.Response.GetAll(),
}
return cond.ID(domain.ExecutionTypeResponse), nil
case *action.Condition_Event:
cond := &command.ExecutionEventCondition{
Event: t.Event.GetEvent(),
Group: t.Event.GetGroup(),
All: t.Event.GetAll(),
}
return cond.ID(), nil
case *action.Condition_Function:
return t.Function, nil
default:
return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid")
}
}
func executionsToPb(executions []*query.Execution) []*action.Execution {
e := make([]*action.Execution, len(executions))
for i, execution := range executions {
e[i] = executionToPb(execution)
}
return e
}
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
}
return &action.Execution{
Details: object.DomainToDetailsPb(&e.ObjectDetails),
ExecutionId: e.ID,
Targets: targets,
Includes: includes,
}
}

View File

@@ -0,0 +1,718 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"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 TestServer_GetTargetByID(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
dep func(context.Context, *action.GetTargetByIDRequest, *action.GetTargetByIDResponse) error
req *action.GetTargetByIDRequest
}
tests := []struct {
name string
args args
want *action.GetTargetByIDResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.GetTargetByIDRequest{},
},
wantErr: true,
},
{
name: "not found",
args: args{
ctx: CTX,
req: &action.GetTargetByIDRequest{TargetId: "notexisting"},
},
wantErr: true,
},
{
name: "get, 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, 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(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
{
name: "get, async, 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, true, 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(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_IsAsync{IsAsync: true},
},
},
},
{
name: "get, 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)
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(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, getErr := Client.GetTargetByID(tt.args.ctx, tt.args.req)
if tt.wantErr {
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())
assert.Equal(t, tt.want.Target, got.Target)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func TestServer_ListTargets(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) error
req *action.ListTargetsRequest
}
tests := []struct {
name string
args args
want *action.ListTargetsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.ListTargetsRequest{},
},
wantErr: true,
},
{
name: "list, not found",
args: args{
ctx: CTX,
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{
{Query: &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{"notfound"},
},
},
},
},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 0,
},
Result: []*action.Target{},
},
},
{
name: "list single id",
args: args{
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)
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.Result[0].Details.ChangeDate = resp.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp.GetDetails().GetSequence()
response.Result[0].TargetId = resp.GetId()
response.Result[0].Name = name
return nil
},
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{{}},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Target{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
}, {
name: "list single name",
args: args{
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)
request.Queries[0].Query = &action.TargetSearchQuery_TargetNameQuery{
TargetNameQuery: &action.TargetNameQuery{
TargetName: name,
},
}
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].TargetId = resp.GetId()
response.Result[0].Name = name
return nil
},
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{{}},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Target{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
},
},
},
},
{
name: "list multiple id",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) error {
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)
request.Queries[0].Query = &action.TargetSearchQuery_InTargetIdsQuery{
InTargetIdsQuery: &action.InTargetIDsQuery{
TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()},
},
}
response.Details.Timestamp = resp3.GetDetails().GetChangeDate()
response.Details.ProcessedSequence = resp3.GetDetails().GetSequence()
response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence()
response.Result[0].TargetId = resp1.GetId()
response.Result[0].Name = name1
response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence()
response.Result[1].TargetId = resp2.GetId()
response.Result[1].Name = name2
response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate()
response.Result[2].Details.Sequence = resp3.GetDetails().GetSequence()
response.Result[2].TargetId = resp3.GetId()
response.Result[2].Name = name3
return nil
},
req: &action.ListTargetsRequest{
Queries: []*action.TargetSearchQuery{{}},
},
},
want: &action.ListTargetsResponse{
Details: &object.ListDetails{
TotalResult: 3,
},
Result: []*action.Target{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
},
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_IsAsync{IsAsync: true},
},
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
TargetType: &action.Target_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: durationpb.New(10 * time.Second),
ExecutionType: &action.Target_InterruptOnError{InterruptOnError: true},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Client.ListTargets(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, listErr, "Error: "+listErr.Error())
} else {
assert.NoError(ttt, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}
func TestServer_ListExecutions_Request(t *testing.T) {
ensureFeatureEnabled(t)
targetResp := Tester.CreateTarget(CTX, t)
type args struct {
ctx context.Context
dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) error
req *action.ListExecutionsRequest
}
tests := []struct {
name string
args args
want *action.ListExecutionsResponse
wantErr bool
}{
{
name: "missing permission",
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.ListExecutionsRequest{},
},
wantErr: true,
},
{
name: "list 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{})
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()
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",
},
},
},
},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.session.v2beta.SessionService/GetSession",
Targets: []string{targetResp.GetId()},
},
},
},
},
{
name: "list single target",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
target := Tester.CreateTarget(ctx, t)
// add target as query to the request
request.Queries[0] = &action.SearchQuery{
Query: &action.SearchQuery_TargetQuery{
TargetQuery: &action.TargetQuery{
TargetId: target.GetId(),
},
},
}
resp := Tester.SetExecution(ctx, t, &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/UpdateAction",
},
},
},
}, []string{target.GetId()}, []string{})
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()
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.management.v1.ManagementService/UpdateAction",
Targets: []string{""},
},
},
},
}, {
name: "list single include",
args: args{
ctx: CTX,
dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) error {
Tester.SetExecution(ctx, t, &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/GetAction",
},
},
},
}, []string{targetResp.GetId()}, []string{})
resp2 := Tester.SetExecution(ctx, t, &action.Condition{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.management.v1.ManagementService/ListActions",
},
},
},
}, []string{}, []string{"request./zitadel.management.v1.ManagementService/GetAction"})
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()
return nil
},
req: &action.ListExecutionsRequest{
Queries: []*action.SearchQuery{{
Query: &action.SearchQuery_IncludeQuery{
IncludeQuery: &action.IncludeQuery{Include: "request./zitadel.management.v1.ManagementService/GetAction"},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 1,
},
Result: []*action.Execution{
{
Details: &object.Details{
ResourceOwner: Tester.Instance.InstanceID(),
},
ExecutionId: "request./zitadel.management.v1.ManagementService/ListActions",
Includes: []string{"request./zitadel.management.v1.ManagementService/GetAction"},
},
},
},
},
{
name: "list multiple conditions",
args: args{
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{})
response.Result[0].Details.ChangeDate = resp1.GetDetails().GetChangeDate()
response.Result[0].Details.Sequence = resp1.GetDetails().GetSequence()
resp2 := Tester.SetExecution(ctx, t, request.Queries[0].GetInConditionsQuery().GetConditions()[1], []string{targetResp.GetId()}, []string{})
response.Result[1].Details.ChangeDate = resp2.GetDetails().GetChangeDate()
response.Result[1].Details.Sequence = resp2.GetDetails().GetSequence()
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()
response.Result[2].Details.ChangeDate = resp3.GetDetails().GetChangeDate()
response.Result[2].Details.Sequence = resp3.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_Method{
Method: "/zitadel.session.v2beta.SessionService/CreateSession",
},
},
},
},
{
ConditionType: &action.Condition_Request{
Request: &action.RequestExecution{
Condition: &action.RequestExecution_Method{
Method: "/zitadel.session.v2beta.SessionService/SetSession",
},
},
},
},
},
},
},
}},
},
},
want: &action.ListExecutionsResponse{
Details: &object.ListDetails{
TotalResult: 3,
},
Result: []*action.Execution{
{
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()},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.dep != nil {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
retryDuration := 5 * time.Second
if ctxDeadline, ok := CTX.Deadline(); ok {
retryDuration = time.Until(ctxDeadline)
}
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())
} else {
assert.NoError(ttt, listErr)
}
if listErr != nil {
return
}
// always first check length, otherwise its failed anyway
assert.Len(ttt, got.Result, len(tt.want.Result))
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
}
integration.AssertListDetails(t, tt.want, got)
}, retryDuration, time.Millisecond*100, "timeout waiting for expected execution result")
})
}
}

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/action/v3alpha"
)
var _ action.ActionServiceServer = (*Server)(nil)
type Server struct {
action.UnimplementedActionServiceServer
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.RegisterActionServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return action.ActionService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return action.ActionService_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return action.RegisterActionServiceHandler
}
func checkExecutionEnabled(ctx context.Context) error {
if authz.GetInstance(ctx).Features().Actions {
return nil
}
return zerrors.ThrowPreconditionFailed(nil, "SCHEMA-141bwx3lef", "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"
action "github.com/zitadel/zitadel/pkg/grpc/action/v3alpha"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
)
var (
CTX context.Context
Tester *integration.Tester
Client action.ActionServiceClient
)
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,107 @@
package action
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
"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/eventstore/v1/models"
action "github.com/zitadel/zitadel/pkg/grpc/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)
details, err := s.command.AddTarget(ctx, add, authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &action.CreateTargetResponse{
Id: add.AggregateID,
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.ChangeTarget(ctx, updateTargetToCommand(req), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &action.UpdateTargetResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) {
if err := checkExecutionEnabled(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteTarget(ctx, req.GetTargetId(), authz.GetInstance(ctx).InstanceID())
if err != nil {
return nil, err
}
return &action.DeleteTargetResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget {
var targetType domain.TargetType
var url string
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()
}
return &command.AddTarget{
Name: req.GetName(),
TargetType: targetType,
URL: url,
Timeout: req.GetTimeout().AsDuration(),
Async: req.GetIsAsync(),
InterruptOnError: req.GetInterruptOnError(),
}
}
func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget {
if req == nil {
return nil
}
target := &command.ChangeTarget{
ObjectRoot: models.ObjectRoot{
AggregateID: req.GetTargetId(),
},
Name: req.Name,
}
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.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
}

View File

@@ -0,0 +1,393 @@
//go:build integration
package action_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"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 TestServer_CreateTarget(t *testing.T) {
ensureFeatureEnabled(t)
tests := []struct {
name string
ctx context.Context
req *action.CreateTargetRequest
want *action.CreateTargetResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
},
wantErr: true,
},
{
name: "empty name",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: "",
},
wantErr: true,
},
{
name: "empty type",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: nil,
},
wantErr: true,
},
{
name: "empty webhook url",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{},
},
},
wantErr: true,
},
{
name: "empty request response url",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{},
},
},
wantErr: true,
},
{
name: "empty timeout",
ctx: CTX,
req: &action.CreateTargetRequest{
Name: fmt.Sprint(time.Now().UnixNano() + 1),
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com",
},
},
Timeout: nil,
ExecutionType: nil,
},
wantErr: true,
},
{
name: "empty execution type, ok",
ctx: CTX,
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),
ExecutionType: nil,
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "async execution, ok",
ctx: CTX,
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),
ExecutionType: &action.CreateTargetRequest_IsAsync{
IsAsync: true,
},
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "interrupt on error execution, ok",
ctx: CTX,
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),
ExecutionType: &action.CreateTargetRequest_InterruptOnError{
InterruptOnError: true,
},
},
want: &action.CreateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreateTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
assert.NotEmpty(t, got.GetId())
})
}
}
func TestServer_UpdateTarget(t *testing.T) {
ensureFeatureEnabled(t)
type args struct {
ctx context.Context
req *action.UpdateTargetRequest
}
tests := []struct {
name string
prepare func(request *action.UpdateTargetRequest) error
args args
want *action.UpdateTargetResponse
wantErr bool
}{
{
name: "missing permission",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "not existing",
prepare: func(request *action.UpdateTargetRequest) error {
request.TargetId = "notexisting"
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
wantErr: true,
},
{
name: "change name, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Name: gu.Ptr(fmt.Sprint(time.Now().UnixNano() + 1)),
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change type, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
TargetType: &action.UpdateTargetRequest_RestRequestResponse{
RestRequestResponse: &action.SetRESTRequestResponse{
Url: "https://example.com",
},
},
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change url, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).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",
},
},
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change timeout, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
Timeout: durationpb.New(20 * time.Second),
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Instance.InstanceID(),
},
},
},
{
name: "change execution type, ok",
prepare: func(request *action.UpdateTargetRequest) error {
targetID := Tester.CreateTarget(CTX, t).GetId()
request.TargetId = targetID
return nil
},
args: args{
ctx: CTX,
req: &action.UpdateTargetRequest{
ExecutionType: &action.UpdateTargetRequest_IsAsync{
IsAsync: true,
},
},
},
want: &action.UpdateTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: 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)
got, err := Client.UpdateTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_DeleteTarget(t *testing.T) {
ensureFeatureEnabled(t)
target := Tester.CreateTarget(CTX, t)
tests := []struct {
name string
ctx context.Context
req *action.DeleteTargetRequest
want *action.DeleteTargetResponse
wantErr bool
}{
{
name: "missing permission",
ctx: Tester.WithAuthorization(context.Background(), integration.OrgOwner),
req: &action.DeleteTargetRequest{
TargetId: target.GetId(),
},
wantErr: true,
},
{
name: "empty id",
ctx: CTX,
req: &action.DeleteTargetRequest{
TargetId: "",
},
wantErr: true,
},
{
name: "delete target",
ctx: CTX,
req: &action.DeleteTargetRequest{
TargetId: target.GetId(),
},
want: &action.DeleteTargetResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: 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.AssertDetails(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,192 @@
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/action/v3alpha"
)
func Test_createTargetToCommand(t *testing.T) {
type args struct {
req *action.CreateTargetRequest
}
tests := []struct {
name string
args args
want *command.AddTarget
}{
{
name: "nil",
args: args{nil},
want: &command.AddTarget{
Name: "",
URL: "",
Timeout: 0,
Async: false,
InterruptOnError: false,
},
},
{
name: "all fields (async webhook)",
args: args{&action.CreateTargetRequest{
Name: "target 1",
TargetType: &action.CreateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/1",
},
},
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",
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",
},
},
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",
Timeout: 10 * time.Second,
Async: false,
InterruptOnError: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_updateTargetToCommand(t *testing.T) {
type args struct {
req *action.UpdateTargetRequest
}
tests := []struct {
name string
args args
want *command.ChangeTarget
}{
{
name: "nil",
args: args{nil},
want: nil,
},
{
name: "all fields nil",
args: args{&action.UpdateTargetRequest{
Name: nil,
TargetType: nil,
Timeout: nil,
ExecutionType: nil,
}},
want: &command.ChangeTarget{
Name: nil,
TargetType: nil,
URL: 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,
}},
want: &command.ChangeTarget{
Name: gu.Ptr(""),
TargetType: nil,
URL: nil,
Timeout: gu.Ptr(0 * time.Second),
Async: nil,
InterruptOnError: nil,
},
},
{
name: "all fields (async webhook)",
args: args{&action.UpdateTargetRequest{
Name: gu.Ptr("target 1"),
TargetType: &action.UpdateTargetRequest_RestWebhook{
RestWebhook: &action.SetRESTWebhook{
Url: "https://example.com/hooks/1",
},
},
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"),
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",
},
},
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"),
Timeout: gu.Ptr(10 * time.Second),
Async: gu.Ptr(false),
InterruptOnError: gu.Ptr(true),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := updateTargetToCommand(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}