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
234 lines
8.3 KiB
Go
234 lines
8.3 KiB
Go
// 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}`),
|
|
})
|
|
}
|