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:
91
pkg/testhelpers/config.go
Normal file
91
pkg/testhelpers/config.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/x/contextx"
|
||||
"github.com/ory/x/randx"
|
||||
)
|
||||
|
||||
func DefaultIdentitySchemaConfig(url string) map[string]any {
|
||||
return map[string]any{
|
||||
config.ViperKeyDefaultIdentitySchemaID: "default",
|
||||
config.ViperKeyIdentitySchemas: config.Schemas{
|
||||
{ID: "default", URL: url},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultRawIdentitySchemaConfig(schema []byte) map[string]any {
|
||||
return DefaultIdentitySchemaConfig("base64://" + base64.URLEncoding.EncodeToString(schema))
|
||||
}
|
||||
|
||||
func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context {
|
||||
return contextx.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url))
|
||||
}
|
||||
|
||||
// Deprecated: Use context-based WithDefaultIdentitySchema instead
|
||||
func SetDefaultIdentitySchema(conf *config.Config, url string) func() {
|
||||
schemaUrl, _ := conf.DefaultIdentityTraitsSchemaURL(context.Background())
|
||||
conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default")
|
||||
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{
|
||||
{ID: "default", URL: url},
|
||||
})
|
||||
return func() {
|
||||
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{
|
||||
{ID: "default", URL: schemaUrl.String()},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithAddIdentitySchema registers an identity schema in the config with a random ID and returns the ID
|
||||
func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Config, url string) (context.Context, string) {
|
||||
id := randx.MustString(16, randx.Alpha)
|
||||
schemas, err := conf.IdentityTraitsSchemas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
return contextx.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{
|
||||
ID: id,
|
||||
URL: url,
|
||||
})), id
|
||||
}
|
||||
|
||||
// UseIdentitySchema registers an identity schema in the config with a random ID and returns the ID
|
||||
//
|
||||
// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes
|
||||
// Deprecated: Use context-based WithAddIdentitySchema instead
|
||||
func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string) {
|
||||
id = randx.MustString(16, randx.Alpha)
|
||||
schemas, err := conf.IdentityTraitsSchemas(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, append(schemas, config.Schema{
|
||||
ID: id,
|
||||
URL: url,
|
||||
}))
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, schemas)
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
// WithDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string.
|
||||
func WithDefaultIdentitySchemaFromRaw(ctx context.Context, schema []byte) context.Context {
|
||||
return WithDefaultIdentitySchema(ctx, "base64://"+base64.URLEncoding.EncodeToString(schema))
|
||||
}
|
||||
|
||||
// Deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead
|
||||
func SetDefaultIdentitySchemaFromRaw(conf *config.Config, schema []byte) {
|
||||
conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default")
|
||||
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{
|
||||
{ID: "default", URL: "base64://" + base64.URLEncoding.EncodeToString(schema)},
|
||||
})
|
||||
}
|
||||
61
pkg/testhelpers/courier.go
Normal file
61
pkg/testhelpers/courier.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier"
|
||||
keysetpagination "github.com/ory/x/pagination/keysetpagination_v2"
|
||||
)
|
||||
|
||||
func CourierExpectMessage(ctx context.Context, t *testing.T, reg interface {
|
||||
courier.PersistenceProvider
|
||||
}, recipient, subject string,
|
||||
) *courier.Message {
|
||||
t.Helper()
|
||||
messages, _, err := reg.CourierPersister().ListMessages(ctx, courier.ListCourierMessagesParameters{
|
||||
Recipient: recipient,
|
||||
}, []keysetpagination.Option{})
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(messages, func(i, j int) bool {
|
||||
return messages[i].CreatedAt.After(messages[j].CreatedAt)
|
||||
})
|
||||
|
||||
for _, m := range messages {
|
||||
if strings.EqualFold(m.Recipient, recipient) && (strings.Contains(m.Subject, subject) || strings.Contains(m.Body, subject)) {
|
||||
return &m
|
||||
}
|
||||
}
|
||||
|
||||
require.Failf(t, "could not find courier messages", "could not find courier messages with recipient %s and subject %s", recipient, subject)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CourierExpectLinkInMessage(t *testing.T, message *courier.Message, offset int) string {
|
||||
if offset == 0 {
|
||||
offset = 1
|
||||
}
|
||||
match := regexp.MustCompile(`(http[^\s]+)`).FindStringSubmatch(message.Body)
|
||||
require.Len(t, match, offset*2)
|
||||
|
||||
return match[offset]
|
||||
}
|
||||
|
||||
func CourierExpectCodeInMessage(t *testing.T, message *courier.Message, offset int) string {
|
||||
if offset == 0 {
|
||||
offset = 1
|
||||
}
|
||||
match := regexp.MustCompile(CodeRegex).FindStringSubmatch(message.Body)
|
||||
require.Len(t, match, offset*2)
|
||||
|
||||
return match[offset]
|
||||
}
|
||||
225
pkg/testhelpers/e2e_server.go
Normal file
225
pkg/testhelpers/e2e_server.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/x/jsonnetsecure"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/ory/x/tlsx"
|
||||
|
||||
"github.com/phayes/freeport"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/cmd"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/cmdx"
|
||||
"github.com/ory/x/configx"
|
||||
)
|
||||
|
||||
type ConfigOptions = map[string]interface{}
|
||||
|
||||
func StartE2EServerOnly(t *testing.T, configFile string, isTLS bool, configOptions ConfigOptions) (publicPort, adminPort int) {
|
||||
return startE2EServerOnly(t, configFile, isTLS, configOptions, 0)
|
||||
}
|
||||
|
||||
func startE2EServerOnly(t *testing.T, configFile string, isTLS bool, configOptions ConfigOptions, tries int) (publicPort, adminPort int) {
|
||||
adminPort, err := freeport.GetFreePort()
|
||||
require.NoError(t, err)
|
||||
|
||||
publicPort, err = freeport.GetFreePort()
|
||||
require.NoError(t, err)
|
||||
|
||||
publicUrl := fmt.Sprintf("http://127.0.0.1:%d", publicPort)
|
||||
adminUrl := fmt.Sprintf("http://127.0.0.1:%d", adminPort)
|
||||
|
||||
if isTLS {
|
||||
publicUrl = fmt.Sprintf("https://127.0.0.1:%d", publicPort)
|
||||
adminUrl = fmt.Sprintf("https://127.0.0.1:%d", adminPort)
|
||||
}
|
||||
|
||||
dsn := "sqlite://" + filepath.Join(t.TempDir(), "db.sqlite") + "?_fk=true&mode=rwc"
|
||||
|
||||
ctx := t.Context()
|
||||
defaultConfig := map[string]any{
|
||||
"dsn": dsn,
|
||||
"dev": true,
|
||||
"log.level": "error",
|
||||
"log.leak_sensitive_values": true,
|
||||
"serve.public.port": publicPort,
|
||||
"serve.admin.port": adminPort,
|
||||
"serve.public.base_url": publicUrl,
|
||||
"serve.admin.base_url": adminUrl,
|
||||
}
|
||||
|
||||
jsonnetPool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0))
|
||||
t.Cleanup(jsonnetPool.Close)
|
||||
|
||||
//nolint:staticcheck
|
||||
//lint:ignore SA1029 we really want this
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
executor := &cmdx.CommandExecuter{
|
||||
New: func() *cobra.Command {
|
||||
return cmd.NewRootCmd(driver.WithJsonnetPool(jsonnetPool), driver.WithConfigOptions(configx.WithValues(defaultConfig), configx.WithValues(configOptions)))
|
||||
},
|
||||
Ctx: ctx,
|
||||
}
|
||||
|
||||
t.Log("Starting migrations...")
|
||||
_ = executor.ExecNoErr(t, "migrate", "sql", dsn, "--yes")
|
||||
t.Logf("Migration done")
|
||||
|
||||
t.Log("Starting server...")
|
||||
stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
eg := executor.ExecBackground(nil, io.MultiWriter(os.Stdout, stdOut), io.MultiWriter(os.Stdout, stdErr), "serve", "--config", configFile, "--watch-courier")
|
||||
|
||||
err = waitTimeout(t, eg, time.Second)
|
||||
if err != nil && tries < 5 {
|
||||
if !errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "address already in use") || strings.Contains(stdErr.String(), "address already in use") {
|
||||
t.Logf("Detected an instance with port reuse, retrying #%d...", tries)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
cancel()
|
||||
return startE2EServerOnly(t, configFile, isTLS, configOptions, tries+1)
|
||||
}
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(cancel)
|
||||
return publicPort, adminPort
|
||||
}
|
||||
|
||||
// waitTimeout waits for the waitgroup for the specified max timeout.
|
||||
// Returns true if waiting timed out.
|
||||
func waitTimeout(t *testing.T, wg *errgroup.Group, timeout time.Duration) (err error) {
|
||||
c := make(chan struct{})
|
||||
go func() {
|
||||
defer close(c)
|
||||
err = wg.Wait()
|
||||
}()
|
||||
select {
|
||||
case <-c:
|
||||
return
|
||||
case <-time.After(timeout):
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func StartE2EServer(t *testing.T, configFile string, configOptions ConfigOptions) (publicUrl, adminUrl string) {
|
||||
publicPort, adminPort := StartE2EServerOnly(t, configFile, false, configOptions)
|
||||
return CheckE2EServerOnHTTP(t, publicPort, adminPort)
|
||||
}
|
||||
|
||||
func CheckE2EServerOnHTTP(t *testing.T, publicPort, adminPort int) (publicUrl, adminUrl string) {
|
||||
publicUrl = fmt.Sprintf("http://127.0.0.1:%d", publicPort)
|
||||
adminUrl = fmt.Sprintf("http://127.0.0.1:%d", adminPort)
|
||||
waitToComeAlive(t, publicUrl, adminUrl)
|
||||
return
|
||||
}
|
||||
|
||||
func waitToComeAlive(t *testing.T, publicUrl, adminUrl string) {
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
//#nosec G402 -- TLS InsecureSkipVerify set true
|
||||
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
for _, url := range []string{
|
||||
publicUrl + "/health/ready",
|
||||
adminUrl + "/health/ready",
|
||||
publicUrl + "/health/alive",
|
||||
adminUrl + "/health/alive",
|
||||
} {
|
||||
res, err := client.Get(url)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := x.MustReadAll(res.Body)
|
||||
_ = res.Body.Close()
|
||||
|
||||
require.Equalf(t, http.StatusOK, res.StatusCode, "%s", body)
|
||||
}
|
||||
}, 10*time.Second, time.Second)
|
||||
}
|
||||
|
||||
func CheckE2EServerOnHTTPS(t *testing.T, publicPort, adminPort int) (publicUrl, adminUrl string) {
|
||||
publicUrl = fmt.Sprintf("https://127.0.0.1:%d", publicPort)
|
||||
adminUrl = fmt.Sprintf("https://127.0.0.1:%d", adminPort)
|
||||
waitToComeAlive(t, publicUrl, adminUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateTLSCertificateFilesForTests writes a new, self-signed TLS
|
||||
// certificate+key (in PEM format) to a temporary location on disk and returns
|
||||
// the paths to both, as well as the respective contents in base64 encoding. The
|
||||
// files are automatically cleaned up when the given *testing.T concludes its
|
||||
// tests.
|
||||
func GenerateTLSCertificateFilesForTests(t *testing.T) (certPath, keyPath, certBase64, keyBase64 string) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := tlsx.CreateSelfSignedCertificate(privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// write cert
|
||||
certFile, err := os.CreateTemp(tmpDir, "test-*-cert.pem")
|
||||
require.NoError(t, err, "Failed to create temp file for certificate: %v", err)
|
||||
certPath = certFile.Name()
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||
certOut := io.MultiWriter(enc, certFile)
|
||||
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
||||
require.NoError(t, err, "Failed to write data to %q: %v", certPath, err)
|
||||
err = enc.Close()
|
||||
require.NoError(t, err, "Error closing base64 encoder")
|
||||
err = certFile.Close()
|
||||
require.NoError(t, err, "Error closing %q: %v", certPath, err)
|
||||
certBase64 = buf.String()
|
||||
t.Log("wrote", certPath)
|
||||
|
||||
// write key
|
||||
keyFile, err := os.CreateTemp(tmpDir, "test-*-key.pem")
|
||||
require.NoError(t, err, "Failed to create temp file for key: %v", err)
|
||||
keyPath = keyFile.Name()
|
||||
buf.Reset()
|
||||
enc = base64.NewEncoder(base64.StdEncoding, &buf)
|
||||
keyOut := io.MultiWriter(enc, keyFile)
|
||||
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
require.NoError(t, err, "Failed to marshal private key: %v", err)
|
||||
|
||||
err = pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
||||
require.NoError(t, err, "Failed to write data to %q: %v", keyPath, err)
|
||||
err = enc.Close()
|
||||
require.NoError(t, err, "Error closing base64 encoder")
|
||||
err = keyFile.Close()
|
||||
require.NoError(t, err, "Error closing %q: %v", keyPath, err)
|
||||
keyBase64 = buf.String()
|
||||
t.Log("wrote", keyPath)
|
||||
return
|
||||
}
|
||||
90
pkg/testhelpers/errorx.go
Normal file
90
pkg/testhelpers/errorx.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/logrusx"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/selfservice/errorx"
|
||||
"github.com/ory/kratos/session"
|
||||
"github.com/ory/kratos/x"
|
||||
)
|
||||
|
||||
func NewErrorTestServer(t *testing.T, reg interface {
|
||||
errorx.PersistenceProvider
|
||||
config.Provider
|
||||
},
|
||||
) *httptest.Server {
|
||||
logger := logrusx.New("", "", logrusx.ForceLevel(logrus.TraceLevel))
|
||||
writer := herodot.NewJSONWriter(logger)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
e, err := reg.SelfServiceErrorPersister().ReadErrorContainer(r.Context(), x.ParseUUID(r.URL.Query().Get("id")))
|
||||
require.NoError(t, err)
|
||||
t.Logf("landed on error UI with: %s", e.Errors)
|
||||
writer.Write(w, r, e.Errors)
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceErrorUI, ts.URL)
|
||||
return ts
|
||||
}
|
||||
|
||||
func NewRedirTS(t *testing.T, body string, conf *config.Config) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(body) == 0 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
conf.MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL)
|
||||
return ts
|
||||
}
|
||||
|
||||
func NewRedirSessionEchoTS(t *testing.T, reg interface {
|
||||
httpx.WriterProvider
|
||||
session.ManagementProvider
|
||||
config.Provider
|
||||
},
|
||||
) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// verify that the client has a session, and echo it back
|
||||
sess, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything)
|
||||
require.NoError(t, err, "Headers: %+v", r.Header)
|
||||
reg.Writer().Write(w, r, sess)
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
reg.Config().MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/return-ts")
|
||||
return ts
|
||||
}
|
||||
|
||||
func NewRedirNoSessionTS(t *testing.T, reg interface {
|
||||
httpx.WriterProvider
|
||||
session.ManagementProvider
|
||||
config.Provider
|
||||
},
|
||||
) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// verify that the client DOES NOT have a session
|
||||
_, err := reg.SessionManager().FetchFromRequest(r.Context(), r, session.ExpandEverything, identity.ExpandEverything)
|
||||
require.Error(t, err, "Headers: %+v", r.Header)
|
||||
reg.Writer().Write(w, r, nil)
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
reg.Config().MustSet(context.Background(), config.ViperKeySelfServiceBrowserDefaultReturnTo, ts.URL+"/return-ts")
|
||||
return ts
|
||||
}
|
||||
18
pkg/testhelpers/fake.go
Normal file
18
pkg/testhelpers/fake.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ory/x/randx"
|
||||
)
|
||||
|
||||
func RandomEmail() string {
|
||||
return strings.ToLower(randx.MustString(16, randx.Alpha) + "@ory.sh")
|
||||
}
|
||||
|
||||
func RandomPhone() string {
|
||||
return "+49151" + randx.MustString(8, randx.Numeric)
|
||||
}
|
||||
233
pkg/testhelpers/handler_mock.go
Normal file
233
pkg/testhelpers/handler_mock.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-faker/faker/v4"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/session"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/httprouterx"
|
||||
)
|
||||
|
||||
type mockDeps interface {
|
||||
identity.PrivilegedPoolProvider
|
||||
identity.ManagementProvider
|
||||
session.ManagementProvider
|
||||
session.PersistenceProvider
|
||||
config.Provider
|
||||
}
|
||||
|
||||
func MockSetSession(t *testing.T, reg mockDeps, conf *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
i.NID = uuid.Must(uuid.NewV4())
|
||||
require.NoError(t, i.SetCredentialsWithConfig(
|
||||
identity.CredentialsTypePassword,
|
||||
identity.Credentials{
|
||||
Type: identity.CredentialsTypePassword,
|
||||
Identifiers: []string{faker.Email()},
|
||||
},
|
||||
json.RawMessage(`{"hashed_password":"$"}`)))
|
||||
require.NoError(t, reg.IdentityManager().Create(t.Context(), i))
|
||||
|
||||
MockSetSessionWithIdentity(t, reg, conf, i)(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func MockSetSessionWithIdentity(t *testing.T, reg mockDeps, _ *config.Config, i *identity.Identity) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
activeSession, err := NewActiveSession(r, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
if aal := r.URL.Query().Get("set_aal"); len(aal) > 0 {
|
||||
activeSession.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel(aal)
|
||||
}
|
||||
require.NoError(t, reg.SessionManager().UpsertAndIssueCookie(t.Context(), w, r, activeSession))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, conf *config.Config, router *httprouterx.RouterPublic, req *http.Request) ([]byte, *http.Response) {
|
||||
return MockMakeAuthenticatedRequestWithClient(t, reg, conf, router, req, NewClientWithCookies(t))
|
||||
}
|
||||
|
||||
func MockMakeAuthenticatedRequestWithClient(t *testing.T, reg mockDeps, conf *config.Config, router *httprouterx.RouterPublic, req *http.Request, client *http.Client) ([]byte, *http.Response) {
|
||||
return MockMakeAuthenticatedRequestWithClientAndID(t, reg, conf, router, req, client, nil)
|
||||
}
|
||||
|
||||
func MockMakeAuthenticatedRequestWithClientAndID(t *testing.T, reg mockDeps, conf *config.Config, router *httprouterx.RouterPublic, req *http.Request, client *http.Client, id *identity.Identity) ([]byte, *http.Response) {
|
||||
set := "/" + uuid.Must(uuid.NewV4()).String() + "/set"
|
||||
if id == nil {
|
||||
router.GET(set, MockSetSession(t, reg, conf))
|
||||
} else {
|
||||
router.GET(set, MockSetSessionWithIdentity(t, reg, conf, id))
|
||||
}
|
||||
|
||||
MockHydrateCookieClient(t, client, "http://"+req.URL.Host+set+"?"+req.URL.Query().Encode())
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, errors.WithStack(err))
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, errors.WithStack(err))
|
||||
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
return body, res
|
||||
}
|
||||
|
||||
// NewTestTransport returns a transport dedicated to a single test client.
|
||||
//
|
||||
// Test helpers spin up short-lived httptest.Server instances and close them
|
||||
// when the helper returns. httptest.Server.Close unconditionally calls
|
||||
// http.DefaultTransport.CloseIdleConnections as a courtesy for users of the
|
||||
// default transport. When many parallel subtests share http.DefaultTransport
|
||||
// (the zero value of http.Client.Transport), one subtest closing its server
|
||||
// tears down idle connections that another subtest's in-flight request is
|
||||
// using, surfacing as "http: CloseIdleConnections called". Giving every test
|
||||
// client its own transport means that courtesy close only ever affects the
|
||||
// unused global transport, never a live test client.
|
||||
func NewTestTransport(t testing.TB) *http.Transport {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
t.Cleanup(transport.CloseIdleConnections)
|
||||
return transport
|
||||
}
|
||||
|
||||
// NewTestClient returns a jar-less client with a dedicated transport, see NewTestTransport.
|
||||
func NewTestClient(t testing.TB) *http.Client {
|
||||
return &http.Client{Transport: NewTestTransport(t)}
|
||||
}
|
||||
|
||||
func NewClientWithCookies(t *testing.T) *http.Client {
|
||||
cj, err := cookiejar.New(&cookiejar.Options{})
|
||||
require.NoError(t, err)
|
||||
return &http.Client{Jar: cj, Transport: NewTestTransport(t)}
|
||||
}
|
||||
|
||||
func NewNoRedirectClientWithCookies(t *testing.T) *http.Client {
|
||||
cj, err := cookiejar.New(&cookiejar.Options{})
|
||||
require.NoError(t, err)
|
||||
return &http.Client{
|
||||
Jar: cj,
|
||||
Transport: NewTestTransport(t),
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) *http.Cookie {
|
||||
var sessionCookie *http.Cookie
|
||||
res, err := c.Get(u)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
body := x.MustReadAll(res.Body)
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
var found bool
|
||||
for _, rc := range res.Cookies() {
|
||||
if rc.Name == config.DefaultSessionCookieName {
|
||||
found = true
|
||||
sessionCookie = rc
|
||||
}
|
||||
}
|
||||
require.True(t, found, "got body: %s\ngot url: %s", body, res.Request.URL.String())
|
||||
return sessionCookie
|
||||
}
|
||||
|
||||
func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (http.HandlerFunc, *session.Session) {
|
||||
var ct []identity.CredentialsType
|
||||
|
||||
// if identity was not created with any credentials,
|
||||
// then assume a 'password' credential type
|
||||
if len(i.Credentials) == 0 {
|
||||
return MockSessionCreateHandlerWithIdentityAndAMR(t, reg, i, []identity.CredentialsType{"password"})
|
||||
}
|
||||
|
||||
// otherwise, mock session with appropriate credential types
|
||||
for _, c := range i.Credentials {
|
||||
ct = append(ct, c.Type)
|
||||
}
|
||||
|
||||
return MockSessionCreateHandlerWithIdentityAndAMR(t, reg, i, ct)
|
||||
}
|
||||
|
||||
// mockMethodAAL maps a credential type to the AAL a successful login
|
||||
// through that method contributes. Mirrors the defaults the real
|
||||
// strategies pass to CompletedLoginFor, so tests that only specify the
|
||||
// method name get a realistic session AAL.
|
||||
func mockMethodAAL(m identity.CredentialsType) identity.AuthenticatorAssuranceLevel {
|
||||
switch m {
|
||||
case identity.CredentialsTypeTOTP,
|
||||
identity.CredentialsTypeWebAuthn,
|
||||
identity.CredentialsTypeLookup:
|
||||
return identity.AuthenticatorAssuranceLevel2
|
||||
default:
|
||||
return identity.AuthenticatorAssuranceLevel1
|
||||
}
|
||||
}
|
||||
|
||||
func MockSessionCreateHandlerWithIdentityAndAMR(t *testing.T, reg mockDeps, i *identity.Identity, methods []identity.CredentialsType) (http.HandlerFunc, *session.Session) {
|
||||
var sess session.Session
|
||||
require.NoError(t, faker.FakeData(&sess))
|
||||
// require AuthenticatedAt to be time.Now() as we always compare it to the current time
|
||||
sess.AuthenticatedAt = time.Now().UTC()
|
||||
sess.IssuedAt = time.Now().UTC()
|
||||
sess.ExpiresAt = time.Now().UTC().Add(time.Hour * 24)
|
||||
sess.Active = true
|
||||
|
||||
for _, m := range methods {
|
||||
if m == identity.CredentialsTypeOIDC {
|
||||
if c, ok := i.Credentials[m]; ok {
|
||||
var target identity.CredentialsOIDC
|
||||
if err := json.Unmarshal(c.Config, &target); err == nil {
|
||||
for _, t := range target.Providers {
|
||||
sess.CompletedLoginForWithProvider(c.Type, identity.AuthenticatorAssuranceLevel1, t.Provider, "")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
sess.CompletedLoginFor(m, mockMethodAAL(m))
|
||||
}
|
||||
|
||||
sess.SetAuthenticatorAssuranceLevel()
|
||||
|
||||
if _, err := reg.Config().DefaultIdentityTraitsSchemaURL(t.Context()); err != nil {
|
||||
SetDefaultIdentitySchema(reg.Config(), "file://./stub/fake-session.schema.json")
|
||||
}
|
||||
|
||||
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(t.Context(), i))
|
||||
|
||||
inserted, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(t.Context(), i.ID)
|
||||
require.NoError(t, err)
|
||||
sess.Identity = inserted
|
||||
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(t.Context(), &sess))
|
||||
require.Len(t, inserted.Credentials, len(i.Credentials))
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
require.NoError(t, reg.SessionManager().IssueCookie(t.Context(), w, r, &sess))
|
||||
}, &sess
|
||||
}
|
||||
|
||||
func MockSessionCreateHandler(t *testing.T, reg mockDeps) (http.HandlerFunc, *session.Session) {
|
||||
return MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{
|
||||
ID: x.NewUUID(), State: identity.StateActive, Traits: identity.Traits(`{"baz":"bar","foo":true,"bar":2.5}`),
|
||||
})
|
||||
}
|
||||
166
pkg/testhelpers/http.go
Normal file
166
pkg/testhelpers/http.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func NewDebugClient(t *testing.T) *http.Client {
|
||||
return &http.Client{Transport: NewTransportWithLogger(NewTestTransport(t), t)}
|
||||
}
|
||||
|
||||
func NewClientWithCookieJar(t *testing.T, jar *cookiejar.Jar, checkRedirect CheckRedirectFunc) *http.Client {
|
||||
if jar == nil {
|
||||
j, err := cookiejar.New(nil)
|
||||
jar = j
|
||||
require.NoError(t, err)
|
||||
}
|
||||
if checkRedirect == nil {
|
||||
checkRedirect = DebugRedirects(t)
|
||||
}
|
||||
return &http.Client{
|
||||
Jar: jar,
|
||||
Transport: NewTestTransport(t),
|
||||
CheckRedirect: checkRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
type CheckRedirectFunc func(req *http.Request, via []*http.Request) error
|
||||
|
||||
func DebugRedirects(t *testing.T) CheckRedirectFunc {
|
||||
return func(req *http.Request, via []*http.Request) error {
|
||||
t.Logf("Redirect: %s", req.URL.String())
|
||||
|
||||
if len(via) >= 20 {
|
||||
for k, v := range via {
|
||||
t.Logf("Failed with redirect (%d): %s", k, v.URL.String())
|
||||
}
|
||||
return errors.New("stopped after 20 redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewPostRequest(t *testing.T, isAPI bool, url string, payload io.Reader) *http.Request {
|
||||
req, err := http.NewRequest("POST", url, payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "text/html")
|
||||
if isAPI {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func NewHTTPGetJSONRequest(t *testing.T, url string) *http.Request {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// req.Header.Set("Content-Type", "application/json;charset=utf-8")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func NewHTTPGetAJAXRequest(t *testing.T, url string) *http.Request {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func NewHTTPDeleteJSONRequest(t *testing.T, url string, in interface{}) *http.Request {
|
||||
var body bytes.Buffer
|
||||
require.NoError(t, json.NewEncoder(&body).Encode(in))
|
||||
req, err := http.NewRequest("DELETE", url, &body)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func HTTPRequestJSON(t *testing.T, client *http.Client, method string, url string, in interface{}) ([]byte, *http.Response) {
|
||||
var body bytes.Buffer
|
||||
require.NoError(t, json.NewEncoder(&body).Encode(in))
|
||||
|
||||
req, err := http.NewRequest(method, url, &body)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
payload, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, res
|
||||
}
|
||||
|
||||
func HTTPPostForm(t *testing.T, client *http.Client, remote string, in *url.Values) ([]byte, *http.Response) {
|
||||
if in == nil {
|
||||
in = new(url.Values)
|
||||
}
|
||||
|
||||
res, err := client.PostForm(remote, *in)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
payload, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, res
|
||||
}
|
||||
|
||||
func NewTestHTTPRequest(t *testing.T, method, url string, body io.Reader) *http.Request {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
require.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
func EasyGet(t *testing.T, c *http.Client, url string) (*http.Response, []byte) {
|
||||
res, err := c.Get(url)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
return res, body
|
||||
}
|
||||
|
||||
func EasyGetJSON(t *testing.T, c *http.Client, url string) (*http.Response, []byte) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, err := c.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
return res, body
|
||||
}
|
||||
|
||||
func EasyGetBody(t *testing.T, c *http.Client, url string) []byte {
|
||||
_, body := EasyGet(t, c, url) // nolint: bodyclose
|
||||
return body
|
||||
}
|
||||
|
||||
func EasyCookieJar(t *testing.T, o *cookiejar.Options) *cookiejar.Jar {
|
||||
cj, err := cookiejar.New(o)
|
||||
require.NoError(t, err)
|
||||
return cj
|
||||
}
|
||||
16
pkg/testhelpers/httptest.go
Normal file
16
pkg/testhelpers/httptest.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func NewHTTPTestServer(t *testing.T, h http.Handler) *httptest.Server {
|
||||
ts := httptest.NewServer(h)
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
26
pkg/testhelpers/identity.go
Normal file
26
pkg/testhelpers/identity.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/session"
|
||||
)
|
||||
|
||||
func CreateSession(t *testing.T, reg driver.Registry) *session.Session {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
|
||||
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
|
||||
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(req.Context(), i))
|
||||
sess, err := NewActiveSession(req, reg, i, time.Now().UTC(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.SessionPersister().UpsertSession(req.Context(), sess))
|
||||
return sess
|
||||
}
|
||||
16
pkg/testhelpers/identity_schema.go
Normal file
16
pkg/testhelpers/identity_schema.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"github.com/ory/kratos/driver/config"
|
||||
)
|
||||
|
||||
func IdentitySchemasConfig(schemas map[string]string) map[string]any {
|
||||
var s []config.Schema
|
||||
for id, location := range schemas {
|
||||
s = append(s, config.Schema{ID: id, URL: location})
|
||||
}
|
||||
return map[string]any{config.ViperKeyIdentitySchemas: s}
|
||||
}
|
||||
26
pkg/testhelpers/json.go
Normal file
26
pkg/testhelpers/json.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func JSONEq(t *testing.T, expected, actual interface{}, messageAndArgs ...interface{}) {
|
||||
var eb, ab bytes.Buffer
|
||||
require.NoError(t, json.NewEncoder(&eb).Encode(expected))
|
||||
require.NoError(t, json.NewEncoder(&ab).Encode(actual))
|
||||
assert.JSONEq(t, eb.String(), ab.String(), messageAndArgs...)
|
||||
}
|
||||
|
||||
func LogJSON(t *testing.T, v interface{}) {
|
||||
out, err := json.MarshalIndent(v, "", " ")
|
||||
require.NoError(t, err)
|
||||
t.Logf("\n%s\n---", out)
|
||||
}
|
||||
106
pkg/testhelpers/mailhog.go
Normal file
106
pkg/testhelpers/mailhog.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/pat"
|
||||
"github.com/ian-kent/go-log/levels"
|
||||
"github.com/ian-kent/go-log/log"
|
||||
"github.com/mailhog/MailHog-Server/api"
|
||||
mailhogconf "github.com/mailhog/MailHog-Server/config"
|
||||
"github.com/mailhog/MailHog-Server/monkey"
|
||||
"github.com/mailhog/MailHog-Server/smtp"
|
||||
"github.com/mailhog/data"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
xmailhog "github.com/ory/x/mailhog"
|
||||
)
|
||||
|
||||
// StartMailhog starts a MailHog server for testing purposes.
|
||||
// It returns the SMTP connection URL and the API URL.
|
||||
// If withChaosMonkey is true, the SMTP server will randomly drop connections and simulate network issues.
|
||||
func StartMailhog(t testing.TB, withChaosMonkey bool) (smtpAddr, apiAddr string) {
|
||||
t.Helper()
|
||||
|
||||
// hacky but should silence most MailHog logs during tests
|
||||
log.Logger().SetLevel(levels.FATAL)
|
||||
stdlog.Default().SetOutput(io.Discard)
|
||||
|
||||
apiconf := &mailhogconf.Config{
|
||||
Storage: xmailhog.NewInMemory(),
|
||||
MessageChan: make(chan *data.Message),
|
||||
}
|
||||
if withChaosMonkey {
|
||||
jim := &monkey.Jim{
|
||||
DisconnectChance: 0.005,
|
||||
AcceptChance: 0.99,
|
||||
LinkSpeedAffect: 0.05,
|
||||
LinkSpeedMin: 1250,
|
||||
LinkSpeedMax: 12500,
|
||||
RejectAuthChance: 0.05,
|
||||
|
||||
// important: set to 0 to avoid flakes in tests, because those errors are not retryable
|
||||
RejectSenderChance: 0,
|
||||
RejectRecipientChance: 0,
|
||||
}
|
||||
jim.Configure(t.Logf)
|
||||
apiconf.Monkey = jim
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
go serveSMTP(t.Context(), ln, apiconf)
|
||||
|
||||
r := pat.New()
|
||||
api.CreateAPI(apiconf, r)
|
||||
s := httptest.NewServer(r)
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
return fmt.Sprintf("smtp://%s?disable_starttls=true", ln.Addr().String()), s.URL
|
||||
}
|
||||
|
||||
func serveSMTP(ctx context.Context, ln net.Listener, cfg *mailhogconf.Config) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
conn, err := ln.Accept()
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("[SMTP] Error accepting connection: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if cfg.Monkey != nil {
|
||||
ok := cfg.Monkey.Accept(conn)
|
||||
if !ok {
|
||||
_ = conn.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
go smtp.Accept(
|
||||
conn.(*net.TCPConn).RemoteAddr().String(),
|
||||
io.ReadWriteCloser(conn),
|
||||
cfg.Storage,
|
||||
cfg.MessageChan,
|
||||
cfg.Hostname,
|
||||
cfg.Monkey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
pkg/testhelpers/network.go
Normal file
45
pkg/testhelpers/network.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/persistence"
|
||||
"github.com/ory/x/networkx"
|
||||
)
|
||||
|
||||
func NewNetworkUnlessExisting(t *testing.T, ctx context.Context, p persistence.Persister) (uuid.UUID, persistence.Persister) {
|
||||
if n, ok := ctx.Value("network").(*networkx.Network); ok && n != nil {
|
||||
return n.ID, p.WithNetworkID(n.ID)
|
||||
}
|
||||
|
||||
n := networkx.NewNetwork()
|
||||
require.NoError(t, p.GetConnection(ctx).Create(n))
|
||||
return n.ID, p.WithNetworkID(n.ID)
|
||||
}
|
||||
|
||||
func NewNetwork(t testing.TB, ctx context.Context, p persistence.Persister) (uuid.UUID, persistence.Persister) {
|
||||
SkipIfNetworkContext(t, ctx)
|
||||
|
||||
n := networkx.NewNetwork()
|
||||
require.NoError(t, p.GetConnection(context.Background()).Create(n))
|
||||
return n.ID, p.WithNetworkID(n.ID)
|
||||
}
|
||||
|
||||
func ExistingNetwork(t *testing.T, p persistence.Persister, id uuid.UUID) persistence.Persister {
|
||||
require.NoError(t, p.GetConnection(context.Background()).Save(&networkx.Network{ID: id}))
|
||||
return p.WithNetworkID(id)
|
||||
}
|
||||
|
||||
func SkipIfNetworkContext(t testing.TB, ctx context.Context) {
|
||||
a, b := ctx.Value("network").(*networkx.Network)
|
||||
if a != nil && b {
|
||||
t.Skip("Network was set in context")
|
||||
}
|
||||
}
|
||||
111
pkg/testhelpers/sdk.go
Normal file
111
pkg/testhelpers/sdk.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
|
||||
"github.com/ory/kratos/ui/node"
|
||||
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
)
|
||||
|
||||
func NewSDKClient(ts *httptest.Server) *kratos.APIClient {
|
||||
return NewSDKClientFromURL(ts.URL)
|
||||
}
|
||||
|
||||
func NewSDKCustomClient(ts *httptest.Server, client *http.Client) *kratos.APIClient {
|
||||
conf := kratos.NewConfiguration()
|
||||
conf.Servers = kratos.ServerConfigurations{{URL: ts.URL}}
|
||||
conf.HTTPClient = client
|
||||
return kratos.NewAPIClient(conf)
|
||||
}
|
||||
|
||||
func NewSDKClientFromURL(u string) *kratos.APIClient {
|
||||
conf := kratos.NewConfiguration()
|
||||
conf.Servers = kratos.ServerConfigurations{{URL: u}}
|
||||
return kratos.NewAPIClient(conf)
|
||||
}
|
||||
|
||||
func SDKFormFieldsToURLValues(ff []kratos.UiNode) url.Values {
|
||||
values := url.Values{}
|
||||
for _, f := range ff {
|
||||
attr := f.Attributes.UiNodeInputAttributes
|
||||
if attr == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
val := attr.Value
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
values.Set(attr.Name, fmt.Sprintf("%v", v))
|
||||
case string:
|
||||
values.Set(attr.Name, fmt.Sprintf("%v", v))
|
||||
case float32:
|
||||
values.Set(attr.Name, fmt.Sprintf("%v", v))
|
||||
case float64:
|
||||
values.Set(attr.Name, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func NewFakeCSRFNode() *kratos.UiNode {
|
||||
return &kratos.UiNode{
|
||||
Group: node.DefaultGroup.String(),
|
||||
Type: "input",
|
||||
Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{
|
||||
Name: "csrf_token",
|
||||
Required: new(true),
|
||||
Type: "hidden",
|
||||
Value: nosurfx.FakeCSRFToken,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func NewSDKEmailNode(group string) *kratos.UiNode {
|
||||
return &kratos.UiNode{
|
||||
Type: "input",
|
||||
Group: group,
|
||||
Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{
|
||||
Name: "email",
|
||||
Type: "email",
|
||||
Required: new(true),
|
||||
Value: "email",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func NewMethodSubmit(group, value string) *kratos.UiNode {
|
||||
return &kratos.UiNode{
|
||||
Type: "input",
|
||||
Group: group,
|
||||
Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{
|
||||
Name: "method",
|
||||
Type: "submit",
|
||||
Value: value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPasswordNode() *kratos.UiNode {
|
||||
return &kratos.UiNode{
|
||||
Type: "input",
|
||||
Group: node.PasswordGroup.String(),
|
||||
Attributes: kratos.UiNodeInputAttributesAsUiNodeAttributes(&kratos.UiNodeInputAttributes{
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
Required: new(true),
|
||||
}),
|
||||
}
|
||||
}
|
||||
256
pkg/testhelpers/selfservice.go
Normal file
256
pkg/testhelpers/selfservice.go
Normal file
@@ -0,0 +1,256 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/go-faker/faker/v4"
|
||||
"github.com/gobuffalo/httptest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/selfservice/flow/login"
|
||||
"github.com/ory/kratos/selfservice/flow/registration"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
)
|
||||
|
||||
func TestSelfServicePreHook(
|
||||
configKey string,
|
||||
makeRequestPre func(t *testing.T, ts *httptest.Server) (*http.Response, string),
|
||||
newServer func(t *testing.T) *httptest.Server,
|
||||
conf *config.Config,
|
||||
) func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
return func(t *testing.T) {
|
||||
t.Run("case=pass without hooks", func(t *testing.T) {
|
||||
t.Cleanup(SelfServiceHookConfigReset(t, conf))
|
||||
|
||||
res, _ := makeRequestPre(t, newServer(t))
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("case=pass if hooks pass", func(t *testing.T) {
|
||||
t.Cleanup(SelfServiceHookConfigReset(t, conf))
|
||||
conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
|
||||
|
||||
res, _ := makeRequestPre(t, newServer(t))
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("case=err if hooks err", func(t *testing.T) {
|
||||
t.Cleanup(SelfServiceHookConfigReset(t, conf))
|
||||
conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPreHook": "err","ExecuteRegistrationPreHook": "err","ExecuteSettingsPreHook": "err","ExecuteVerificationPreHook": "err","ExecuteRecoveryPreHook": "err"}`)}})
|
||||
|
||||
res, body := makeRequestPre(t, newServer(t))
|
||||
assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode, "%s", body)
|
||||
assert.EqualValues(t, "err", body)
|
||||
})
|
||||
|
||||
t.Run("case=abort if hooks aborts", func(t *testing.T) {
|
||||
t.Cleanup(SelfServiceHookConfigReset(t, conf))
|
||||
conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginPreHook": "abort","ExecuteRegistrationPreHook": "abort","ExecuteSettingsPreHook": "abort","ExecuteVerificationPreHook": "abort","ExecuteRecoveryPreHook": "abort"}`)}})
|
||||
|
||||
res, body := makeRequestPre(t, newServer(t))
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
assert.Empty(t, body)
|
||||
})
|
||||
|
||||
t.Run("case=redirect", func(t *testing.T) {
|
||||
t.Skipf("Skipped because pre-redirect is no longer supported")
|
||||
|
||||
t.Cleanup(SelfServiceHookConfigReset(t, conf))
|
||||
conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "redirect", Config: []byte(`{"to": "https://www.ory.com/"}`)}})
|
||||
|
||||
res, _ := makeRequestPre(t, newServer(t))
|
||||
assert.EqualValues(t, http.StatusOK, res.StatusCode)
|
||||
assert.EqualValues(t, "https://www.ory.com/", res.Request.URL.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SelfServiceHookCreateFakeIdentity(t *testing.T, reg driver.Registry) *identity.Identity {
|
||||
i := SelfServiceHookFakeIdentity(t)
|
||||
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
|
||||
return i
|
||||
}
|
||||
|
||||
func SelfServiceHookFakeIdentity(t *testing.T) *identity.Identity {
|
||||
var i identity.Identity
|
||||
require.NoError(t, faker.FakeData(&i))
|
||||
i.Traits = identity.Traits(`{}`)
|
||||
i.State = identity.StateActive
|
||||
i.NID = uuid.Must(uuid.NewV4())
|
||||
return &i
|
||||
}
|
||||
|
||||
func SelfServiceHookConfigReset(t *testing.T, conf *config.Config) func() {
|
||||
ctx := context.Background()
|
||||
return func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter, nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter, nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter, nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter, nil)
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func SelfServiceHookSettingsSetDefaultRedirectTo(t *testing.T, conf *config.Config, value string) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+"."+config.DefaultBrowserReturnURL, value)
|
||||
}
|
||||
|
||||
func SelfServiceHookSettingsSetDefaultRedirectToStrategy(t *testing.T, conf *config.Config, strategy, value string) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+"."+strategy+"."+config.DefaultBrowserReturnURL, value)
|
||||
}
|
||||
|
||||
func SelfServiceHookLoginSetDefaultRedirectTo(t *testing.T, conf *config.Config, value string) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+"."+config.DefaultBrowserReturnURL, value)
|
||||
}
|
||||
|
||||
func SelfServiceHookLoginSetDefaultRedirectToStrategy(t *testing.T, conf *config.Config, strategy, value string) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+"."+strategy+"."+config.DefaultBrowserReturnURL, value)
|
||||
}
|
||||
|
||||
func SelfServiceHookRegistrationSetDefaultRedirectTo(t *testing.T, conf *config.Config, value string) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, value)
|
||||
}
|
||||
|
||||
func SelfServiceHookRegistrationSetDefaultRedirectToStrategy(t *testing.T, conf *config.Config, strategy, value string) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+"."+strategy+"."+config.DefaultBrowserReturnURL, value)
|
||||
}
|
||||
|
||||
func SelfServiceHookLoginViperSetPost(t *testing.T, conf *config.Config, strategy string, c []config.SelfServiceHook) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceLoginAfter, strategy), c)
|
||||
}
|
||||
|
||||
func SelfServiceHookRegistrationViperSetPost(t *testing.T, conf *config.Config, strategy string, c []config.SelfServiceHook) {
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, strategy), c)
|
||||
}
|
||||
|
||||
func SelfServiceHookLoginErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool {
|
||||
return SelfServiceHookErrorHandler(t, w, r, login.ErrHookAbortFlow, err)
|
||||
}
|
||||
|
||||
func SelfServiceHookRegistrationErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool {
|
||||
return SelfServiceHookErrorHandler(t, w, r, registration.ErrHookAbortFlow, err)
|
||||
}
|
||||
|
||||
func SelfServiceHookSettingsErrorHandler(t *testing.T, w http.ResponseWriter, r *http.Request, err error) bool {
|
||||
return SelfServiceHookErrorHandler(t, w, r, settings.ErrHookAbortFlow, err)
|
||||
}
|
||||
|
||||
func SelfServiceHookErrorHandler(t *testing.T, w http.ResponseWriter, _ *http.Request, abortErr error, actualErr error) bool {
|
||||
if actualErr != nil {
|
||||
t.Logf("received error: %+v", actualErr)
|
||||
if errors.Is(actualErr, abortErr) {
|
||||
return false
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(actualErr.Error()))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SelfServiceMakeLoginPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/login/pre", false, url.Values{})
|
||||
}
|
||||
|
||||
func SelfServiceMakeLoginPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/login/post", asAPI, query)
|
||||
}
|
||||
|
||||
func SelfServiceMakeRegistrationPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/registration/pre", false, url.Values{})
|
||||
}
|
||||
|
||||
func SelfServiceMakeSettingsPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/settings/pre", false, url.Values{})
|
||||
}
|
||||
|
||||
func SelfServiceMakeRecoveryPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/recovery/pre", false, url.Values{})
|
||||
}
|
||||
|
||||
func SelfServiceMakeVerificationPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/verification/pre", false, url.Values{})
|
||||
}
|
||||
|
||||
func SelfServiceMakeRegistrationPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/registration/post", asAPI, query)
|
||||
}
|
||||
|
||||
func SelfServiceMakeSettingsPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) {
|
||||
return SelfServiceMakeHookRequest(t, ts, "/settings/post", asAPI, query)
|
||||
}
|
||||
|
||||
func SelfServiceMakeHookRequest(t *testing.T, ts *httptest.Server, suffix string, asAPI bool, query url.Values) (*http.Response, string) {
|
||||
if len(query) > 0 {
|
||||
suffix += "?" + query.Encode()
|
||||
}
|
||||
req, err := http.NewRequest("GET", ts.URL+suffix, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Accept", "text/html")
|
||||
if asAPI {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
res, err := ts.Client().Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
return res, string(body)
|
||||
}
|
||||
|
||||
func GetSelfServiceRedirectLocation(t *testing.T, url string) string {
|
||||
c := &http.Client{
|
||||
Transport: NewTestTransport(t),
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
res, err := c.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
return res.Header.Get("Location")
|
||||
}
|
||||
|
||||
func AssertMessage[B string | []byte](t *testing.T, body B, message string) {
|
||||
t.Helper()
|
||||
require.Len(t, gjson.Get(string(body), "ui.messages").Array(), 1)
|
||||
assert.Equalf(t, message, gjson.Get(string(body), "ui.messages.0.text").String(), "%s", body)
|
||||
}
|
||||
|
||||
func AssertFieldMessage[B string | []byte](t *testing.T, body B, fieldName, message string) {
|
||||
t.Helper()
|
||||
messages := gjson.Get(string(body), "ui.nodes.#(attributes.name=="+fieldName+").messages")
|
||||
require.Len(t, messages.Array(), 1, "expected field %s to have one message, got %s", fieldName, messages)
|
||||
assert.Equalf(t, message, messages.Get("0.text").String(), "%s", body)
|
||||
}
|
||||
362
pkg/testhelpers/selfservice_login.go
Normal file
362
pkg/testhelpers/selfservice_login.go
Normal file
@@ -0,0 +1,362 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/x/urlx"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
"github.com/ory/kratos/selfservice/flow/login"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/ioutilx"
|
||||
)
|
||||
|
||||
func NewLoginUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
e, err := reg.LoginFlowPersister().GetLoginFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
reg.Writer().Write(w, r, e)
|
||||
}))
|
||||
ts.URL = strings.ReplaceAll(ts.URL, "127.0.0.1", "localhost")
|
||||
reg.Config().MustSet(t.Context(), config.ViperKeySelfServiceLoginUI, ts.URL+"/login-ts")
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
|
||||
func NewLoginUIWith401Response(t *testing.T, c *config.Config) *httptest.Server {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
ts.URL = strings.ReplaceAll(ts.URL, "127.0.0.1", "localhost")
|
||||
c.MustSet(t.Context(), config.ViperKeySelfServiceLoginUI, ts.URL+"/login-ts")
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
|
||||
type initFlowOptions struct {
|
||||
aal identity.AuthenticatorAssuranceLevel
|
||||
returnTo string
|
||||
refresh bool
|
||||
oauth2LoginChallenge string
|
||||
via string
|
||||
identitySchema string
|
||||
ctx context.Context
|
||||
expectActive *string
|
||||
}
|
||||
|
||||
func newInitFlowOptions(opts []InitFlowWithOption) *initFlowOptions {
|
||||
return new(initFlowOptions).apply(opts)
|
||||
}
|
||||
|
||||
func (o *initFlowOptions) apply(opts []InitFlowWithOption) *initFlowOptions {
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func getURLFromInitOptions(ts *httptest.Server, path string, forced bool, opts ...InitFlowWithOption) string {
|
||||
o := new(initFlowOptions).apply(opts)
|
||||
q := url.Values{}
|
||||
|
||||
if forced || o.refresh {
|
||||
q.Set("refresh", "true")
|
||||
}
|
||||
|
||||
if o.aal != "" {
|
||||
q.Set("aal", string(o.aal))
|
||||
}
|
||||
|
||||
if o.returnTo != "" {
|
||||
q.Set("return_to", string(o.returnTo))
|
||||
}
|
||||
|
||||
if o.oauth2LoginChallenge != "" {
|
||||
q.Set("login_challenge", o.oauth2LoginChallenge)
|
||||
}
|
||||
if o.via != "" {
|
||||
q.Set("via", o.via)
|
||||
}
|
||||
if o.identitySchema != "" {
|
||||
q.Set("identity_schema", o.identitySchema)
|
||||
}
|
||||
|
||||
u := urlx.ParseOrPanic(ts.URL + path)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
type InitFlowWithOption func(*initFlowOptions)
|
||||
|
||||
func InitFlowWithAAL(aal identity.AuthenticatorAssuranceLevel) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.aal = aal
|
||||
}
|
||||
}
|
||||
|
||||
func InitFlowWithReturnTo(returnTo string) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.returnTo = returnTo
|
||||
}
|
||||
}
|
||||
|
||||
func InitFlowWithRefresh() InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.refresh = true
|
||||
}
|
||||
}
|
||||
|
||||
func InitFlowWithContext(ctx context.Context) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
func InitFlowWithOAuth2LoginChallenge(hlc string) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.oauth2LoginChallenge = hlc
|
||||
}
|
||||
}
|
||||
|
||||
func InitFlowWithIdentitySchema(schema string) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.identitySchema = schema
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectActive(active string) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.expectActive = &active
|
||||
}
|
||||
}
|
||||
|
||||
// InitFlowWithVia sets the `via` query parameter which is used by the code MFA flows to determine the trait to use to send the code to the user
|
||||
func InitFlowWithVia(via string) InitFlowWithOption {
|
||||
return func(o *initFlowOptions) {
|
||||
o.via = via
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: use InitializeLoginFlowViaBrowserCtx instead.
|
||||
func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httptest.Server, forced, isSPA, expectInitError, expectGetError bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
|
||||
return InitializeLoginFlowViaBrowserCtx(t.Context(), t, client, ts, forced, isSPA, expectInitError, expectGetError, opts...)
|
||||
}
|
||||
|
||||
func InitializeLoginFlowViaBrowserCtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced, isSPA, expectInitError, expectGetError bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
req, err := http.NewRequest("GET", getURLFromInitOptions(ts, login.RouteInitBrowserFlow, forced, opts...), nil)
|
||||
require.NoError(t, err)
|
||||
o := newInitFlowOptions(opts)
|
||||
|
||||
if isSPA {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
|
||||
res, err := client.Do(req.WithContext(ctx))
|
||||
require.NoError(t, err)
|
||||
body := x.MustReadAll(res.Body)
|
||||
if isSPA {
|
||||
require.Truef(t, gjson.ValidBytes(body), "body is not valid JSON: %s", body)
|
||||
}
|
||||
require.NoError(t, res.Body.Close())
|
||||
require.Equal(t, 200, res.StatusCode, "%s", body)
|
||||
if expectInitError {
|
||||
require.NotNil(t, res.Request.URL)
|
||||
require.Contains(t, res.Request.URL.String(), "error-ts")
|
||||
}
|
||||
|
||||
flowID := res.Request.URL.Query().Get("flow")
|
||||
if isSPA {
|
||||
flowID = gjson.GetBytes(body, "id").String()
|
||||
}
|
||||
if !expectGetError {
|
||||
require.NotEmpty(t, flowID)
|
||||
}
|
||||
|
||||
rs, r, err := publicClient.FrontendAPI.GetLoginFlow(ctx).Id(flowID).Execute()
|
||||
if expectGetError {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, rs)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NoErrorf(t, err, "%s", ioutilx.MustReadAll(r.Body))
|
||||
assert.Equal(t, o.expectActive, rs.Active)
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func InitializeLoginFlowViaAPICtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
|
||||
return initializeLoginFlowViaAPI(ctx, t, client, ts, forced, false, opts...)
|
||||
}
|
||||
|
||||
func initializeLoginFlowViaAPI(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced bool, expectError bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
o := new(initFlowOptions).apply(opts)
|
||||
req := publicClient.FrontendAPI.CreateNativeLoginFlow(ctx).Refresh(forced)
|
||||
if o.aal != "" {
|
||||
req = req.Aal(string(o.aal))
|
||||
}
|
||||
if o.via != "" {
|
||||
req = req.Via(o.via)
|
||||
}
|
||||
if o.identitySchema != "" {
|
||||
req = req.IdentitySchema(o.identitySchema)
|
||||
}
|
||||
|
||||
rs, res, err := req.Execute()
|
||||
if expectError {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, rs)
|
||||
} else {
|
||||
require.NoError(t, err, "%s", ioutilx.MustReadAll(res.Body))
|
||||
assert.Equal(t, o.expectActive, rs.Active)
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
// Deprecated: use InitializeLoginFlowViaAPICtx instead.
|
||||
func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
|
||||
return InitializeLoginFlowViaAPICtx(t.Context(), t, client, ts, forced, opts...)
|
||||
}
|
||||
|
||||
func InitializeLoginFlowViaAPIExpectError(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
|
||||
return initializeLoginFlowViaAPI(ctx, t, client, ts, forced, true, opts...)
|
||||
}
|
||||
|
||||
// Deprecated: use LoginMakeRequestCtx instead.
|
||||
func LoginMakeRequest(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
f *kratos.LoginFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
opt ...RequestOption,
|
||||
) (string, *http.Response) {
|
||||
return LoginMakeRequestCtx(t.Context(), t, isAPI, isSPA, f, hc, values, opt...)
|
||||
}
|
||||
|
||||
type RequestOption func(*http.Request)
|
||||
|
||||
func WithHeader(key, value string) RequestOption {
|
||||
return func(r *http.Request) {
|
||||
r.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func LoginMakeRequestCtx(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
f *kratos.LoginFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
opt ...RequestOption,
|
||||
) (string, *http.Response) {
|
||||
require.NotEmpty(t, f.Ui.Action)
|
||||
|
||||
req := NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values))
|
||||
if isSPA && !isAPI {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
}
|
||||
for _, o := range opt {
|
||||
o(req)
|
||||
}
|
||||
|
||||
res, err := hc.Do(req.WithContext(ctx))
|
||||
require.NoError(t, err, "action: %s", f.Ui.Action)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
return string(ioutilx.MustReadAll(res.Body)), res
|
||||
}
|
||||
|
||||
func GetLoginFlow(t *testing.T, client *http.Client, ts *httptest.Server, flowID string) *kratos.LoginFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
rs, _, err := publicClient.FrontendAPI.GetLoginFlow(context.Background()).Id(flowID).Execute()
|
||||
require.NoError(t, err)
|
||||
return rs
|
||||
}
|
||||
|
||||
// Deprecated: use SubmitLoginFormCtx instead.
|
||||
func SubmitLoginForm(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
isSPA bool,
|
||||
forced bool,
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
opts ...InitFlowWithOption,
|
||||
) string {
|
||||
return SubmitLoginFormCtx(context.Background(), t, isAPI, hc, publicTS, withValues, isSPA, forced, expectedStatusCode, expectedURL, opts...)
|
||||
}
|
||||
|
||||
// SubmitLoginFormCtx initiates a login flow (for Browser and API!), fills out the form and modifies
|
||||
// the form values with `withValues`, and submits the form. Returns the body and checks for expectedStatusCode and
|
||||
// expectedURL on completion
|
||||
func SubmitLoginFormCtx(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
isSPA bool,
|
||||
forced bool,
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
opts ...InitFlowWithOption,
|
||||
) string {
|
||||
if hc == nil {
|
||||
hc = NewTestClient(t)
|
||||
if !isAPI {
|
||||
hc = NewClientWithCookies(t)
|
||||
}
|
||||
}
|
||||
|
||||
hc.Transport = NewTransportWithLogger(hc.Transport, t)
|
||||
var f *kratos.LoginFlow
|
||||
if isAPI {
|
||||
f = InitializeLoginFlowViaAPICtx(ctx, t, hc, publicTS, forced, opts...)
|
||||
} else {
|
||||
f = InitializeLoginFlowViaBrowserCtx(ctx, t, hc, publicTS, forced, isSPA, false, false, opts...)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out.
|
||||
|
||||
payload := SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
withValues(payload)
|
||||
b, res := LoginMakeRequestCtx(ctx, t, isAPI, isSPA, f, hc, EncodeFormAsJSON(t, isAPI, payload))
|
||||
assert.EqualValuesf(t, expectedStatusCode, res.StatusCode, "%s", b)
|
||||
assert.Containsf(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b)
|
||||
|
||||
t.Logf("%+v", res.Header)
|
||||
|
||||
return b
|
||||
}
|
||||
135
pkg/testhelpers/selfservice_recovery.go
Normal file
135
pkg/testhelpers/selfservice_recovery.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
"github.com/ory/kratos/selfservice/flow/verification"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/ioutilx"
|
||||
)
|
||||
|
||||
func NewVerificationUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server {
|
||||
ctx := context.Background()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
e, err := reg.VerificationFlowPersister().GetVerificationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
reg.Writer().Write(w, r, e)
|
||||
}))
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceVerificationUI, ts.URL+"/verification-ts")
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
|
||||
func GetVerificationFlow(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.VerificationFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
res, err := client.Get(ts.URL + verification.RouteInitBrowserFlow)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
rs, _, err := publicClient.FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
|
||||
require.NoErrorf(t, err, "%s", res.Request.URL.String())
|
||||
assert.NotEmpty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func InitializeVerificationFlowViaBrowser(t *testing.T, client *http.Client, isSPA bool, ts *httptest.Server) *kratos.VerificationFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
req, err := http.NewRequest("GET", ts.URL+verification.RouteInitBrowserFlow, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if isSPA {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if isSPA {
|
||||
var f kratos.VerificationFlow
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&f))
|
||||
return &f
|
||||
}
|
||||
|
||||
rs, _, err := publicClient.FrontendAPI.GetVerificationFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func InitializeVerificationFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.VerificationFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
rs, _, err := publicClient.FrontendAPI.CreateNativeVerificationFlow(context.Background()).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func VerificationMakeRequest(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
f *kratos.VerificationFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
) (string, *http.Response) {
|
||||
require.NotEmpty(t, f.Ui.Action)
|
||||
|
||||
res, err := hc.Do(NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values)))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
return string(ioutilx.MustReadAll(res.Body)), res
|
||||
}
|
||||
|
||||
// SubmitVerificationForm initiates a registration flow (for Browser and API!), fills out the form and modifies
|
||||
// the form values with `withValues`, and submits the form. If completed, it will return the flow as JSON.
|
||||
func SubmitVerificationForm(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
) string {
|
||||
var f *kratos.VerificationFlow
|
||||
if isAPI {
|
||||
f = InitializeVerificationFlowViaAPI(t, hc, publicTS)
|
||||
} else {
|
||||
f = InitializeVerificationFlowViaBrowser(t, hc, isSPA, publicTS)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out.
|
||||
|
||||
formPayload := SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
withValues(formPayload)
|
||||
|
||||
b, res := VerificationMakeRequest(t, isAPI || isSPA, f, hc, EncodeFormAsJSON(t, isAPI || isSPA, formPayload))
|
||||
assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b)
|
||||
assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b)
|
||||
|
||||
return b
|
||||
}
|
||||
202
pkg/testhelpers/selfservice_registration.go
Normal file
202
pkg/testhelpers/selfservice_registration.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/kratos/selfservice/flow/registration"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/x/assertx"
|
||||
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
|
||||
"github.com/ory/x/ioutilx"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/x"
|
||||
)
|
||||
|
||||
func NewRegistrationUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server {
|
||||
ctx := context.Background()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
e, err := reg.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
reg.Writer().Write(w, r, e)
|
||||
}))
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceRegistrationUI, ts.URL+"/registration-ts")
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
|
||||
// Deprecated: Use context-based InitializeRegistrationFlowViaBrowserCtx instead
|
||||
func InitializeRegistrationFlowViaBrowser(t *testing.T, client *http.Client, ts *httptest.Server, isSPA bool, expectInitError bool, expectGetError bool, opts ...InitFlowWithOption) *kratos.RegistrationFlow {
|
||||
return InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, client, ts, isSPA, expectInitError, expectGetError, opts...)
|
||||
}
|
||||
|
||||
func InitializeRegistrationFlowViaBrowserCtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, isSPA bool, expectInitError bool, expectGetError bool, opts ...InitFlowWithOption) *kratos.RegistrationFlow {
|
||||
req, err := http.NewRequest("GET", getURLFromInitOptions(ts, registration.RouteInitBrowserFlow, false, opts...), nil)
|
||||
require.NoError(t, err)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
if isSPA {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
body := x.MustReadAll(res.Body)
|
||||
if isSPA {
|
||||
require.True(t, gjson.ValidBytes(body), "body is not valid JSON: %s", string(body))
|
||||
}
|
||||
require.NoError(t, res.Body.Close())
|
||||
if expectInitError {
|
||||
require.Equal(t, 200, res.StatusCode)
|
||||
require.NotNil(t, res.Request.URL)
|
||||
require.Contains(t, res.Request.URL.String(), "error-ts")
|
||||
}
|
||||
|
||||
flowID := res.Request.URL.Query().Get("flow")
|
||||
if isSPA {
|
||||
flowID = gjson.GetBytes(body, "id").String()
|
||||
}
|
||||
|
||||
rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.GetRegistrationFlow(ctx).Id(flowID).Execute()
|
||||
if expectGetError {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, rs)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rs.Active)
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
func InitializeRegistrationFlowViaAPIExpectError(t *testing.T, client *http.Client, ts *httptest.Server, opts ...InitFlowWithOption) {
|
||||
o := new(initFlowOptions).apply(opts)
|
||||
|
||||
_, _, err := NewSDKCustomClient(ts, client).FrontendAPI.CreateNativeRegistrationFlow(context.Background()).IdentitySchema(o.identitySchema).Execute()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// Deprecated: Use context-based InitializeRegistrationFlowViaAPICtx instead
|
||||
func InitializeRegistrationFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, opts ...InitFlowWithOption) *kratos.RegistrationFlow {
|
||||
return InitializeRegistrationFlowViaAPICtx(context.Background(), t, client, ts, opts...)
|
||||
}
|
||||
|
||||
func InitializeRegistrationFlowViaAPICtx(ctx context.Context, t *testing.T, client *http.Client, ts *httptest.Server, opts ...InitFlowWithOption) *kratos.RegistrationFlow {
|
||||
o := new(initFlowOptions).apply(opts)
|
||||
|
||||
rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.CreateNativeRegistrationFlow(ctx).IdentitySchema(o.identitySchema).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rs.Active)
|
||||
return rs
|
||||
}
|
||||
|
||||
func GetRegistrationFlow(t *testing.T, client *http.Client, ts *httptest.Server, flowID string) *kratos.RegistrationFlow {
|
||||
rs, _, err := NewSDKCustomClient(ts, client).FrontendAPI.GetRegistrationFlow(context.Background()).Id(flowID).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rs.Active)
|
||||
return rs
|
||||
}
|
||||
|
||||
func RegistrationMakeRequest(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
f *kratos.RegistrationFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
) (string, *http.Response) {
|
||||
return RegistrationMakeRequestCtx(t.Context(), t, isAPI, isSPA, f, hc, values)
|
||||
}
|
||||
|
||||
func RegistrationMakeRequestCtx(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
f *kratos.RegistrationFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
) (string, *http.Response) {
|
||||
require.NotEmpty(t, f.Ui.Action)
|
||||
|
||||
req := NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values))
|
||||
if isSPA {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
res, err := hc.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
return string(ioutilx.MustReadAll(res.Body)), res
|
||||
}
|
||||
|
||||
// Deprecated: Use context-based SubmitRegistrationFormCtx instead
|
||||
func SubmitRegistrationForm(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
isSPA bool,
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
opts ...InitFlowWithOption,
|
||||
) string {
|
||||
return SubmitRegistrationFormCtx(context.Background(), t, isAPI, hc, publicTS, withValues, isSPA, expectedStatusCode, expectedURL, opts...)
|
||||
}
|
||||
|
||||
// SubmitRegistrationFormCtx (for Browser and API!), fills out the form and modifies
|
||||
// the form values with `withValues`, and submits the form. Returns the body and checks for expectedStatusCode and
|
||||
// expectedURL on completion
|
||||
func SubmitRegistrationFormCtx(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
isSPA bool,
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
opts ...InitFlowWithOption,
|
||||
) string {
|
||||
if hc == nil {
|
||||
hc = NewTestClient(t)
|
||||
}
|
||||
|
||||
hc.Transport = NewTransportWithLogger(hc.Transport, t)
|
||||
var payload *kratos.RegistrationFlow
|
||||
if isAPI {
|
||||
payload = InitializeRegistrationFlowViaAPICtx(ctx, t, hc, publicTS, opts...)
|
||||
} else {
|
||||
payload = InitializeRegistrationFlowViaBrowserCtx(ctx, t, hc, publicTS, isSPA, false, false, opts...)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out.
|
||||
|
||||
values := SDKFormFieldsToURLValues(payload.Ui.Nodes)
|
||||
withValues(values)
|
||||
b, res := RegistrationMakeRequest(t, isAPI, isSPA, payload, hc, EncodeFormAsJSON(t, isAPI, values))
|
||||
assert.EqualValues(t, expectedStatusCode, res.StatusCode, assertx.PrettifyJSONPayload(t, b))
|
||||
assert.Containsf(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, assertx.PrettifyJSONPayload(t, b))
|
||||
return b
|
||||
}
|
||||
276
pkg/testhelpers/selfservice_settings.go
Normal file
276
pkg/testhelpers/selfservice_settings.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/identity"
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
"github.com/ory/kratos/selfservice/flow/settings"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/x/ioutilx"
|
||||
"github.com/ory/x/urlx"
|
||||
)
|
||||
|
||||
func NewSettingsUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server {
|
||||
ctx := context.Background()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
e, err := reg.SettingsFlowPersister().GetSettingsFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
reg.Writer().Write(w, r, e)
|
||||
}))
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings-ts")
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
|
||||
func InitializeSettingsFlowViaBrowser(t *testing.T, client *http.Client, isSPA bool, ts *httptest.Server) *kratos.SettingsFlow {
|
||||
t.Helper()
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL+settings.RouteInitBrowserFlow, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if isSPA {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var flowID string
|
||||
if isSPA {
|
||||
flowID = gjson.GetBytes(x.MustReadAll(res.Body), "id").String()
|
||||
} else {
|
||||
flowID = res.Request.URL.Query().Get("flow")
|
||||
}
|
||||
|
||||
require.NoError(t, res.Body.Close())
|
||||
|
||||
rs, res, err := publicClient.FrontendAPI.GetSettingsFlow(context.Background()).Id(flowID).Execute()
|
||||
require.NoError(t, err, "%s", ioutilx.MustReadAll(res.Body))
|
||||
assert.Empty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func InitializeSettingsFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.SettingsFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
rs, _, err := publicClient.FrontendAPI.CreateNativeSettingsFlow(context.Background()).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func EncodeFormAsJSON(t *testing.T, isApi bool, values url.Values) (payload string) {
|
||||
if !isApi {
|
||||
return values.Encode()
|
||||
}
|
||||
payload = "{}"
|
||||
for k := range values {
|
||||
var err error
|
||||
payload, err = sjson.Set(payload, strings.ReplaceAll(k, ".", "\\."), values.Get(k))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func ExpectStatusCode(isAPI bool, api, browser int) int {
|
||||
if isAPI {
|
||||
return api
|
||||
}
|
||||
return browser
|
||||
}
|
||||
|
||||
func ExpectURL(isAPI bool, api, browser string) string {
|
||||
if isAPI {
|
||||
return api
|
||||
}
|
||||
return browser
|
||||
}
|
||||
|
||||
func NewSettingsUITestServer(t *testing.T, conf *config.Config) *httptest.Server {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /settings", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings")
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL+"/login")
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
func NewSettingsUIEchoServer(t *testing.T, reg *driver.RegistryDefault) *httptest.Server {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /settings", func(w http.ResponseWriter, r *http.Request) {
|
||||
res, err := reg.SettingsFlowPersister().GetSettingsFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
reg.Writer().Write(w, r, res)
|
||||
})
|
||||
|
||||
router.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
ctx := context.Background()
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, ts.URL+"/settings")
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceLoginUI, ts.URL+"/login")
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
func NewSettingsLoginAcceptAPIServer(t *testing.T, publicClient *kratos.APIClient, conf *config.Config) *httptest.Server {
|
||||
var called int
|
||||
loginTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, 0, called)
|
||||
called++
|
||||
|
||||
conf.MustSet(r.Context(), config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "5m")
|
||||
|
||||
res, _, err := publicClient.FrontendAPI.GetLoginFlow(context.Background()).Id(r.URL.Query().Get("flow")).Execute()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res.RequestUrl)
|
||||
|
||||
redir := urlx.ParseOrPanic(res.RequestUrl).Query().Get("return_to")
|
||||
t.Logf("Redirecting to: %s", redir)
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
}))
|
||||
t.Cleanup(func() {
|
||||
loginTS.Close()
|
||||
})
|
||||
ctx := context.Background()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, loginTS.URL+"/login")
|
||||
return loginTS
|
||||
}
|
||||
|
||||
// AddAndLoginIdentity adds the given identity to the store (like a registration flow) and returns an http.Client
|
||||
// which contain their session.
|
||||
func AddAndLoginIdentity(t *testing.T, reg *driver.RegistryDefault, i *identity.Identity) (uuid.UUID, *http.Client) {
|
||||
loggedIn := AddAndLoginIdentities(t, reg, map[string]*identity.Identity{"": i})[""]
|
||||
return loggedIn.ID, loggedIn.Client
|
||||
}
|
||||
|
||||
// AddAndLoginIdentities adds the given identities to the store (like a registration flow) and returns http.Clients
|
||||
// which contain their sessions.
|
||||
func AddAndLoginIdentities(t *testing.T, reg *driver.RegistryDefault, ids map[string]*identity.Identity) map[string]struct {
|
||||
Client *http.Client
|
||||
ID uuid.UUID
|
||||
} {
|
||||
result := map[string]struct {
|
||||
Client *http.Client
|
||||
ID uuid.UUID
|
||||
}{}
|
||||
r := http.NewServeMux()
|
||||
s := httptest.NewServer(r)
|
||||
defer s.Close()
|
||||
|
||||
for k, i := range ids {
|
||||
i.ID = uuid.Must(uuid.NewV4())
|
||||
tid := x.NewUUID().String()
|
||||
route, _ := MockSessionCreateHandlerWithIdentity(t, reg, i)
|
||||
location := "/sessions/set/" + tid
|
||||
|
||||
r.Handle("GET "+location, route)
|
||||
result[k] = struct {
|
||||
Client *http.Client
|
||||
ID uuid.UUID
|
||||
}{
|
||||
Client: NewSessionClient(t, s.URL+location),
|
||||
ID: i.ID,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func SettingsMakeRequest(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
f *kratos.SettingsFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
) (string, *http.Response) {
|
||||
require.NotEmpty(t, f.Ui.Action)
|
||||
|
||||
req := NewPostRequest(t, isSPA || isAPI, f.Ui.Action, bytes.NewBufferString(values))
|
||||
if isSPA || isAPI {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
if isSPA {
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
}
|
||||
|
||||
res, err := hc.Do(req)
|
||||
require.NoError(t, err, "action: %s", f.Ui.Action)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
return string(ioutilx.MustReadAll(res.Body)), res
|
||||
}
|
||||
|
||||
// SubmitSettingsForm initiates a settings flow (for Browser and API!), fills out the form and modifies
|
||||
// the form values with `withValues`, and submits the form. If completed, it will return the flow as JSON.
|
||||
func SubmitSettingsForm(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
) string {
|
||||
if hc == nil {
|
||||
hc = NewTestClient(t)
|
||||
if !isAPI {
|
||||
hc = NewClientWithCookies(t)
|
||||
}
|
||||
}
|
||||
|
||||
hc.Transport = NewTransportWithLogger(hc.Transport, t)
|
||||
var payload *kratos.SettingsFlow
|
||||
if isAPI {
|
||||
payload = InitializeSettingsFlowViaAPI(t, hc, publicTS)
|
||||
} else {
|
||||
payload = InitializeSettingsFlowViaBrowser(t, hc, isSPA, publicTS)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 10) // add a bit of delay to allow `1ns` to time out.
|
||||
|
||||
values := SDKFormFieldsToURLValues(payload.Ui.Nodes)
|
||||
withValues(values)
|
||||
|
||||
b, res := SettingsMakeRequest(t, isAPI, isSPA, payload, hc, EncodeFormAsJSON(t, isAPI || isSPA, values))
|
||||
assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b)
|
||||
assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b)
|
||||
|
||||
return b
|
||||
}
|
||||
239
pkg/testhelpers/selfservice_verification.go
Normal file
239
pkg/testhelpers/selfservice_verification.go
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
kratos "github.com/ory/kratos/pkg/httpclient"
|
||||
|
||||
"github.com/ory/x/ioutilx"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/recovery"
|
||||
"github.com/ory/kratos/x"
|
||||
)
|
||||
|
||||
func NewVerifyAfterHookWebHookTarget(ctx context.Context, t *testing.T, conf *config.Config, assert func(t *testing.T, body []byte)) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert(t, msg)
|
||||
}))
|
||||
before := conf.GetProvider(ctx).Get(config.ViperKeySelfServiceVerificationAfter + ".hooks")
|
||||
// A hook to ensure that the verification hook is called with the correct data
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{
|
||||
{
|
||||
"hook": "web_hook",
|
||||
"config": map[string]interface{}{
|
||||
"url": ts.URL,
|
||||
"method": "POST",
|
||||
"body": "base64://ZnVuY3Rpb24oY3R4KSB7CiAgICBpZGVudGl0eTogY3R4LmlkZW50aXR5Cn0=",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
t.Cleanup(ts.Close)
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", before)
|
||||
})
|
||||
}
|
||||
|
||||
func NewRecoveryAfterHookWebHookTarget(ctx context.Context, t *testing.T, conf *config.Config, assert func(t *testing.T, body []byte)) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert(t, msg)
|
||||
}))
|
||||
|
||||
// A hook to ensure that the recovery hook is called with the correct data
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{
|
||||
{
|
||||
"hook": "web_hook",
|
||||
"config": map[string]interface{}{
|
||||
"url": ts.URL,
|
||||
"method": "POST",
|
||||
"body": "base64://ZnVuY3Rpb24oY3R4KSB7CiAgICBpZGVudGl0eTogY3R4LmlkZW50aXR5Cn0=",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
t.Cleanup(ts.Close)
|
||||
t.Cleanup(func() {
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{})
|
||||
})
|
||||
}
|
||||
|
||||
func NewRecoveryUIFlowEchoServer(t *testing.T, reg driver.Registry) *httptest.Server {
|
||||
ctx := context.Background()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
e, err := reg.RecoveryFlowPersister().GetRecoveryFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
reg.Writer().Write(w, r, e)
|
||||
}))
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceRecoveryUI, ts.URL+"/recovery-ts")
|
||||
t.Cleanup(ts.Close)
|
||||
return ts
|
||||
}
|
||||
|
||||
func GetRecoveryFlowForType(t *testing.T, client *http.Client, ts *httptest.Server, ft flow.Type) *kratos.RecoveryFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
var url string
|
||||
switch ft {
|
||||
case flow.TypeBrowser:
|
||||
url = ts.URL + recovery.RouteInitBrowserFlow
|
||||
case flow.TypeAPI:
|
||||
url = ts.URL + recovery.RouteInitAPIFlow
|
||||
default:
|
||||
t.Errorf("unknown type: %s", ft)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
res, err := client.Get(url)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
var flowID string
|
||||
switch ft {
|
||||
case flow.TypeBrowser:
|
||||
flowID = res.Request.URL.Query().Get("flow")
|
||||
case flow.TypeAPI:
|
||||
flowID = gjson.GetBytes(ioutilx.MustReadAll(res.Body), "id").String()
|
||||
default:
|
||||
t.Errorf("unknown type: %s", ft)
|
||||
t.FailNow()
|
||||
}
|
||||
require.NotEmpty(t, flowID, "expected to receive a flow id, got none. %s", ioutilx.MustReadAll(res.Body))
|
||||
|
||||
rs, _, err := publicClient.FrontendAPI.GetRecoveryFlow(context.Background()).
|
||||
Id(flowID).
|
||||
Execute()
|
||||
require.NoError(t, err, "expected no error when fetching recovery flow: %s", err)
|
||||
assert.NotEmpty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func GetRecoveryFlow(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.RecoveryFlow {
|
||||
return GetRecoveryFlowForType(t, client, ts, flow.TypeBrowser)
|
||||
}
|
||||
|
||||
func InitializeRecoveryFlowViaBrowser(t *testing.T, client *http.Client, isSPA bool, ts *httptest.Server, values url.Values) *kratos.RecoveryFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
u := ts.URL + recovery.RouteInitBrowserFlow
|
||||
if values != nil {
|
||||
u += "?" + values.Encode()
|
||||
}
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if isSPA {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if isSPA {
|
||||
var f kratos.RecoveryFlow
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&f))
|
||||
return &f
|
||||
}
|
||||
|
||||
require.NoError(t, res.Body.Close())
|
||||
rs, _, err := publicClient.FrontendAPI.GetRecoveryFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func InitializeRecoveryFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server) *kratos.RecoveryFlow {
|
||||
publicClient := NewSDKCustomClient(ts, client)
|
||||
|
||||
rs, _, err := publicClient.FrontendAPI.CreateNativeRecoveryFlow(context.Background()).Execute()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rs.Active)
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func RecoveryMakeRequest(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
f *kratos.RecoveryFlow,
|
||||
hc *http.Client,
|
||||
values string,
|
||||
) (string, *http.Response) {
|
||||
require.NotEmpty(t, f.Ui.Action)
|
||||
|
||||
res, err := hc.Do(NewPostRequest(t, isAPI, f.Ui.Action, bytes.NewBufferString(values)))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
return string(ioutilx.MustReadAll(res.Body)), res
|
||||
}
|
||||
|
||||
// SubmitRecoveryForm initiates a registration flow (for Browser and API!), fills out the form and modifies
|
||||
// the form values with `withValues`, and submits the form. If completed, it will return the flow as JSON.
|
||||
func SubmitRecoveryForm(
|
||||
t *testing.T,
|
||||
isAPI bool,
|
||||
isSPA bool,
|
||||
hc *http.Client,
|
||||
publicTS *httptest.Server,
|
||||
withValues func(v url.Values),
|
||||
expectedStatusCode int,
|
||||
expectedURL string,
|
||||
) string {
|
||||
hc.Transport = NewTransportWithLogger(hc.Transport, t)
|
||||
var f *kratos.RecoveryFlow
|
||||
if isAPI {
|
||||
f = InitializeRecoveryFlowViaAPI(t, hc, publicTS)
|
||||
} else {
|
||||
f = InitializeRecoveryFlowViaBrowser(t, hc, isSPA, publicTS, nil)
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond) // add a bit of delay to allow `1ns` to time out.
|
||||
|
||||
formPayload := SDKFormFieldsToURLValues(f.Ui.Nodes)
|
||||
withValues(formPayload)
|
||||
|
||||
b, res := RecoveryMakeRequest(t, isAPI || isSPA, f, hc, EncodeFormAsJSON(t, isAPI || isSPA, formPayload))
|
||||
assert.EqualValues(t, expectedStatusCode, res.StatusCode, "%s", b)
|
||||
assert.Contains(t, res.Request.URL.String(), expectedURL, "%+v\n\t%s", res.Request, b)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func PersistNewRecoveryFlow(t *testing.T, strategies recovery.Strategies, conf *config.Config, reg *driver.RegistryDefault) *recovery.Flow {
|
||||
t.Helper()
|
||||
req := NewTestHTTPRequest(t, "GET", conf.SelfPublicURL(context.Background()).String()+"/test", nil)
|
||||
f, err := recovery.NewFlow(reg, conf.SelfServiceFlowRecoveryRequestLifespan(context.Background()), reg.GenerateCSRFToken(req), req, strategies, flow.TypeBrowser)
|
||||
require.NoError(t, err, "Expected no error when creating a new recovery flow: %s", err)
|
||||
|
||||
err = reg.RecoveryFlowPersister().CreateRecoveryFlow(context.Background(), f)
|
||||
require.NoError(t, err, "Expected no error when persisting a new recover flow: %s", err)
|
||||
return f
|
||||
}
|
||||
84
pkg/testhelpers/server.go
Normal file
84
pkg/testhelpers/server.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/negroni"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
"github.com/ory/x/httprouterx"
|
||||
)
|
||||
|
||||
func NewKratosServer(t *testing.T, reg driver.Registry) (public, admin *httptest.Server) {
|
||||
return NewKratosServerWithRouters(t, reg, httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix())
|
||||
}
|
||||
|
||||
func NewKratosServerWithCSRF(t *testing.T, reg driver.Registry) (public, admin *httptest.Server) {
|
||||
public, admin, _, _ = NewKratosServerWithCSRFAndRouters(t, reg)
|
||||
return
|
||||
}
|
||||
|
||||
func NewKratosServerWithCSRFAndRouters(t *testing.T, reg driver.Registry) (public, admin *httptest.Server, rp *httprouterx.RouterPublic, ra *httprouterx.RouterAdmin) {
|
||||
rp, ra = httprouterx.NewRouterPublic(), httprouterx.NewRouterAdminWithPrefix()
|
||||
csrfHandler := nosurfx.NewTestCSRFHandler(rp, reg)
|
||||
reg.WithCSRFHandler(csrfHandler)
|
||||
|
||||
ran := negroni.New()
|
||||
ran.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni)
|
||||
ran.UseHandler(ra)
|
||||
|
||||
rpn := negroni.New()
|
||||
rpn.UseFunc(x.HTTPLoaderContextMiddleware(reg))
|
||||
rpn.UseHandler(rp)
|
||||
|
||||
public = httptest.NewServer(nosurfx.NewTestCSRFHandler(rpn, reg))
|
||||
admin = httptest.NewServer(ran)
|
||||
|
||||
public.URL = strings.ReplaceAll(public.URL, "127.0.0.1", "localhost")
|
||||
|
||||
initKratosServers(t, reg, public, admin, rp, ra)
|
||||
|
||||
t.Cleanup(public.Close)
|
||||
t.Cleanup(admin.Close)
|
||||
return
|
||||
}
|
||||
|
||||
func NewKratosServerWithRouters(t *testing.T, reg driver.Registry, rp *httprouterx.RouterPublic, ra *httprouterx.RouterAdmin) (public, admin *httptest.Server) {
|
||||
np := negroni.New()
|
||||
np.UseFunc(httprouterx.TrimTrailingSlashNegroni)
|
||||
np.UseHandler(rp)
|
||||
|
||||
na := negroni.New()
|
||||
na.UseFunc(httprouterx.AddAdminPrefixIfNotPresentNegroni)
|
||||
na.UseFunc(httprouterx.TrimTrailingSlashNegroni)
|
||||
na.UseHandler(ra)
|
||||
|
||||
public = httptest.NewServer(np)
|
||||
admin = httptest.NewServer(na)
|
||||
|
||||
initKratosServers(t, reg, public, admin, rp, ra)
|
||||
|
||||
t.Cleanup(public.Close)
|
||||
t.Cleanup(admin.Close)
|
||||
return
|
||||
}
|
||||
|
||||
func initKratosServers(t *testing.T, reg driver.Registry, public, admin *httptest.Server, rp *httprouterx.RouterPublic, ra *httprouterx.RouterAdmin) {
|
||||
ctx := t.Context()
|
||||
if len(reg.Config().GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI)) == 0 {
|
||||
reg.Config().MustSet(ctx, config.ViperKeySelfServiceLoginUI, "http://NewKratosServerWithRouters/you-forgot-to-set-me/login")
|
||||
}
|
||||
reg.Config().MustSet(ctx, config.ViperKeyPublicBaseURL, public.URL)
|
||||
reg.Config().MustSet(ctx, config.ViperKeyAdminBaseURL, admin.URL)
|
||||
|
||||
reg.RegisterPublicRoutes(ctx, rp)
|
||||
reg.RegisterAdminRoutes(ctx, ra)
|
||||
}
|
||||
285
pkg/testhelpers/session.go
Normal file
285
pkg/testhelpers/session.go
Normal file
@@ -0,0 +1,285 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/session"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/nosurf"
|
||||
"github.com/ory/x/contextx"
|
||||
)
|
||||
|
||||
type SessionLifespanProvider struct {
|
||||
e time.Duration
|
||||
}
|
||||
|
||||
func (p *SessionLifespanProvider) SessionLifespan(context.Context) time.Duration {
|
||||
return p.e
|
||||
}
|
||||
|
||||
func NewSessionClient(t *testing.T, u string) *http.Client {
|
||||
c := NewClientWithCookies(t)
|
||||
MockHydrateCookieClient(t, c, u)
|
||||
return c
|
||||
}
|
||||
|
||||
func maybePersistSession(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) {
|
||||
id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID)
|
||||
if err != nil {
|
||||
require.NoError(t, sess.Identity.SetAvailableAAL(ctx, reg.IdentityManager()))
|
||||
require.NoError(t, reg.IdentityManager().Create(ctx, sess.Identity))
|
||||
id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
sess.Identity = id
|
||||
sess.IdentityID = id.ID
|
||||
|
||||
require.NoError(t, err, reg.SessionPersister().UpsertSession(ctx, sess))
|
||||
}
|
||||
|
||||
func NewHTTPClientWithSessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
|
||||
maybePersistSession(ctx, t, reg, sess)
|
||||
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
|
||||
})
|
||||
|
||||
if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok {
|
||||
handler = nosurf.New(handler)
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(handler)
|
||||
defer ts.Close()
|
||||
|
||||
c := NewClientWithCookies(t)
|
||||
MockHydrateCookieClient(t, c, ts.URL)
|
||||
return c
|
||||
}
|
||||
|
||||
func NewHTTPClientWithSessionCookieLocalhost(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
|
||||
maybePersistSession(ctx, t, reg, sess)
|
||||
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
|
||||
})
|
||||
|
||||
if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok {
|
||||
handler = nosurf.New(handler)
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(handler)
|
||||
defer ts.Close()
|
||||
|
||||
c := NewClientWithCookies(t)
|
||||
|
||||
ts.URL = strings.ReplaceAll(ts.URL, "127.0.0.1", "localhost")
|
||||
MockHydrateCookieClient(t, c, ts.URL)
|
||||
return c
|
||||
}
|
||||
|
||||
func NewNoRedirectHTTPClientWithSessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
|
||||
maybePersistSession(ctx, t, reg, sess)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
c := NewNoRedirectClientWithCookies(t)
|
||||
|
||||
MockHydrateCookieClient(t, c, ts.URL)
|
||||
return c
|
||||
}
|
||||
|
||||
func NewTransportWithLogger(parent http.RoundTripper, t *testing.T) *TransportWithLogger {
|
||||
return &TransportWithLogger{
|
||||
RoundTripper: parent,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
type TransportWithLogger struct {
|
||||
http.RoundTripper
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (ct *TransportWithLogger) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
ct.t.Logf("Made request to: %s", req.URL.String())
|
||||
if ct.RoundTripper == nil {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
return ct.RoundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
func NewHTTPClientWithSessionToken(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
|
||||
maybePersistSession(ctx, t, reg, sess)
|
||||
|
||||
return &http.Client{
|
||||
Transport: NewTransportWithHeader(t, http.Header{
|
||||
"Authorization": {"Bearer " + sess.Token},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func NewHTTPClientWithArbitrarySessionToken(ctx context.Context, t *testing.T, reg *driver.RegistryDefault) *http.Client {
|
||||
return NewHTTPClientWithArbitrarySessionTokenAndTraits(ctx, t, reg, nil)
|
||||
}
|
||||
|
||||
func NewHTTPClientWithArbitrarySessionTokenAndTraits(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, traits identity.Traits) *http.Client {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil).WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour))
|
||||
s, err := NewActiveSession(req, reg,
|
||||
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: traits, NID: x.NewUUID(), SchemaID: "default"},
|
||||
time.Now(),
|
||||
identity.CredentialsTypePassword,
|
||||
identity.AuthenticatorAssuranceLevel1,
|
||||
)
|
||||
require.NoError(t, err, "Could not initialize session from identity.")
|
||||
|
||||
return NewHTTPClientWithSessionToken(ctx, t, reg, s)
|
||||
}
|
||||
|
||||
func NewHTTPClientWithArbitrarySessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault) *http.Client {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
|
||||
req = req.WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour))
|
||||
id := x.NewUUID()
|
||||
s, err := NewActiveSession(req, reg,
|
||||
&identity.Identity{ID: id, State: identity.StateActive, Traits: []byte("{}"), Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{id.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)},
|
||||
}},
|
||||
time.Now(),
|
||||
identity.CredentialsTypePassword,
|
||||
identity.AuthenticatorAssuranceLevel1,
|
||||
)
|
||||
require.NoError(t, err, "Could not initialize session from identity.")
|
||||
|
||||
return NewHTTPClientWithSessionCookie(ctx, t, reg, s)
|
||||
}
|
||||
|
||||
func NewNoRedirectHTTPClientWithArbitrarySessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault) *http.Client {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
|
||||
req = req.WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour))
|
||||
id := x.NewUUID()
|
||||
s, err := NewActiveSession(req, reg,
|
||||
&identity.Identity{ID: id, State: identity.StateActive,
|
||||
Credentials: map[identity.CredentialsType]identity.Credentials{
|
||||
identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{id.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)},
|
||||
}},
|
||||
time.Now(),
|
||||
identity.CredentialsTypePassword,
|
||||
identity.AuthenticatorAssuranceLevel1,
|
||||
)
|
||||
require.NoError(t, err, "Could not initialize session from identity.")
|
||||
|
||||
return NewNoRedirectHTTPClientWithSessionCookie(ctx, t, reg, s)
|
||||
}
|
||||
|
||||
func NewHTTPClientWithIdentitySessionCookie(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
|
||||
req = req.WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour))
|
||||
s, err := NewActiveSession(req, reg,
|
||||
id,
|
||||
time.Now(),
|
||||
identity.CredentialsTypePassword,
|
||||
identity.AuthenticatorAssuranceLevel1,
|
||||
)
|
||||
require.NoError(t, err, "Could not initialize session from identity.")
|
||||
|
||||
return NewHTTPClientWithSessionCookie(ctx, t, reg, s)
|
||||
}
|
||||
|
||||
func NewHTTPClientWithIdentitySessionCookieLocalhost(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
|
||||
s, err := NewActiveSession(req, reg,
|
||||
id,
|
||||
time.Now(),
|
||||
identity.CredentialsTypePassword,
|
||||
identity.AuthenticatorAssuranceLevel1,
|
||||
)
|
||||
require.NoError(t, err, "Could not initialize session from identity.")
|
||||
|
||||
return NewHTTPClientWithSessionCookieLocalhost(ctx, t, reg, s)
|
||||
}
|
||||
|
||||
func NewHTTPClientWithIdentitySessionToken(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
|
||||
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
|
||||
req = req.WithContext(contextx.WithConfigValue(ctx, "session.lifespan", time.Hour))
|
||||
s, err := NewActiveSession(req, reg,
|
||||
id,
|
||||
time.Now(),
|
||||
identity.CredentialsTypePassword,
|
||||
identity.AuthenticatorAssuranceLevel1,
|
||||
)
|
||||
require.NoError(t, err, "Could not initialize session from identity.")
|
||||
|
||||
return NewHTTPClientWithSessionToken(ctx, t, reg, s)
|
||||
}
|
||||
|
||||
func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, methods ...string) {
|
||||
res, err := c.Get(ts.URL + session.RouteWhoami)
|
||||
require.NoError(t, err)
|
||||
sess := x.MustReadAll(res.Body)
|
||||
require.NoError(t, res.Body.Close())
|
||||
assert.EqualValues(t, aal, gjson.GetBytes(sess, "authenticator_assurance_level").String())
|
||||
for _, method := range methods {
|
||||
assert.EqualValues(t, method, gjson.GetBytes(sess, "authentication_methods.#(method=="+method+").method").String())
|
||||
}
|
||||
assert.Len(t, gjson.GetBytes(sess, "authentication_methods").Array(), 1+len(methods))
|
||||
}
|
||||
|
||||
func NewAuthorizedTransport(ctx context.Context, t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader {
|
||||
maybePersistSession(ctx, t, reg, sess)
|
||||
|
||||
return NewTransportWithHeader(t, http.Header{
|
||||
"Authorization": {"Bearer " + sess.Token},
|
||||
})
|
||||
}
|
||||
|
||||
func NewTransportWithHeader(t *testing.T, h http.Header) *TransportWithHeader {
|
||||
if t == nil {
|
||||
panic("This function is for testing use only.")
|
||||
}
|
||||
return &TransportWithHeader{
|
||||
RoundTripper: NewTestTransport(t),
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
type TransportWithHeader struct {
|
||||
http.RoundTripper
|
||||
h http.Header
|
||||
}
|
||||
|
||||
func (ct *TransportWithHeader) GetHeader() http.Header {
|
||||
return ct.h
|
||||
}
|
||||
|
||||
func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
for k := range ct.h {
|
||||
req.Header.Set(k, ct.h.Get(k))
|
||||
}
|
||||
return ct.RoundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
func AssertNoCSRFCookieInResponse(t *testing.T, r *http.Response) {
|
||||
found := false
|
||||
for _, c := range r.Cookies() {
|
||||
if strings.HasPrefix(c.Name, "csrf_token") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.False(t, found)
|
||||
}
|
||||
23
pkg/testhelpers/session_active.go
Normal file
23
pkg/testhelpers/session_active.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright © 2024 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/session"
|
||||
)
|
||||
|
||||
func NewActiveSession(r *http.Request, reg interface {
|
||||
session.ManagementProvider
|
||||
}, i *identity.Identity, authenticatedAt time.Time, completedLoginFor identity.CredentialsType, completedLoginAAL identity.AuthenticatorAssuranceLevel) (*session.Session, error) {
|
||||
s := session.NewInactiveSession()
|
||||
s.CompletedLoginFor(completedLoginFor, completedLoginAAL)
|
||||
if err := reg.SessionManager().ActivateSession(r, s, i, authenticatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
29
pkg/testhelpers/snapshot.go
Normal file
29
pkg/testhelpers/snapshot.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func SnapshotTExcept(t *testing.T, actual interface{}, except []string) {
|
||||
t.Helper()
|
||||
compare, err := json.MarshalIndent(actual, "", " ")
|
||||
require.NoError(t, err, "%+v", actual)
|
||||
for _, e := range except {
|
||||
compare, err = sjson.DeleteBytes(compare, e)
|
||||
require.NoError(t, err, "%s", e)
|
||||
}
|
||||
|
||||
cupaloy.New(
|
||||
cupaloy.CreateNewAutomatically(true),
|
||||
cupaloy.FailOnUpdate(true),
|
||||
cupaloy.SnapshotFileExtension(".json"),
|
||||
).SnapshotT(t, compare)
|
||||
}
|
||||
24
pkg/testhelpers/strategies.go
Normal file
24
pkg/testhelpers/strategies.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
)
|
||||
|
||||
// Deprecated: use MethodEnableConfig instead.
|
||||
func StrategyEnable(t *testing.T, c *config.Config, strategy string, enable bool) {
|
||||
ctx := context.Background()
|
||||
c.MustSet(ctx, fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, strategy), enable)
|
||||
}
|
||||
|
||||
func MethodEnableConfig[S ~string](method S, enable bool) map[string]any {
|
||||
return map[string]any{
|
||||
fmt.Sprintf("%s.%s.enabled", config.ViperKeySelfServiceStrategyConfig, method): enable,
|
||||
}
|
||||
}
|
||||
12
pkg/testhelpers/strategy_code.go
Normal file
12
pkg/testhelpers/strategy_code.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/kratos/selfservice/strategy/code"
|
||||
)
|
||||
|
||||
var CodeRegex = fmt.Sprintf(`(\d{%d})`, code.CodeLength)
|
||||
7
pkg/testhelpers/stub/fake-session.schema.json
Normal file
7
pkg/testhelpers/stub/fake-session.schema.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$id": "https://example.com/registration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
Reference in New Issue
Block a user