zitadel/internal/execution/execution_test.go
Elio Bischof cc3ec1e2a7
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
2024-07-31 14:42:12 +02:00

516 lines
12 KiB
Go

package execution_test
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"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"
)
func Test_Call(t *testing.T) {
type args struct {
ctx context.Context
timeout time.Duration
sleep time.Duration
method string
body []byte
respBody []byte
statusCode int
}
type res struct {
body []byte
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"not ok status",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusBadRequest,
},
res{
wantErr: true,
},
},
{
"timeout",
args{
ctx: context.Background(),
timeout: time.Second,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
},
res{
wantErr: true,
},
},
{
"ok",
args{
ctx: context.Background(),
timeout: time.Minute,
sleep: time.Second,
method: http.MethodPost,
body: []byte("{\"request\": \"values\"}"),
respBody: []byte("{\"response\": \"values\"}"),
statusCode: http.StatusOK,
},
res{
body: []byte("{\"response\": \"values\"}"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
respBody, err := 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 {
assert.Error(t, err)
assert.Nil(t, respBody)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.res.body, respBody)
}
})
}
}
func Test_CallTarget(t *testing.T) {
type args struct {
ctx context.Context
info *middleware.ContextInfoRequest
server *callTestServer
target *mockTarget
}
type res struct {
body []byte
wantErr bool
}
tests := []struct {
name string
args args
res res
}{
{
"unknown targettype, error",
args{
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,
},
},
res{
wantErr: true,
},
},
{
"webhook, error",
args{
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,
},
},
res{
wantErr: true,
},
},
{
"webhook, ok",
args{
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,
},
},
res{
body: nil,
},
},
{
"request response, error",
args{
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,
},
},
res{
wantErr: true,
},
},
{
"request response, ok",
args{
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,
},
},
res{
body: []byte("{\"request\":\"content2\"}"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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 {
assert.NoError(t, err)
}
assert.Equal(t, tt.res.body, respBody)
})
}
}
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"`
}