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

This commit is contained in:
upstream-mirror
2026-09-15 21:19:12 +05:00
commit c60cc7ca75
8092 changed files with 394160 additions and 0 deletions

91
pkg/testhelpers/config.go Normal file
View 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)},
})
}

View 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]
}

View 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
View 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
View 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)
}

View 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
View 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
}

View 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
}

View 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
}

View 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
View 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
View 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,
)
}
}
}

View 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
View 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),
}),
}
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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
View 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)
}

View 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
}

View 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)
}

View 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,
}
}

View 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)

View 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": {}
}