Files
upstream-ory-kratos/selfservice/hook/verification_test.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

281 lines
11 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hook_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/tidwall/gjson"
"github.com/ory/kratos/courier"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/ui/node"
"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/pkg"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/hook"
"github.com/ory/kratos/session"
"github.com/ory/kratos/text"
"github.com/ory/kratos/x"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)
func TestVerifier(t *testing.T) {
ctx := context.Background()
u := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.com/")}
for _, tc := range []struct {
name string
execHook func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error
originalFlow func() interface {
flow.InternalContexter
flow.FlowWithContinueWith
}
}{
{
name: "login",
execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error {
return h.ExecuteLoginPostHook(
httptest.NewRecorder(), u, node.CodeGroup, f.(*login.Flow), &session.Session{ID: x.NewUUID(), Identity: i})
},
originalFlow: func() interface {
flow.InternalContexter
flow.FlowWithContinueWith
} {
return &login.Flow{RequestURL: "http://foo.com/login", RequestedAAL: "aal1"}
},
},
{
name: "registration",
execHook: func(h *hook.Verifier, i *identity.Identity, f flow.Flow) error {
return h.ExecutePostRegistrationPostPersistHook(
httptest.NewRecorder(), u, f.(*registration.Flow), &session.Session{ID: x.NewUUID(), Identity: i})
},
originalFlow: func() interface {
flow.InternalContexter
flow.FlowWithContinueWith
} {
return &registration.Flow{RequestURL: "http://foo.com/registration?after_verification_return_to=verification_callback"}
},
},
} {
t.Run("flow="+tc.name, func(t *testing.T) {
for _, enabled := range []bool{true, false} {
t.Run(fmt.Sprintf("legacy flag=%v", enabled), func(t *testing.T) {
t.Run("case=should send out emails for unverified addresses", func(t *testing.T) {
originalFlow := tc.originalFlow()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json")
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/")
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
conf.MustSet(ctx, config.ViperKeyUseLegacyShowVerificationUI, enabled)
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
h := hook.NewVerifier(reg)
require.NoError(t, tc.execHook(h, i, originalFlow))
assert.Lenf(t, gjson.GetBytes(originalFlow.GetInternalContext(), hook.InternalContextRegistrationVerificationFlow).Array(), 1, "%s", originalFlow.GetInternalContext())
var verificationID uuid.UUID
if enabled {
assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#v", originalFlow.ContinueWith())
assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"})
vf := originalFlow.ContinueWith()[0]
assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf)
verificationID = vf.(*flow.ContinueWithVerificationUI).Flow.ID
} else {
assert.Lenf(t, originalFlow.ContinueWith(), 0, "%#v", originalFlow.ContinueWith())
verificationID = uuid.FromStringOrNil(gjson.GetBytes(originalFlow.GetInternalContext(), hook.InternalContextRegistrationVerificationFlow+".id").String())
require.NotEqual(t, uuid.Nil, verificationID, "%s", originalFlow.GetInternalContext())
}
expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, verificationID)
require.NoError(t, err)
require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent)
require.NotEmpty(t, expectedVerificationFlow.SessionID.UUID)
require.NotEmpty(t, expectedVerificationFlow.IdentityID.UUID)
require.NotNil(t, expectedVerificationFlow.UI.Nodes.Find("email"))
require.Len(t, expectedVerificationFlow.UI.Messages, 1)
require.Equal(t, text.InfoSelfServiceVerificationEmailWithCodeSent, expectedVerificationFlow.UI.Messages[0].ID)
messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
require.NoError(t, err)
if enabled {
require.Len(t, messages, 2)
} else {
require.Len(t, messages, 1)
}
})
})
}
t.Run("case should skip already verified addresses", func(t *testing.T) {
t.Parallel()
originalFlow := tc.originalFlow()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json")
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/")
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
h := hook.NewVerifier(reg)
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh"]}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
for _, address := range i.VerifiableAddresses {
address.Verified = true
address.VerifiedAt = new(sqlxx.NullTime(time.Now()))
address.Status = identity.VerifiableAddressStatusCompleted
require.NoError(t, reg.Persister().UpdateVerifiableAddress(context.Background(), &address))
}
i, err := reg.PrivilegedIdentityPool().GetIdentity(ctx, i.ID, identity.ExpandDefault)
require.NoError(t, err)
require.NoError(t, tc.execHook(h, i, originalFlow))
assert.Empty(t, originalFlow.ContinueWith(), "%#ßv", originalFlow.ContinueWith())
})
})
}
t.Run("flow=login/case=does not run if aal is not 1", func(t *testing.T) {
t.Parallel()
_, reg := pkg.NewFastRegistryWithMocks(t)
h := hook.NewVerifier(reg)
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
f := &login.Flow{RequestedAAL: "aal2"}
require.NoError(t, h.ExecuteLoginPostHook(httptest.NewRecorder(), u, node.CodeGroup, f, &session.Session{ID: x.NewUUID(), Identity: i}))
messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
require.EqualError(t, err, "queue is empty")
require.Len(t, messages, 0)
})
t.Run("name=settings", func(t *testing.T) {
t.Parallel()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/verify.schema.json")
conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.com/")
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
conf.MustSet(ctx, config.ViperKeyUseLegacyShowVerificationUI, true)
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"emails":["foo@ory.sh","bar@ory.sh","baz@ory.sh"]}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
actual, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "foo@ory.sh")
require.NoError(t, err)
assert.EqualValues(t, "foo@ory.sh", actual.Value)
actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "bar@ory.sh")
require.NoError(t, err)
assert.EqualValues(t, "bar@ory.sh", actual.Value)
actual, err = reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "baz@ory.sh")
require.NoError(t, err)
assert.EqualValues(t, "baz@ory.sh", actual.Value)
verifiedAt := sqlxx.NullTime(time.Now())
actual.Status = identity.VerifiableAddressStatusCompleted
actual.Verified = true
actual.VerifiedAt = &verifiedAt
require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(context.Background(), actual))
i, err = reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandDefault)
require.NoError(t, err)
originalFlow := &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"}
h := hook.NewVerifier(reg)
require.NoError(t, h.ExecuteSettingsPostPersistHook(
httptest.NewRecorder(), u, settings.PostHookPostPersistExecutorParams{
Flow: originalFlow,
Updated: i,
Previous: i,
Session: &session.Session{ID: x.NewUUID(), Identity: i},
}))
assert.Lenf(t, originalFlow.ContinueWith(), 2, "%#ßv", originalFlow.ContinueWith())
assertContinueWithAddresses(t, originalFlow.ContinueWith(), []string{"foo@ory.sh", "bar@ory.sh"})
vf := originalFlow.ContinueWith()[0]
assert.IsType(t, &flow.ContinueWithVerificationUI{}, vf)
fView := vf.(*flow.ContinueWithVerificationUI).Flow
expectedVerificationFlow, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, fView.ID)
require.NoError(t, err)
require.Equal(t, expectedVerificationFlow.State, flow.StateEmailSent)
messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
require.NoError(t, err)
require.Len(t, messages, 2)
recipients := make([]string, len(messages))
for k, m := range messages {
recipients[k] = m.Recipient
}
assert.Contains(t, recipients, "foo@ory.sh")
assert.Contains(t, recipients, "bar@ory.sh")
// Email to baz@ory.sh is skipped because it is verified already.
assert.NotContains(t, recipients, "baz@ory.sh")
// these addresses will be marked as sent and won't be sent again by the settings hook
address1, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "foo@ory.sh")
require.NoError(t, err)
assert.EqualValues(t, identity.VerifiableAddressStatusSent, address1.Status)
address2, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.AddressTypeEmail, "bar@ory.sh")
require.NoError(t, err)
assert.EqualValues(t, identity.VerifiableAddressStatusSent, address2.Status)
originalFlow = &settings.Flow{RequestURL: "http://foo.com/settings?after_verification_return_to=verification_callback"}
require.NoError(t, h.ExecuteSettingsPostPersistHook(
httptest.NewRecorder(), u, settings.PostHookPostPersistExecutorParams{
Flow: originalFlow,
Updated: i,
Previous: i,
Session: &session.Session{ID: x.NewUUID(), Identity: i},
}))
assert.Emptyf(t, originalFlow.ContinueWith(), "%+v", originalFlow.ContinueWith())
require.NoError(t, err)
messages, err = reg.CourierPersister().NextMessages(context.Background(), 12)
require.EqualError(t, err, courier.ErrQueueEmpty.Error())
assert.Len(t, messages, 0)
})
}
func assertContinueWithAddresses(t *testing.T, cs []flow.ContinueWith, addresses []string) {
t.Helper()
require.Len(t, cs, len(addresses))
e := make([]string, len(cs))
for i, c := range cs {
require.IsType(t, &flow.ContinueWithVerificationUI{}, c)
fView := c.(*flow.ContinueWithVerificationUI).Flow
e[i] = fView.VerifiableAddress
}
require.ElementsMatch(t, addresses, e)
}