kratos/selfservice/hook/web_hook_integration_test.go

1375 lines
45 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hook_test
import (
"context"
"crypto/tls"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"slices"
"strconv"
"sync"
"testing"
"time"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/internal"
"github.com/ory/kratos/request"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/flow/verification"
"github.com/ory/kratos/selfservice/hook"
"github.com/ory/kratos/session"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/kratos/x/events"
"github.com/ory/x/jsonnetsecure"
"github.com/ory/x/logrusx"
"github.com/ory/x/otelx"
"github.com/ory/x/otelx/semconv"
"github.com/ory/x/pointerx"
"github.com/ory/x/snapshotx"
)
var transientPayload = json.RawMessage(`{
"stuff": {
"name": "fubar",
"numbers": [42, 12345, 3.1415]
}
}`)
func TestWebHooks(t *testing.T) {
ctx := context.Background()
conf, reg := internal.NewFastRegistryWithMocks(t)
logger := logrusx.New("kratos", "test")
conf.Set(ctx, config.ViperKeyWebhookHeaderAllowlist, []string{
"Accept",
"Accept-Encoding",
"Accept-Language",
"Content-Length",
"Content-Type",
"Origin",
"Priority",
"Referer",
"Sec-Ch-Ua",
"Sec-Ch-Ua-Mobile",
"Sec-Ch-Ua-Platform",
"Sec-Fetch-Dest",
"Sec-Fetch-Mode",
"Sec-Fetch-Site",
"Sec-Fetch-User",
"True-Client-Ip",
"User-Agent",
"Valid-Header",
})
whDeps := struct {
x.SimpleLoggerWithClient
*jsonnetsecure.TestProvider
config.Provider
}{
x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(ctx), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
jsonnetsecure.NewTestProvider(t),
reg,
}
type WebHookRequest struct {
Body string
Headers http.Header
Method string
}
webHookEndPoint := func(whr *WebHookRequest) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
whr.Body = string(body)
whr.Headers = r.Header
whr.Method = r.Method
}
}
webHookHttpCodeEndPoint := func(code int) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(code)
}
}
webHookHttpCodeWithBodyEndPoint := func(t *testing.T, code int, body []byte) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(code)
_, err := w.Write(body)
assert.NoError(t, err, "error while returning response from webHookHttpCodeWithBodyEndPoint")
}
}
path := "/web_hook"
newServer := func(f http.HandlerFunc) *httptest.Server {
r := http.NewServeMux()
r.HandleFunc("CONNECT "+path, f)
r.HandleFunc("DELETE "+path, f)
r.HandleFunc("GET "+path, f)
r.HandleFunc("OPTIONS "+path, f)
r.HandleFunc("PATCH "+path, f)
r.HandleFunc("POST "+path, f)
r.HandleFunc("PUT "+path, f)
r.HandleFunc("TRACE "+path, f)
ts := httptest.NewServer(r)
t.Cleanup(ts.Close)
return ts
}
bodyWithFlowOnly := func(req *http.Request, f flow.Flow) string {
body := fmt.Sprintf(`{
"flow_id": "%s",
"method": "%s",
"url": "%s",
"cookies": {
"Some-Cookie-1": "Some-Cookie-Value",
"Some-Cookie-2": "Some-other-Cookie-Value",
"Some-Cookie-3": "Third-Cookie-Value"
}
}`, f.GetID(), req.Method, "http://www.ory.sh/some_end_point")
if len(req.Header) != 0 {
if ua := req.Header.Get("User-Agent"); ua != "" {
body, _ = sjson.Set(body, "headers.User-Agent", []string{ua})
}
}
return body
}
bodyWithFlowAndIdentityAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string {
body := fmt.Sprintf(`{
"flow_id": "%s",
"identity_id": "%s",
"method": "%s",
"url": "%s",
"cookies": {
"Some-Cookie-1": "Some-Cookie-Value",
"Some-Cookie-2": "Some-other-Cookie-Value",
"Some-Cookie-3": "Third-Cookie-Value"
},
"transient_payload": %s
}`, f.GetID(), s.Identity.ID, req.Method, "http://www.ory.sh/some_end_point", string(tp))
if len(req.Header) != 0 {
if ua := req.Header.Get("User-Agent"); ua != "" {
body, _ = sjson.Set(body, "headers.User-Agent", []string{ua})
}
}
return body
}
bodyWithFlowAndIdentityAndSessionAndTransientPayload := func(req *http.Request, f flow.Flow, s *session.Session, tp json.RawMessage) string {
body := fmt.Sprintf(`{
"flow_id": "%s",
"identity_id": "%s",
"session_id": "%s",
"method": "%s",
"url": "%s",
"cookies": {
"Some-Cookie-1": "Some-Cookie-Value",
"Some-Cookie-2": "Some-other-Cookie-Value",
"Some-Cookie-3": "Third-Cookie-Value"
},
"transient_payload": %s
}`, f.GetID(), s.Identity.ID, s.ID, req.Method, "http://www.ory.sh/some_end_point", string(tp))
if len(req.Header) != 0 {
if ua := req.Header.Get("User-Agent"); ua != "" {
body, _ = sjson.Set(body, "headers.User-Agent", []string{ua})
}
}
return body
}
for _, tc := range []struct {
uc string
callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error
expectedBody func(req *http.Request, f flow.Flow, s *session.Session) string
createFlow func() flow.Flow
}{
{
uc: "Pre Login Hook",
createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error {
return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow))
},
expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string {
return bodyWithFlowOnly(req, f)
},
},
{
uc: "Post Login Hook",
createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s)
},
expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string {
return bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload)
},
},
{
uc: "Pre Registration Hook",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error {
return wh.ExecuteRegistrationPreHook(nil, req, f.(*registration.Flow))
},
expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string {
return bodyWithFlowOnly(req, f)
},
},
{
uc: "Post Registration Hook",
createFlow: func() flow.Flow {
return &registration.Flow{
ID: x.NewUUID(),
TransientPayload: transientPayload,
}
},
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s)
},
expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string {
return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload)
},
},
{
uc: "Post Recovery Hook",
createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s)
},
expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string {
return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload)
},
},
{
uc: "Post Verification Hook",
createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity)
},
expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string {
return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload)
},
},
{
uc: "Post Settings Hook",
createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID(), TransientPayload: transientPayload} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity, s)
},
expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string {
return bodyWithFlowAndIdentityAndTransientPayload(req, f, s, transientPayload)
},
},
} {
tc := tc
t.Run("uc="+tc.uc, func(t *testing.T) {
t.Parallel()
for _, auth := range []struct {
uc string
authConfig request.AuthConfig
expectedHeader func(header http.Header)
}{
{
uc: "no auth",
authConfig: request.AuthConfig{},
expectedHeader: func(header http.Header) {},
},
{
uc: "api key in header",
authConfig: request.AuthConfig{Type: "api_key", Config: map[string]any{
"name": "My-Key",
"value": "My-Key-Value",
"in": "header",
}},
expectedHeader: func(header http.Header) {
header.Set("My-Key", "My-Key-Value")
},
},
{
uc: "api key in cookie",
authConfig: request.AuthConfig{Type: "api_key", Config: map[string]any{
"name": "My-Key",
"value": "My-Key-Value",
"in": "cookie",
}},
expectedHeader: func(header http.Header) {
header.Set("Cookie", "My-Key=My-Key-Value")
},
},
{
uc: "basic auth",
authConfig: request.AuthConfig{Type: "basic_auth", Config: map[string]any{
"user": "My-User",
"password": "Super-Secret",
}},
expectedHeader: func(header http.Header) {
header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("My-User:Super-Secret")))
},
},
} {
t.Run("auth="+auth.uc, func(t *testing.T) {
for _, method := range []string{"CONNECT", "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE", "GARBAGE"} {
t.Run("method="+method, func(t *testing.T) {
f := tc.createFlow()
req := (&http.Request{
Host: "www.ory.sh",
Header: map[string][]string{
"Some-Header": {"Some-Value"},
"User-Agent": {"Foo-Bar-Browser"},
"Invalid-Header": {"should be ignored"},
"Valid-Header": {"should not be ignored"},
"Cookie": {"Some-Cookie-1=Some-Cookie-Value; Some-Cookie-2=Some-other-Cookie-Value", "Some-Cookie-3=Third-Cookie-Value"},
},
RequestURI: "/some_end_point",
Method: http.MethodPost,
URL: &url.URL{Path: "/some_end_point"},
}).WithContext(ctx)
cookie, err := req.Cookie("Some-Cookie-1")
require.NoError(t, err)
require.Equal(t, cookie.Name, "Some-Cookie-1")
require.Equal(t, cookie.Value, "Some-Cookie-Value")
cookie, err = req.Cookie("Some-Cookie-2")
require.NoError(t, err)
require.Equal(t, cookie.Name, "Some-Cookie-2")
require.Equal(t, cookie.Value, "Some-other-Cookie-Value")
cookie, err = req.Cookie("Some-Cookie-3")
require.NoError(t, err)
require.Equal(t, cookie.Name, "Some-Cookie-3")
require.Equal(t, cookie.Value, "Third-Cookie-Value")
s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}}
whr := &WebHookRequest{}
ts := newServer(webHookEndPoint(whr))
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: method,
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
Auth: auth.authConfig,
})
err = tc.callWebHook(wh, req, f, s)
if method == "GARBAGE" {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, method, whr.Method)
expectedHeader := http.Header{}
expectedHeader.Set("Content-Type", "application/json")
auth.expectedHeader(expectedHeader)
for k, v := range expectedHeader {
vals := whr.Headers.Values(k)
assert.Equal(t, v, vals)
}
assert.NotZero(t, whr.Headers.Get("Ory-Webhook-Request-ID"))
assert.NotZero(t, whr.Headers.Get("Ory-Webhook-Trigger-ID"))
if method != "TRACE" {
// According to the HTTP spec any request method, but TRACE is allowed to
// have a body. Even this is a really bad practice for some of them, like for
// GET
assert.Zero(t, gjson.Get(whr.Body, "headers.Invalid-Header"))
assert.NotZero(t, gjson.Get(whr.Body, "headers.Valid-Header"))
whr.Body, err = sjson.Delete(whr.Body, "headers.Valid-Header")
assert.NoError(t, err)
assert.JSONEq(t, tc.expectedBody(req, f, s), whr.Body)
} else {
assert.Emptyf(t, whr.Body, "HTTP %s is not allowed to have a body", method)
}
})
}
})
}
})
}
webHookResponse := []byte(
`{
"messages": [{
"instance_ptr": "#/traits/username",
"messages": [{
"id": 1234,
"text": "error message",
"type": "info"
}]
}]
}`,
)
webhookError := schema.NewValidationListError([]*schema.ValidationError{schema.NewHookValidationError("#/traits/username", "a webhook target returned an error", text.Messages{{ID: 1234, Type: "info", Text: "error message"}})})
for _, tc := range []struct {
uc string
callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error
webHookResponse func() (int, []byte)
createFlow func() flow.Flow
expectedError error
}{
{
uc: "Pre Login Hook - no block",
createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error {
return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow))
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Pre Login Hook - block",
createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error {
return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow))
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Post Login Hook - no block",
createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s)
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Post Login Hook - block",
createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteLoginPostHook(nil, req, node.PasswordGroup, f.(*login.Flow), s)
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Pre Registration Hook - no block",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error {
return wh.ExecuteRegistrationPreHook(nil, req, f.(*registration.Flow))
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Pre Registration Hook - block",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error {
return wh.ExecuteRegistrationPreHook(nil, req, f.(*registration.Flow))
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Post Registration Post Persist Hook - no block",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s)
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Post Registration Post Persist Hook - block has no effect",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s)
},
// This would usually error, but post persist does not execute blocking web hooks, so we expect no error.
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: nil,
},
{
uc: "Post Registration Pre Persist Hook - no block",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRegistrationPrePersistHook(nil, req, f.(*registration.Flow), s.Identity)
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Post Registration Pre Persist Hook - block",
createFlow: func() flow.Flow { return &registration.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRegistrationPrePersistHook(nil, req, f.(*registration.Flow), s.Identity)
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Post Recovery Hook - no block",
createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s)
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Post Recovery Hook - block",
createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s)
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Post Verification Hook - no block",
createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity)
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Post Verification Hook - block",
createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity)
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Post Settings Hook - no block",
createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity, s)
},
webHookResponse: func() (int, []byte) {
return http.StatusOK, []byte{}
},
expectedError: nil,
},
{
uc: "Post Settings Hook Pre Persist - block",
createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteSettingsPrePersistHook(nil, req, f.(*settings.Flow), s.Identity)
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: webhookError,
},
{
uc: "Post Settings Hook Post Persist - block has no effect",
createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} },
callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error {
return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity, s)
},
webHookResponse: func() (int, []byte) {
return http.StatusBadRequest, webHookResponse
},
expectedError: nil,
},
} {
tc := tc
t.Run("uc="+tc.uc, func(t *testing.T) {
t.Parallel()
for _, method := range []string{"CONNECT", "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"} {
t.Run("method="+method, func(t *testing.T) {
f := tc.createFlow()
req := &http.Request{
Host: "www.ory.sh",
Header: map[string][]string{
"Some-Header": {"Some-Value"},
"X-Forwarded-Proto": {"https"},
"Cookie": {"Some-Cookie-1=Some-Cookie-Value; Some-Cookie-2=Some-other-Cookie-Value", "Some-Cookie-3=Third-Cookie-Value"},
},
RequestURI: "/some_end_point",
Method: http.MethodPost,
URL: &url.URL{
Path: "some_end_point",
},
}
s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}}
code, res := tc.webHookResponse()
ts := newServer(webHookHttpCodeWithBodyEndPoint(t, code, res))
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: method,
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
CanInterrupt: true,
})
err := tc.callWebHook(wh, req, f, s)
if tc.expectedError == nil {
assert.NoError(t, err)
return
}
var validationError *schema.ValidationListError
var expectedError *schema.ValidationListError
if assert.ErrorAs(t, err, &validationError) && assert.ErrorAs(t, tc.expectedError, &expectedError) {
assert.Equal(t, expectedError, validationError)
}
})
}
})
}
t.Run("update identity fields", func(t *testing.T) {
t.Parallel()
run := func(t *testing.T, id identity.Identity, responseCode int, response []byte) *identity.WithCredentialsAndAdminMetadataInJSON {
f := &registration.Flow{ID: x.NewUUID()}
req := &http.Request{
Host: "www.ory.sh",
Header: map[string][]string{},
RequestURI: "/some_end_point",
Method: http.MethodPost,
URL: &url.URL{Path: "some_end_point"},
}
ts := newServer(webHookHttpCodeWithBodyEndPoint(t, responseCode, response))
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "POST",
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
Response: request.ResponseConfig{
Parse: true,
},
})
in := &id
err := wh.ExecutePostRegistrationPrePersistHook(nil, req, f, in)
require.NoError(t, err)
result := identity.WithCredentialsAndAdminMetadataInJSON(*in)
return &result
}
t.Run("case=update identity fields", func(t *testing.T) {
expected := identity.Identity{
Credentials: map[identity.CredentialsType]identity.Credentials{identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{"test"}, Config: []byte(`{"hashed_password":"$argon2id$v=19$m=65536,t=1,p=1$Z3JlZW5hbmRlcnNlY3JldA$Z3JlZW5hbmRlcnNlY3JldA"}`)}},
SchemaID: "default",
SchemaURL: "file://stub/default.schema.json",
State: identity.StateActive,
Traits: []byte(`{"email":"some@example.org"}`),
VerifiableAddresses: []identity.VerifiableAddress{{
Value: "some@example.org",
Verified: false,
Via: "email",
Status: identity.VerifiableAddressStatusPending,
}},
RecoveryAddresses: []identity.RecoveryAddress{{
Value: "some@example.org",
Via: "email",
}},
MetadataPublic: []byte(`{"public":"data"}`),
MetadataAdmin: []byte(`{"admin":"data"}`),
}
t.Run("case=body is empty", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity is present but empty", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{}}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity has updated traits", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"}}}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity has updated state", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"state":"inactive"}}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity has updated public metadata", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"metadata_public":{"useful":"metadata"}}}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity has updated admin metadata", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"metadata_admin":{"useful":"admin metadata"}}}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity has updated verified addresses", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"},"verifiable_addresses":[{"value":"some@other-example.org","via":"email"}]}}`))
snapshotx.SnapshotT(t, &actual)
})
t.Run("case=identity has updated recovery addresses", func(t *testing.T) {
actual := run(t, expected, http.StatusOK, []byte(`{"identity":{"traits":{"email":"some@other-example.org"},"recovery_addresses":[{"value":"some@other-example.org","via":"email"}]}}`))
snapshotx.SnapshotT(t, &actual)
})
})
})
t.Run("cannot have parse and ignore both set", func(t *testing.T) {
t.Parallel()
ts := newServer(webHookHttpCodeEndPoint(200))
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "GET",
URL: ts.URL + path,
TemplateURI: "./stub/test_body.jsonnet",
Response: request.ResponseConfig{
Ignore: true,
Parse: true,
},
})
err := wh.ExecuteLoginPreHook(nil, req, f)
assert.Error(t, err)
})
for _, tc := range []struct {
uc string
parse bool
}{
{uc: "Post Settings Hook - parse true", parse: true},
{uc: "Post Settings Hook - parse false", parse: false},
} {
t.Run("uc="+tc.uc, func(t *testing.T) {
t.Parallel()
ts := newServer(webHookHttpCodeWithBodyEndPoint(t, 200, []byte(`{"identity":{"traits":{"email":"some@other-example.org"}}}`)))
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &settings.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "POST",
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
Response: request.ResponseConfig{
Parse: tc.parse,
},
})
uuid := x.NewUUID()
in := &identity.Identity{ID: uuid}
s := &session.Session{ID: x.NewUUID(), Identity: in}
postPersistErr := wh.ExecuteSettingsPostPersistHook(nil, req, f, in, s)
assert.NoError(t, postPersistErr)
assert.Equal(t, in, &identity.Identity{ID: uuid})
prePersistErr := wh.ExecuteSettingsPrePersistHook(nil, req, f, in)
assert.NoError(t, prePersistErr)
if tc.parse == true {
assert.Equal(t, in, &identity.Identity{ID: uuid, Traits: identity.Traits(`{"email":"some@other-example.org"}`)})
} else {
assert.Equal(t, in, &identity.Identity{ID: uuid})
}
})
}
t.Run("must error when template is erroneous", func(t *testing.T) {
t.Parallel()
ts := newServer(webHookHttpCodeEndPoint(200))
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "POST",
URL: ts.URL + path,
TemplateURI: "file://./stub/bad_template.jsonnet",
})
err := wh.ExecuteLoginPreHook(nil, req, f)
assert.Error(t, err)
})
t.Run("must not error when template is erroneous and responses are ignored", func(t *testing.T) {
ts := newServer(webHookHttpCodeEndPoint(200))
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "GET",
URL: ts.URL + path,
TemplateURI: "file://./stub/bad_template.jsonnet",
Response: request.ResponseConfig{
Ignore: true,
},
})
err := wh.ExecuteLoginPreHook(nil, req, f)
assert.NoError(t, err)
})
t.Run("must not make request", func(t *testing.T) {
t.Parallel()
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "POST",
URL: "https://i-do-not-exist/",
TemplateURI: "file://./stub/cancel_template.jsonnet",
})
err := wh.ExecuteLoginPreHook(nil, req, f)
assert.NoError(t, err)
})
boolToString := func(f bool) string {
if f {
return " not"
} else {
return ""
}
}
t.Run("ignores the response and is async", func(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
wg.Add(1)
waitTime := time.Millisecond * 100
ts := newServer(func(w http.ResponseWriter, _ *http.Request) {
defer wg.Done()
time.Sleep(waitTime)
w.WriteHeader(http.StatusBadRequest)
})
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "GET",
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
Response: request.ResponseConfig{
Ignore: true,
},
})
start := time.Now()
err := wh.ExecuteLoginPreHook(nil, req, f)
assert.NoError(t, err)
assert.Less(t, time.Since(start), waitTime)
wg.Wait()
})
t.Run("does not error on 500 request with retry", func(t *testing.T) {
t.Parallel()
// This test essentially ensures that we do not regress on the bug we had where 500 status code
// would cause a retry, but because the body was incorrectly set we ended up with a ContentLength
// error.
var wg sync.WaitGroup
wg.Add(3) // HTTP client does 3 attempts
ts := newServer(func(w http.ResponseWriter, r *http.Request) {
defer wg.Done()
w.WriteHeader(500)
_, _ = w.Write([]byte(`{"error":"some error"}`))
})
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "GET",
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
})
err := wh.ExecuteLoginPreHook(nil, req, f)
require.Error(t, err)
assert.NotContains(t, err.Error(), "ContentLength")
wg.Wait()
})
for _, tc := range []struct {
code int
mustSuccess bool
}{
{200, true},
{299, true},
{300, true},
{399, true},
{400, false},
{499, false},
{500, false},
{599, false},
} {
tc := tc
t.Run("Must"+boolToString(tc.mustSuccess)+" error when end point is returning "+strconv.Itoa(tc.code), func(t *testing.T) {
t.Parallel()
ts := newServer(webHookHttpCodeEndPoint(tc.code))
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
Method: "POST",
URL: ts.URL + path,
TemplateURI: "file://./stub/test_body.jsonnet",
})
err := wh.ExecuteLoginPreHook(nil, req, f)
if tc.mustSuccess {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
func TestDisallowPrivateIPRanges(t *testing.T) {
t.Parallel()
ctx := context.Background()
conf, reg := internal.NewFastRegistryWithMocks(t)
conf.MustSet(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)
conf.MustSet(ctx, config.ViperKeyClientHTTPPrivateIPExceptionURLs, []string{"http://localhost/exception"})
logger := logrusx.New("kratos", "test")
whDeps := struct {
x.SimpleLoggerWithClient
*jsonnetsecure.TestProvider
config.Provider
}{
x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
jsonnetsecure.NewTestProvider(t),
reg,
}
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}}
f := &login.Flow{ID: x.NewUUID()}
t.Run("not allowed to call url", func(t *testing.T) {
t.Parallel()
wh := hook.NewWebHook(&whDeps, &request.Config{
URL: "https://localhost:1234/",
Method: "GET",
TemplateURI: "file://stub/test_body.jsonnet",
})
err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s)
require.Error(t, err)
require.Contains(t, err.Error(), "is not a permitted destination")
})
t.Run("allowed to call exempt url", func(t *testing.T) {
t.Parallel()
wh := hook.NewWebHook(&whDeps, &request.Config{
URL: "http://localhost/exception",
Method: "GET",
TemplateURI: "file://stub/test_body.jsonnet",
})
err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s)
require.Error(t, err, "the target does not exist and we still receive an error")
require.NotContains(t, err.Error(), "is not a permitted destination", "but the error is not related to the IP range.")
})
t.Run("not allowed to load from source", func(t *testing.T) {
t.Parallel()
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}}
f := &login.Flow{ID: x.NewUUID()}
wh := hook.NewWebHook(&whDeps, &request.Config{
URL: "https://www.google.com/",
Method: "GET",
TemplateURI: "http://192.168.178.0/test_body.jsonnet",
})
err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s)
require.Error(t, err)
require.Contains(t, err.Error(), "is not a permitted destination")
})
}
func TestAsyncWebhook(t *testing.T) {
t.Parallel()
_, reg := internal.NewFastRegistryWithMocks(t)
logger := logrusx.New("kratos", "test")
logHook := new(test.Hook)
logger.Logger.Hooks.Add(logHook)
whDeps := struct {
x.SimpleLoggerWithClient
*jsonnetsecure.TestProvider
config.Provider
}{
x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
jsonnetsecure.NewTestProvider(t),
reg,
}
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
incomingCtx, incomingCancel := context.WithCancel(context.Background())
if deadline, ok := t.Deadline(); ok {
// cancel this context one second before test timeout for clean shutdown
var cleanup context.CancelFunc
incomingCtx, cleanup = context.WithDeadline(incomingCtx, deadline.Add(-time.Second))
defer cleanup()
}
req = req.WithContext(incomingCtx)
s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}}
f := &login.Flow{ID: x.NewUUID()}
handlerEntered, blockHandlerOnExit := make(chan struct{}), make(chan struct{})
webhookReceiver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(handlerEntered)
<-blockHandlerOnExit
w.Write([]byte("ok"))
}))
t.Cleanup(webhookReceiver.Close)
wh := hook.NewWebHook(&whDeps, &request.Config{
URL: webhookReceiver.URL,
Method: "GET",
TemplateURI: "file://stub/test_body.jsonnet",
Response: request.ResponseConfig{
Ignore: true,
},
})
err := wh.ExecuteLoginPostHook(nil, req, node.DefaultGroup, f, s)
require.NoError(t, err) // execution returns immediately for async webhook
select {
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for webhook request to reach test handler")
case <-handlerEntered:
// ok
}
// at this point, a goroutine is in the middle of the call to our test handler and waiting for a response
incomingCancel() // simulate the incoming Kratos request having finished
close(blockHandlerOnExit)
timeout := time.After(1 * time.Second)
var found bool
for !found {
for _, entry := range logHook.AllEntries() {
if entry.Message == "Webhook request succeeded" {
found = true
break
}
}
select {
case <-timeout:
t.Fatal("timed out waiting for successful webhook completion")
case <-time.After(50 * time.Millisecond):
// continue loop
}
}
require.True(t, found)
}
func TestWebhookEvents(t *testing.T) {
t.Parallel()
_, reg := internal.NewFastRegistryWithMocks(t)
logger := logrusx.New("kratos", "test")
whDeps := struct {
x.SimpleLoggerWithClient
*jsonnetsecure.TestProvider
config.Provider
}{
x.SimpleLoggerWithClient{L: logger, C: reg.HTTPClient(context.Background()), T: otelx.NewNoop(logger, &otelx.Config{ServiceName: "kratos"})},
jsonnetsecure.NewTestProvider(t),
reg,
}
req := &http.Request{
Header: map[string][]string{"Some-Header": {"Some-Value"}},
Host: "www.ory.sh",
TLS: new(tls.ConnectionState),
URL: &url.URL{Path: "/some_end_point"},
Method: http.MethodPost,
}
f := &login.Flow{ID: x.NewUUID()}
webhookReceiver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ok" {
w.WriteHeader(200)
w.Write([]byte("ok"))
} else {
w.WriteHeader(500)
w.Write([]byte("fail"))
}
}))
t.Cleanup(webhookReceiver.Close)
getAttributes := func(attrs []attribute.KeyValue) (webhookID, triggerID, requestID string) {
for _, kv := range attrs {
switch semconv.AttributeKey(kv.Key) {
case events.AttributeKeyWebhookID:
webhookID = kv.Value.Emit()
case events.AttributeKeyWebhookTriggerID:
triggerID = kv.Value.Emit()
case events.AttributeKeyWebhookRequestID:
requestID = kv.Value.Emit()
}
}
return
}
t.Run("success", func(t *testing.T) {
whID := x.NewUUID()
wh := hook.NewWebHook(&whDeps, &request.Config{
ID: whID.String(),
URL: webhookReceiver.URL + "/ok",
Method: "GET",
TemplateURI: "file://stub/test_body.jsonnet",
})
recorder := tracetest.NewSpanRecorder()
tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test")
ctx, span := tracer.Start(context.Background(), "parent")
defer span.End()
r1 := req.Clone(ctx)
require.NoError(t, wh.ExecuteLoginPreHook(nil, r1, f))
ended := recorder.Ended()
require.NotEmpty(t, ended)
i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { return sp.Name() == "selfservice.webhook" })
require.GreaterOrEqual(t, i, 0)
evs := ended[i].Events()
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == events.WebhookDelivered.String() })
require.GreaterOrEqual(t, i, 0)
actualWhID, deliveredTriggerID, deliveredRequestID := getAttributes(evs[i].Attributes)
require.Equal(t, whID.String(), actualWhID)
require.NotEmpty(t, deliveredTriggerID)
require.NotEmpty(t, deliveredRequestID)
assert.NotEqual(t, deliveredTriggerID, deliveredRequestID)
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == events.WebhookSucceeded.String() })
require.GreaterOrEqual(t, i, 0)
actualWhID, succeededTriggerID, _ := getAttributes(evs[i].Attributes)
require.Equal(t, whID.String(), actualWhID)
require.NotEmpty(t, succeededTriggerID)
assert.Equal(t, deliveredTriggerID, succeededTriggerID)
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == events.WebhookFailed.String() })
require.Equal(t, -1, i)
})
t.Run("failed", func(t *testing.T) {
whID := x.NewUUID()
wh := hook.NewWebHook(&whDeps, &request.Config{
ID: whID.String(),
URL: webhookReceiver.URL + "/fail",
Method: "GET",
TemplateURI: "file://stub/test_body.jsonnet",
})
recorder := tracetest.NewSpanRecorder()
tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test")
ctx, span := tracer.Start(context.Background(), "parent")
defer span.End()
r1 := req.Clone(ctx)
require.Error(t, wh.ExecuteLoginPreHook(nil, r1, f))
ended := recorder.Ended()
require.NotEmpty(t, ended)
i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool { return sp.Name() == "selfservice.webhook" })
require.GreaterOrEqual(t, i, 0)
evs := ended[i].Events()
var deliveredEvents []sdktrace.Event
deliveredTriggerIDs := map[string]struct{}{}
deliveredRequestIDs := map[string]struct{}{}
for _, ev := range evs {
if ev.Name == events.WebhookDelivered.String() {
deliveredEvents = append(deliveredEvents, ev)
actualWhID, triggerID, requestID := getAttributes(ev.Attributes)
require.Equal(t, whID.String(), actualWhID)
require.NotEmpty(t, triggerID)
require.NotEmpty(t, requestID)
deliveredTriggerIDs[triggerID] = struct{}{}
deliveredRequestIDs[requestID] = struct{}{}
}
}
assert.Len(t, deliveredEvents, 3)
assert.Len(t, deliveredTriggerIDs, 1)
assert.Len(t, deliveredRequestIDs, 3)
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == "WebhookFailed" })
require.GreaterOrEqual(t, i, 0)
actualWhID, failedTriggerID, _ := getAttributes(evs[i].Attributes)
require.Equal(t, whID.String(), actualWhID)
require.NotEmpty(t, failedTriggerID)
assert.Contains(t, deliveredTriggerIDs, failedTriggerID)
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool { return ev.Name == "WebhookSucceeded" })
require.Equal(t, i, -1)
})
t.Run("event disabled", func(t *testing.T) {
wh := hook.NewWebHook(&whDeps, &request.Config{
URL: webhookReceiver.URL + "/fail",
Method: "GET",
TemplateURI: "file://stub/test_body.jsonnet",
EmitAnalyticsEvent: pointerx.Ptr(false),
})
recorder := tracetest.NewSpanRecorder()
tracer := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)).Tracer("test")
ctx, span := tracer.Start(context.Background(), "parent")
defer span.End()
r1 := req.Clone(ctx)
require.Error(t, wh.ExecuteLoginPreHook(nil, r1, f))
ended := recorder.Ended()
require.NotEmpty(t, ended)
i := slices.IndexFunc(ended, func(sp sdktrace.ReadOnlySpan) bool {
return sp.Name() == "selfservice.webhook"
})
require.GreaterOrEqual(t, i, 0)
evs := ended[i].Events()
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool {
return ev.Name == "WebhookDelivered"
})
require.Equal(t, -1, i)
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool {
return ev.Name == "WebhookFailed"
})
require.Equal(t, -1, i)
i = slices.IndexFunc(evs, func(ev sdktrace.Event) bool {
return ev.Name == "WebhookSucceeded"
})
require.Equal(t, i, -1)
})
}