mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:07:31 +00:00
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:
@@ -1,8 +1,8 @@
|
||||
package execution
|
||||
package execution_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -12,37 +12,11 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/execution"
|
||||
)
|
||||
|
||||
var _ Target = &mockTarget{}
|
||||
|
||||
type mockTarget struct {
|
||||
InstanceID string
|
||||
ExecutionID string
|
||||
TargetID string
|
||||
TargetType domain.TargetType
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
}
|
||||
|
||||
func (e *mockTarget) GetTargetID() string {
|
||||
return e.TargetID
|
||||
}
|
||||
func (e *mockTarget) IsInterruptOnError() bool {
|
||||
return e.InterruptOnError
|
||||
}
|
||||
func (e *mockTarget) GetEndpoint() string {
|
||||
return e.Endpoint
|
||||
}
|
||||
func (e *mockTarget) GetTargetType() domain.TargetType {
|
||||
return e.TargetType
|
||||
}
|
||||
func (e *mockTarget) GetTimeout() time.Duration {
|
||||
return e.Timeout
|
||||
}
|
||||
|
||||
func Test_Call(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -110,12 +84,14 @@ func Test_Call(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := testServerCall(t,
|
||||
tt.args.method,
|
||||
tt.args.body,
|
||||
tt.args.sleep,
|
||||
tt.args.statusCode,
|
||||
tt.args.respBody,
|
||||
respBody, err := testServer(t,
|
||||
&callTestServer{
|
||||
method: tt.args.method,
|
||||
expectBody: tt.args.body,
|
||||
timeout: tt.args.sleep,
|
||||
statusCode: tt.args.statusCode,
|
||||
respondBody: tt.args.respBody,
|
||||
},
|
||||
testCall(tt.args.ctx, tt.args.timeout, tt.args.body),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
@@ -129,98 +105,12 @@ func Test_Call(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
|
||||
return func(url string) ([]byte, error) {
|
||||
return call(ctx, url, timeout, body)
|
||||
}
|
||||
}
|
||||
|
||||
func testCallTarget(ctx context.Context,
|
||||
target *mockTarget,
|
||||
info ContextInfoRequest,
|
||||
) func(string) ([]byte, error) {
|
||||
return func(url string) (r []byte, err error) {
|
||||
target.Endpoint = url
|
||||
return CallTarget(ctx, target, info)
|
||||
}
|
||||
}
|
||||
|
||||
func testServerCall(
|
||||
t *testing.T,
|
||||
method string,
|
||||
body []byte,
|
||||
timeout time.Duration,
|
||||
statusCode int,
|
||||
respBody []byte,
|
||||
call func(string) ([]byte, error),
|
||||
) ([]byte, error) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
checkRequest(t, r, method, body)
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
http.Error(w, "error", statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(timeout)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := io.WriteString(w, string(respBody)); err != nil {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer server.Close()
|
||||
|
||||
return call(server.URL)
|
||||
}
|
||||
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
|
||||
sentBody, err := io.ReadAll(sent.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, sentBody)
|
||||
require.Equal(t, method, sent.Method)
|
||||
}
|
||||
|
||||
var _ ContextInfoRequest = &mockContextInfoRequest{}
|
||||
|
||||
type request struct {
|
||||
Request string `json:"request"`
|
||||
}
|
||||
|
||||
type mockContextInfoRequest struct {
|
||||
Request *request `json:"request"`
|
||||
}
|
||||
|
||||
func newMockContextInfoRequest(s string) *mockContextInfoRequest {
|
||||
return &mockContextInfoRequest{&request{s}}
|
||||
}
|
||||
|
||||
func (c *mockContextInfoRequest) GetHTTPRequestBody() []byte {
|
||||
data, _ := json.Marshal(c)
|
||||
return data
|
||||
}
|
||||
|
||||
func (c *mockContextInfoRequest) GetContent() []byte {
|
||||
data, _ := json.Marshal(c.Request)
|
||||
return data
|
||||
}
|
||||
|
||||
func Test_CallTarget(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
info *middleware.ContextInfoRequest
|
||||
server *callTestServer
|
||||
target *mockTarget
|
||||
sleep time.Duration
|
||||
|
||||
info ContextInfoRequest
|
||||
|
||||
method string
|
||||
body []byte
|
||||
|
||||
respBody []byte
|
||||
statusCode int
|
||||
}
|
||||
type res struct {
|
||||
body []byte
|
||||
@@ -234,16 +124,18 @@ func Test_CallTarget(t *testing.T) {
|
||||
{
|
||||
"unknown targettype, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
timeout: time.Second,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: 4,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
@@ -252,17 +144,19 @@ func Test_CallTarget(t *testing.T) {
|
||||
{
|
||||
"webhook, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
@@ -271,17 +165,19 @@ func Test_CallTarget(t *testing.T) {
|
||||
{
|
||||
"webhook, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeWebhook,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
res{
|
||||
body: nil,
|
||||
@@ -290,17 +186,19 @@ func Test_CallTarget(t *testing.T) {
|
||||
{
|
||||
"request response, error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
@@ -309,17 +207,19 @@ func Test_CallTarget(t *testing.T) {
|
||||
{
|
||||
"request response, ok",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
sleep: time.Second,
|
||||
method: http.MethodPost,
|
||||
info: newMockContextInfoRequest("content1"),
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
server: &callTestServer{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respondBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
target: &mockTarget{
|
||||
TargetType: domain.TargetTypeCall,
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
body: []byte("{\"request\":{\"request\":\"content1\"}}"),
|
||||
respBody: []byte("{\"request\":\"content2\"}"),
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
res{
|
||||
body: []byte("{\"request\":\"content2\"}"),
|
||||
@@ -328,14 +228,7 @@ func Test_CallTarget(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := testServerCall(t,
|
||||
tt.args.method,
|
||||
tt.args.body,
|
||||
tt.args.sleep,
|
||||
tt.args.statusCode,
|
||||
tt.args.respBody,
|
||||
testCallTarget(tt.args.ctx, tt.args.target, tt.args.info),
|
||||
)
|
||||
respBody, err := testServer(t, tt.args.server, testCallTarget(tt.args.ctx, tt.args.info, tt.args.target))
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -345,3 +238,278 @@ func Test_CallTarget(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CallTargets(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
info *middleware.ContextInfoRequest
|
||||
servers []*callTestServer
|
||||
targets []*mockTarget
|
||||
}
|
||||
type res struct {
|
||||
ret interface{}
|
||||
wantErr bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"interrupt on status",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}},
|
||||
targets: []*mockTarget{
|
||||
{InterruptOnError: false},
|
||||
{InterruptOnError: true},
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"continue on status",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusInternalServerError,
|
||||
}},
|
||||
targets: []*mockTarget{
|
||||
{InterruptOnError: false},
|
||||
{InterruptOnError: false},
|
||||
},
|
||||
},
|
||||
res{
|
||||
ret: requestContextInfo1.GetContent(),
|
||||
},
|
||||
},
|
||||
{
|
||||
"interrupt on json error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusOK,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: []byte("just a string, not json"),
|
||||
statusCode: http.StatusOK,
|
||||
}},
|
||||
targets: []*mockTarget{
|
||||
{InterruptOnError: false},
|
||||
{InterruptOnError: true},
|
||||
},
|
||||
},
|
||||
res{
|
||||
wantErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"continue on json error",
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
info: requestContextInfo1,
|
||||
servers: []*callTestServer{{
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: requestContextInfoBody2,
|
||||
statusCode: http.StatusOK,
|
||||
}, {
|
||||
timeout: time.Second,
|
||||
method: http.MethodPost,
|
||||
expectBody: requestContextInfoBody1,
|
||||
respondBody: []byte("just a string, not json"),
|
||||
statusCode: http.StatusOK,
|
||||
}},
|
||||
targets: []*mockTarget{
|
||||
{InterruptOnError: false},
|
||||
{InterruptOnError: false},
|
||||
}},
|
||||
res{
|
||||
ret: requestContextInfo1.GetContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
respBody, err := testServers(t,
|
||||
tt.args.servers,
|
||||
testCallTargets(tt.args.ctx, tt.args.info, tt.args.targets),
|
||||
)
|
||||
if tt.res.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
fmt.Println(respBody)
|
||||
assert.Equal(t, tt.res.ret, respBody)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var _ execution.Target = &mockTarget{}
|
||||
|
||||
type mockTarget struct {
|
||||
InstanceID string
|
||||
ExecutionID string
|
||||
TargetID string
|
||||
TargetType domain.TargetType
|
||||
Endpoint string
|
||||
Timeout time.Duration
|
||||
InterruptOnError bool
|
||||
}
|
||||
|
||||
func (e *mockTarget) GetTargetID() string {
|
||||
return e.TargetID
|
||||
}
|
||||
func (e *mockTarget) IsInterruptOnError() bool {
|
||||
return e.InterruptOnError
|
||||
}
|
||||
func (e *mockTarget) GetEndpoint() string {
|
||||
return e.Endpoint
|
||||
}
|
||||
func (e *mockTarget) GetTargetType() domain.TargetType {
|
||||
return e.TargetType
|
||||
}
|
||||
func (e *mockTarget) GetTimeout() time.Duration {
|
||||
return e.Timeout
|
||||
}
|
||||
|
||||
type callTestServer struct {
|
||||
method string
|
||||
expectBody []byte
|
||||
timeout time.Duration
|
||||
statusCode int
|
||||
respondBody []byte
|
||||
}
|
||||
|
||||
func testServers(
|
||||
t *testing.T,
|
||||
c []*callTestServer,
|
||||
call func([]string) (interface{}, error),
|
||||
) (interface{}, error) {
|
||||
urls := make([]string, len(c))
|
||||
for i := range c {
|
||||
url, close := listen(t, c[i])
|
||||
defer close()
|
||||
urls[i] = url
|
||||
}
|
||||
return call(urls)
|
||||
}
|
||||
|
||||
func testServer(
|
||||
t *testing.T,
|
||||
c *callTestServer,
|
||||
call func(string) ([]byte, error),
|
||||
) ([]byte, error) {
|
||||
url, close := listen(t, c)
|
||||
defer close()
|
||||
return call(url)
|
||||
}
|
||||
|
||||
func listen(
|
||||
t *testing.T,
|
||||
c *callTestServer,
|
||||
) (url string, close func()) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
checkRequest(t, r, c.method, c.expectBody)
|
||||
|
||||
if c.statusCode != http.StatusOK {
|
||||
http.Error(w, "error", c.statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(c.timeout)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := io.WriteString(w, string(c.respondBody)); err != nil {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(handler))
|
||||
return server.URL, server.Close
|
||||
}
|
||||
|
||||
func checkRequest(t *testing.T, sent *http.Request, method string, expectedBody []byte) {
|
||||
sentBody, err := io.ReadAll(sent.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBody, sentBody)
|
||||
require.Equal(t, method, sent.Method)
|
||||
}
|
||||
|
||||
func testCall(ctx context.Context, timeout time.Duration, body []byte) func(string) ([]byte, error) {
|
||||
return func(url string) ([]byte, error) {
|
||||
return execution.Call(ctx, url, timeout, body)
|
||||
}
|
||||
}
|
||||
|
||||
func testCallTarget(ctx context.Context,
|
||||
info *middleware.ContextInfoRequest,
|
||||
target *mockTarget,
|
||||
) func(string) ([]byte, error) {
|
||||
return func(url string) (r []byte, err error) {
|
||||
target.Endpoint = url
|
||||
return execution.CallTarget(ctx, target, info)
|
||||
}
|
||||
}
|
||||
|
||||
func testCallTargets(ctx context.Context,
|
||||
info *middleware.ContextInfoRequest,
|
||||
target []*mockTarget,
|
||||
) func([]string) (interface{}, error) {
|
||||
return func(urls []string) (interface{}, error) {
|
||||
targets := make([]execution.Target, len(target))
|
||||
for i, t := range target {
|
||||
t.Endpoint = urls[i]
|
||||
targets[i] = t
|
||||
}
|
||||
return execution.CallTargets(ctx, targets, info)
|
||||
}
|
||||
}
|
||||
|
||||
var requestContextInfo1 = &middleware.ContextInfoRequest{
|
||||
Request: &request{
|
||||
Request: "content1",
|
||||
},
|
||||
}
|
||||
|
||||
var requestContextInfoBody1 = []byte("{\"request\":{\"request\":\"content1\"}}")
|
||||
var requestContextInfoBody2 = []byte("{\"request\":{\"request\":\"content2\"}}")
|
||||
|
||||
type request struct {
|
||||
Request string `json:"request"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user