snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
94
request/auth.go
Normal file
94
request/auth.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
)
|
||||
|
||||
type (
|
||||
noopAuthStrategy struct{}
|
||||
basicAuthStrategy struct {
|
||||
user string
|
||||
password string
|
||||
}
|
||||
apiKeyStrategy struct {
|
||||
name string
|
||||
value string
|
||||
in string
|
||||
}
|
||||
AuthStrategy interface {
|
||||
apply(req *retryablehttp.Request)
|
||||
}
|
||||
)
|
||||
|
||||
func authStrategy(typ string, config map[string]any) (AuthStrategy, error) {
|
||||
switch typ {
|
||||
case "":
|
||||
return NewNoopAuthStrategy(), nil
|
||||
case "api_key":
|
||||
name, ok := config["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("api_key auth strategy requires a string name")
|
||||
}
|
||||
value, ok := config["value"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("api_key auth strategy requires a string value")
|
||||
}
|
||||
in, _ := config["in"].(string) // in is optional
|
||||
return NewAPIKeyStrategy(in, name, value), nil
|
||||
case "basic_auth":
|
||||
user, ok := config["user"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("basic_auth auth strategy requires a string user")
|
||||
}
|
||||
password, ok := config["password"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("basic_auth auth strategy requires a string password")
|
||||
}
|
||||
return NewBasicAuthStrategy(user, password), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported auth type: %s", typ)
|
||||
}
|
||||
|
||||
func NewNoopAuthStrategy() AuthStrategy {
|
||||
return &noopAuthStrategy{}
|
||||
}
|
||||
|
||||
func (c *noopAuthStrategy) apply(_ *retryablehttp.Request) {}
|
||||
|
||||
func NewBasicAuthStrategy(user, password string) AuthStrategy {
|
||||
return &basicAuthStrategy{
|
||||
user: user,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *basicAuthStrategy) apply(req *retryablehttp.Request) {
|
||||
req.SetBasicAuth(c.user, c.password)
|
||||
}
|
||||
|
||||
func NewAPIKeyStrategy(in, name, value string) AuthStrategy {
|
||||
return &apiKeyStrategy{
|
||||
in: in,
|
||||
name: name,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *apiKeyStrategy) apply(req *retryablehttp.Request) {
|
||||
switch c.in {
|
||||
case "cookie":
|
||||
req.AddCookie(&http.Cookie{Name: c.name, Value: c.value})
|
||||
default:
|
||||
// TODO add deprecation warning
|
||||
fallthrough
|
||||
case "header", "":
|
||||
req.Header.Set(c.name, c.value)
|
||||
}
|
||||
}
|
||||
120
request/auth_test.go
Normal file
120
request/auth_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNoopAuthStrategy(t *testing.T) {
|
||||
req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}}
|
||||
auth := noopAuthStrategy{}
|
||||
|
||||
auth.apply(&req)
|
||||
|
||||
assert.Empty(t, req.Header, "Empty auth strategy shall not modify any request headers")
|
||||
}
|
||||
|
||||
func TestBasicAuthStrategy(t *testing.T) {
|
||||
req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}}
|
||||
auth := basicAuthStrategy{
|
||||
user: "test-user",
|
||||
password: "test-pass",
|
||||
}
|
||||
|
||||
auth.apply(&req)
|
||||
|
||||
assert.Len(t, req.Header, 1)
|
||||
|
||||
user, pass, _ := req.BasicAuth()
|
||||
assert.Equal(t, "test-user", user)
|
||||
assert.Equal(t, "test-pass", pass)
|
||||
}
|
||||
|
||||
func TestApiKeyInHeaderStrategy(t *testing.T) {
|
||||
req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}}
|
||||
auth := apiKeyStrategy{
|
||||
in: "header",
|
||||
name: "my-api-key-name",
|
||||
value: "my-api-key-value",
|
||||
}
|
||||
|
||||
auth.apply(&req)
|
||||
|
||||
require.Len(t, req.Header, 1)
|
||||
|
||||
actualValue := req.Header.Get("my-api-key-name")
|
||||
assert.Equal(t, "my-api-key-value", actualValue)
|
||||
}
|
||||
|
||||
func TestApiKeyInCookieStrategy(t *testing.T) {
|
||||
req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}}
|
||||
auth := apiKeyStrategy{
|
||||
in: "cookie",
|
||||
name: "my-api-key-name",
|
||||
value: "my-api-key-value",
|
||||
}
|
||||
|
||||
auth.apply(&req)
|
||||
|
||||
cookies := req.Cookies()
|
||||
assert.Len(t, cookies, 1)
|
||||
|
||||
assert.Equal(t, "my-api-key-name", cookies[0].Name)
|
||||
assert.Equal(t, "my-api-key-value", cookies[0].Value)
|
||||
}
|
||||
|
||||
func TestAuthStrategy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range map[string]struct {
|
||||
name string
|
||||
config map[string]any
|
||||
expected AuthStrategy
|
||||
}{
|
||||
"noop": {
|
||||
name: "",
|
||||
config: map[string]any{},
|
||||
expected: &noopAuthStrategy{},
|
||||
},
|
||||
"basic_auth": {
|
||||
name: "basic_auth",
|
||||
config: map[string]any{
|
||||
"user": "test-api-user",
|
||||
"password": "secret",
|
||||
},
|
||||
expected: &basicAuthStrategy{},
|
||||
},
|
||||
"api-key/header": {
|
||||
name: "api_key",
|
||||
config: map[string]any{
|
||||
"in": "header",
|
||||
"name": "my-api-key",
|
||||
"value": "secret",
|
||||
},
|
||||
expected: &apiKeyStrategy{},
|
||||
},
|
||||
"api-key/cookie": {
|
||||
name: "api_key",
|
||||
config: map[string]any{
|
||||
"in": "cookie",
|
||||
"name": "my-api-key",
|
||||
"value": "secret",
|
||||
},
|
||||
expected: &apiKeyStrategy{},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
strategy, err := authStrategy(tc.name, tc.config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.IsTypef(t, tc.expected, strategy, "auth strategy should be of the expected type")
|
||||
})
|
||||
}
|
||||
}
|
||||
301
request/builder.go
Normal file
301
request/builder.go
Normal file
@@ -0,0 +1,301 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
"github.com/google/go-jsonnet"
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/x/fetcher"
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/jsonnetsecure"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/otelx"
|
||||
)
|
||||
|
||||
var ErrCancel = errors.New("request cancel by JsonNet")
|
||||
|
||||
const (
|
||||
ContentTypeForm = "application/x-www-form-urlencoded"
|
||||
ContentTypeJSON = "application/json"
|
||||
)
|
||||
|
||||
type (
|
||||
Dependencies interface {
|
||||
logrusx.Provider
|
||||
otelx.Provider
|
||||
httpx.ClientProvider
|
||||
jsonnetsecure.VMProvider
|
||||
}
|
||||
Builder struct {
|
||||
r *retryablehttp.Request
|
||||
Config *Config
|
||||
deps Dependencies
|
||||
cache *ristretto.Cache[[]byte, []byte]
|
||||
bodySizeHint uint
|
||||
}
|
||||
options struct {
|
||||
cache *ristretto.Cache[[]byte, []byte]
|
||||
bodySizeHint uint
|
||||
}
|
||||
BuilderOption = func(*options)
|
||||
)
|
||||
|
||||
func WithCache(cache *ristretto.Cache[[]byte, []byte]) BuilderOption {
|
||||
return func(o *options) {
|
||||
o.cache = cache
|
||||
}
|
||||
}
|
||||
|
||||
func WithBodySizeHint(hint uint) BuilderOption {
|
||||
return func(o *options) {
|
||||
o.bodySizeHint = hint
|
||||
}
|
||||
}
|
||||
|
||||
func NewBuilder(c *Config, deps Dependencies, o ...BuilderOption) (*Builder, error) {
|
||||
var opts options
|
||||
for _, f := range o {
|
||||
f(&opts)
|
||||
}
|
||||
|
||||
r, err := retryablehttp.NewRequest(c.Method, c.URL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.header = make(http.Header, len(c.Headers))
|
||||
for k, v := range c.Headers {
|
||||
c.header.Add(k, v)
|
||||
}
|
||||
if c.header.Get("Content-Type") == "" {
|
||||
c.header.Set("Content-Type", ContentTypeJSON)
|
||||
}
|
||||
|
||||
c.auth, err = authStrategy(c.Auth.Type, c.Auth.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Builder{
|
||||
r: r,
|
||||
Config: c,
|
||||
deps: deps,
|
||||
cache: opts.cache,
|
||||
bodySizeHint: opts.bodySizeHint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *Builder) addBody(ctx context.Context, body interface{}) (err error) {
|
||||
ctx, span := b.deps.Tracer(ctx).Tracer().Start(ctx, "request.Builder.addBody")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
if isNilInterface(body) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.Config.TemplateURI == "" {
|
||||
return errors.New("got empty template path for request with body")
|
||||
}
|
||||
|
||||
tpl, err := b.readTemplate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch b.r.Header.Get("Content-Type") {
|
||||
case ContentTypeForm:
|
||||
if err := b.addURLEncodedBody(ctx, tpl, body); err != nil {
|
||||
return err
|
||||
}
|
||||
case "":
|
||||
b.r.Header.Set("Content-Type", ContentTypeJSON)
|
||||
fallthrough
|
||||
case ContentTypeJSON:
|
||||
if err := b.addJSONBody(ctx, tpl, body); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("invalid config - incorrect Content-Type for request with body")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) addJSONBody(ctx context.Context, jsonnetSnippet []byte, body interface{}) error {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, b.bodySizeHint))
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", "")
|
||||
|
||||
if err := enc.Encode(body); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
vm, err := b.deps.JsonnetVM(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
vm.TLACode("ctx", buf.String())
|
||||
|
||||
res, err := vm.EvaluateAnonymousSnippet(
|
||||
b.Config.TemplateURI,
|
||||
string(jsonnetSnippet),
|
||||
)
|
||||
if err != nil {
|
||||
// Unfortunately we can not use errors.As / errors.Is, see:
|
||||
// https://github.com/google/go-jsonnet/issues/592
|
||||
if strings.Contains(err.Error(), (&jsonnet.RuntimeError{Msg: "cancel"}).Error()) {
|
||||
return errors.WithStack(ErrCancel)
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
rb := strings.NewReader(res)
|
||||
if err := b.r.SetBody(io.NopCloser(rb)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) addURLEncodedBody(ctx context.Context, jsonnetSnippet []byte, body interface{}) error {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, b.bodySizeHint))
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", "")
|
||||
|
||||
if err := enc.Encode(body); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
vm, err := b.deps.JsonnetVM(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
vm.TLACode("ctx", buf.String())
|
||||
|
||||
res, err := vm.EvaluateAnonymousSnippet(b.Config.TemplateURI, string(jsonnetSnippet))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
values := map[string]string{}
|
||||
if err := json.Unmarshal([]byte(res), &values); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
u := url.Values{}
|
||||
|
||||
for key, value := range values {
|
||||
u.Add(key, value)
|
||||
}
|
||||
|
||||
rb := strings.NewReader(u.Encode())
|
||||
if err := b.r.SetBody(io.NopCloser(rb)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) BuildRequest(ctx context.Context, body interface{}) (*retryablehttp.Request, error) {
|
||||
b.r.Header = b.Config.header
|
||||
b.Config.auth.apply(b.r)
|
||||
|
||||
// According to the HTTP spec any request method, but TRACE is allowed to
|
||||
// have a body. Even this is a bad practice for some of them, like for GET
|
||||
if b.Config.Method != http.MethodTrace {
|
||||
if err := b.addBody(ctx, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b.r.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
func (b *Builder) addRawBody(body any) (err error) {
|
||||
if isNilInterface(body) {
|
||||
return nil
|
||||
}
|
||||
buf := bytes.NewBuffer(make([]byte, 0, b.bodySizeHint))
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
||||
if err := enc.Encode(body); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
switch contentType := b.r.Header.Get("Content-Type"); contentType {
|
||||
case "":
|
||||
b.r.Header.Set("Content-Type", ContentTypeJSON)
|
||||
fallthrough
|
||||
case ContentTypeJSON:
|
||||
if err := b.r.SetBody(buf); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
default:
|
||||
return herodot.ErrMisconfiguration().WithDetail("invalid_content_type", contentType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) BuildRawRequest(ctx context.Context, body any) (*retryablehttp.Request, error) {
|
||||
b.r.Header = b.Config.header
|
||||
b.Config.auth.apply(b.r)
|
||||
|
||||
// According to the HTTP spec any request method, but TRACE is allowed to
|
||||
// have a body. Even this is a bad practice for some of them, like for GET
|
||||
if b.Config.Method != http.MethodTrace {
|
||||
if err := b.addRawBody(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b.r.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
func (b *Builder) readTemplate(ctx context.Context) ([]byte, error) {
|
||||
templateURI := b.Config.TemplateURI
|
||||
|
||||
if templateURI == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
f := fetcher.NewFetcher(fetcher.WithClient(b.deps.HTTPClient(ctx)), fetcher.WithCache(b.cache, 60*time.Minute))
|
||||
tpl, err := f.FetchContext(ctx, templateURI)
|
||||
if errors.Is(err, fetcher.ErrUnknownScheme) {
|
||||
// legacy filepath
|
||||
templateURI = "file://" + templateURI
|
||||
b.deps.Logger().WithError(err).Warnf(
|
||||
"support for filepaths without a 'file://' scheme will be dropped in the next release, please use %s instead in your config",
|
||||
templateURI)
|
||||
|
||||
tpl, err = f.FetchContext(ctx, templateURI)
|
||||
}
|
||||
// this handles the first error if it is a known scheme error, or the second fetch error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tpl.Bytes(), nil
|
||||
}
|
||||
|
||||
func isNilInterface(i interface{}) bool {
|
||||
return i == nil || (reflect.ValueOf(i).Kind() == reflect.Pointer && reflect.ValueOf(i).IsNil())
|
||||
}
|
||||
343
request/builder_test.go
Normal file
343
request/builder_test.go
Normal file
@@ -0,0 +1,343 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/jsonnetsecure"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/otelx"
|
||||
)
|
||||
|
||||
type testRequestBody struct {
|
||||
To string `json:"to"`
|
||||
From string `json:"from"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
//go:embed stub/test_body.jsonnet
|
||||
var testJSONNetTemplate []byte
|
||||
|
||||
func TestBuildRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
method string
|
||||
url string
|
||||
authStrategy AuthStrategy
|
||||
expectedHeader http.Header
|
||||
bodyTemplateURI string
|
||||
body *testRequestBody
|
||||
expectedJSONBody string
|
||||
expectedRawBody string
|
||||
config Config
|
||||
}{
|
||||
{
|
||||
name: "POST request without auth",
|
||||
method: "POST",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint1",
|
||||
authStrategy: NewNoopAuthStrategy(),
|
||||
bodyTemplateURI: "file://./stub/test_body.jsonnet",
|
||||
body: &testRequestBody{
|
||||
To: "+15056445993",
|
||||
From: "+12288534869",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+12288534869",
|
||||
"to": "+15056445993"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint1",
|
||||
Method: "POST",
|
||||
TemplateURI: "file://./stub/test_body.jsonnet",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST request with legacy template path",
|
||||
method: "POST",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint1",
|
||||
authStrategy: NewNoopAuthStrategy(),
|
||||
bodyTemplateURI: "./stub/test_body.jsonnet",
|
||||
body: &testRequestBody{
|
||||
To: "+15056445993",
|
||||
From: "+12288534869",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+12288534869",
|
||||
"to": "+15056445993"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint1",
|
||||
Method: "POST",
|
||||
TemplateURI: "./stub/test_body.jsonnet",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST request with base64 encoded template path",
|
||||
method: "POST",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint1",
|
||||
authStrategy: NewNoopAuthStrategy(),
|
||||
bodyTemplateURI: "base64://" + base64.StdEncoding.EncodeToString(testJSONNetTemplate),
|
||||
body: &testRequestBody{
|
||||
To: "+15056445993",
|
||||
From: "+12288534869",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+12288534869",
|
||||
"to": "+15056445993"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint1",
|
||||
Method: "POST",
|
||||
TemplateURI: "base64://" + base64.StdEncoding.EncodeToString(testJSONNetTemplate),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST request with custom header",
|
||||
method: "POST",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint2",
|
||||
authStrategy: NewNoopAuthStrategy(),
|
||||
expectedHeader: map[string][]string{"Custom-Header": {"test"}},
|
||||
bodyTemplateURI: "file://./stub/test_body.jsonnet",
|
||||
body: &testRequestBody{
|
||||
To: "+12127110378",
|
||||
From: "+15822228108",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+15822228108",
|
||||
"to": "+12127110378"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint2",
|
||||
Method: "POST",
|
||||
Headers: map[string]string{
|
||||
"Custom-Header": "test",
|
||||
},
|
||||
TemplateURI: "file://./stub/test_body.jsonnet",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET request with body",
|
||||
method: "GET",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint3",
|
||||
authStrategy: NewBasicAuthStrategy("test-api-user", "secret"),
|
||||
bodyTemplateURI: "file://./stub/test_body.jsonnet",
|
||||
body: &testRequestBody{
|
||||
To: "+14134242223",
|
||||
From: "+13104661805",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+13104661805",
|
||||
"to": "+14134242223"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint3",
|
||||
Method: "GET",
|
||||
Auth: AuthConfig{
|
||||
Type: "basic_auth",
|
||||
Config: map[string]any{
|
||||
"user": "test-api-user",
|
||||
"password": "secret",
|
||||
},
|
||||
},
|
||||
TemplateURI: "file://./stub/test_body.jsonnet",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET request without body",
|
||||
method: "GET",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint4",
|
||||
authStrategy: NewBasicAuthStrategy("test-api-user", "secret"),
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint4",
|
||||
Method: "GET",
|
||||
Auth: AuthConfig{
|
||||
Type: "basic_auth",
|
||||
Config: map[string]any{
|
||||
"user": "test-api-user",
|
||||
"password": "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DELETE request with body",
|
||||
method: "DELETE",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint5",
|
||||
authStrategy: NewAPIKeyStrategy("header", "my-api-key", "secret"),
|
||||
bodyTemplateURI: "file://./stub/test_body.jsonnet",
|
||||
body: &testRequestBody{
|
||||
To: "+12235499085",
|
||||
From: "+14253787846",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+14253787846",
|
||||
"to": "+12235499085"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint5",
|
||||
Method: "DELETE",
|
||||
TemplateURI: "file://./stub/test_body.jsonnet",
|
||||
Auth: AuthConfig{
|
||||
Type: "api_key",
|
||||
Config: map[string]any{
|
||||
"in": "header",
|
||||
"name": "my-api-key",
|
||||
"value": "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST request with urlencoded body",
|
||||
method: "POST",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint6",
|
||||
bodyTemplateURI: "file://./stub/test_body.jsonnet",
|
||||
authStrategy: NewAPIKeyStrategy("cookie", "my-api-key", "secret"),
|
||||
expectedHeader: map[string][]string{"Content-Type": {ContentTypeForm}},
|
||||
body: &testRequestBody{
|
||||
To: "+14134242223",
|
||||
From: "+13104661805",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedRawBody: "body=test-sms-body&from=%2B13104661805&to=%2B14134242223",
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint6",
|
||||
Method: "POST",
|
||||
TemplateURI: "file://./stub/test_body.jsonnet",
|
||||
Headers: map[string]string{
|
||||
"Content-Type": ContentTypeForm,
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
Type: "api_key",
|
||||
Config: map[string]any{
|
||||
"in": "cookie",
|
||||
"name": "my-api-key",
|
||||
"value": "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST request with default body type",
|
||||
method: "POST",
|
||||
url: "https://test.kratos.ory.sh/my_endpoint7",
|
||||
bodyTemplateURI: "file://./stub/test_body.jsonnet",
|
||||
authStrategy: NewBasicAuthStrategy("test-api-user", "secret"),
|
||||
expectedHeader: map[string][]string{"Content-Type": {ContentTypeJSON}},
|
||||
body: &testRequestBody{
|
||||
To: "+14134242223",
|
||||
From: "+13104661805",
|
||||
Body: "test-sms-body",
|
||||
},
|
||||
expectedJSONBody: `{
|
||||
"body": "test-sms-body",
|
||||
"from": "+13104661805",
|
||||
"to": "+14134242223"
|
||||
}`,
|
||||
config: Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint7",
|
||||
Method: "POST",
|
||||
TemplateURI: "file://./stub/test_body.jsonnet",
|
||||
Auth: AuthConfig{
|
||||
Type: "basic_auth",
|
||||
Config: map[string]any{
|
||||
"user": "test-api-user",
|
||||
"password": "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run("request-type="+tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rb, err := NewBuilder(&tc.config, newTestDependencyProvider(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.bodyTemplateURI, rb.Config.TemplateURI)
|
||||
assert.Equal(t, tc.authStrategy, rb.Config.auth)
|
||||
|
||||
req, err := rb.BuildRequest(context.Background(), tc.body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.url, req.URL.String())
|
||||
assert.Equal(t, tc.method, req.Method)
|
||||
|
||||
if tc.expectedJSONBody != "" {
|
||||
requestBody, err := req.BodyBytes()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, tc.expectedJSONBody, string(requestBody))
|
||||
} else if tc.expectedRawBody != "" {
|
||||
requestBody, err := req.BodyBytes()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expectedRawBody, string(requestBody))
|
||||
}
|
||||
|
||||
if tc.expectedHeader != nil {
|
||||
mustContainHeader(t, tc.expectedHeader, req.Header)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("cancel request", func(t *testing.T) {
|
||||
rb, err := NewBuilder(&Config{
|
||||
URL: "https://test.kratos.ory.sh/my_endpoint6",
|
||||
Method: "POST",
|
||||
TemplateURI: "file://./stub/cancel_body.jsonnet",
|
||||
}, newTestDependencyProvider(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = rb.BuildRequest(context.Background(), json.RawMessage(`{}`))
|
||||
require.ErrorIs(t, err, ErrCancel)
|
||||
})
|
||||
}
|
||||
|
||||
type testDependencyProvider struct {
|
||||
x.BasicRegistry
|
||||
*jsonnetsecure.TestProvider
|
||||
}
|
||||
|
||||
func newTestDependencyProvider(t *testing.T) *testDependencyProvider {
|
||||
return &testDependencyProvider{
|
||||
BasicRegistry: x.BasicRegistry{
|
||||
L: logrusx.New("kratos", "test"),
|
||||
T: otelx.NewNoop(),
|
||||
},
|
||||
TestProvider: jsonnetsecure.NewTestProvider(t),
|
||||
}
|
||||
}
|
||||
|
||||
func mustContainHeader(t *testing.T, expected http.Header, actual http.Header) {
|
||||
t.Helper()
|
||||
for k := range expected {
|
||||
require.Contains(t, actual, k)
|
||||
assert.Equal(t, expected[k], actual[k])
|
||||
}
|
||||
}
|
||||
33
request/config.go
Normal file
33
request/config.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
AuthConfig = struct {
|
||||
Type string `json:"type" koanf:"type"`
|
||||
Config map[string]any `json:"config" koanf:"config"`
|
||||
}
|
||||
ResponseConfig = struct {
|
||||
Parse bool `json:"parse" koanf:"parse"`
|
||||
Ignore bool `json:"ignore" koanf:"ignore"`
|
||||
}
|
||||
Config struct {
|
||||
ID string `json:"id" koanf:"id"`
|
||||
Method string `json:"method" koanf:"method"`
|
||||
URL string `json:"url" koanf:"url"`
|
||||
TemplateURI string `json:"body" koanf:"body"`
|
||||
Headers map[string]string `json:"headers" koanf:"headers"`
|
||||
Auth AuthConfig `json:"auth" koanf:"auth"`
|
||||
EmitAnalyticsEvent *bool `json:"emit_analytics_event" koanf:"emit_analytics_event"`
|
||||
CanInterrupt bool `json:"can_interrupt" koanf:"can_interrupt"`
|
||||
Response ResponseConfig `json:"response" koanf:"response"`
|
||||
|
||||
auth AuthStrategy
|
||||
header http.Header
|
||||
}
|
||||
)
|
||||
1
request/stub/cancel_body.jsonnet
Normal file
1
request/stub/cancel_body.jsonnet
Normal file
@@ -0,0 +1 @@
|
||||
error "cancel"
|
||||
5
request/stub/test_body.jsonnet
Normal file
5
request/stub/test_body.jsonnet
Normal file
@@ -0,0 +1,5 @@
|
||||
function(ctx) {
|
||||
from: ctx.from,
|
||||
to: ctx.to,
|
||||
body: ctx.body,
|
||||
}
|
||||
Reference in New Issue
Block a user