Files
upstream-ory-kratos/selfservice/hook/verify_new_address_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

607 lines
24 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package hook_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/pkg/errors"
"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/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"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/ui/container"
"github.com/ory/kratos/x"
"github.com/ory/x/contextx"
"github.com/ory/x/randx"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)
func TestVerifyNewAddress(t *testing.T) {
t.Parallel()
ctx := context.Background()
setup := func(t *testing.T) (context.Context, *hook.VerifyNewAddress, *driver.RegistryDefault) {
_, reg := pkg.NewFastRegistryWithMocks(t)
ctx := testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_single_email.schema.json")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/")
ctx = contextx.WithConfigValue(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
h := hook.NewVerifyNewAddress(reg)
return ctx, h, reg
}
newSettingsFlow := func(t *testing.T, i identity.Identity) *settings.Flow {
t.Helper()
now := time.Now().UTC()
return &settings.Flow{
ID: x.NewUUID(),
ExpiresAt: now.Add(time.Hour),
IssuedAt: now,
RequestURL: "http://foo.com/settings",
IdentityID: i.ID,
Identity: &i,
Type: flow.TypeBrowser,
State: flow.StateShowForm,
UI: &container.Container{Method: "POST"},
InternalContext: []byte("{}"),
}
}
verifyAllAddresses := func(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, i *identity.Identity) *identity.Identity {
t.Helper()
for idx := range i.VerifiableAddresses {
i.VerifiableAddresses[idx].Verified = true
verifiedAt := sqlxx.NullTime(time.Now())
i.VerifiableAddresses[idx].VerifiedAt = &verifiedAt
i.VerifiableAddresses[idx].Status = identity.VerifiableAddressStatusCompleted
require.NoError(t, reg.PrivilegedIdentityPool().UpdateVerifiableAddress(ctx, &i.VerifiableAddresses[idx]))
}
var err error
i, err = reg.PrivilegedIdentityPool().GetIdentity(ctx, i.ID, identity.ExpandDefault)
require.NoError(t, err)
return i
}
createIdentity := func(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, email string, verified bool) *identity.Identity {
t.Helper()
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"email":"` + email + `","name":"Test"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, i))
if verified {
i = verifyAllAddresses(t, ctx, reg, i)
}
return i
}
t.Run("case=no-op when address does not change", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
original := createIdentity(t, ctx, reg, "old@example.com", true)
f := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
proposed := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"old@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "old@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
},
}
sess := &session.Session{ID: x.NewUUID(), Identity: original, AuthenticatedAt: time.Now()}
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.NoError(t, err, "hook should be a no-op when address did not change")
})
t.Run("case=aborts flow when address changes", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
original := createIdentity(t, ctx, reg, "old@example.com", true)
f := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
proposed := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"new@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "new@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: original,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.Error(t, err)
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
// Settings flow should have a ContinueWith entry pointing to the verification flow.
require.NotEmpty(t, f.ContinueWith(), "settings flow should have ContinueWith items")
cw := f.ContinueWith()[0]
assert.IsType(t, &flow.ContinueWithVerificationUI{}, cw)
vfID := cw.(*flow.ContinueWithVerificationUI).Flow.ID
// A PendingTraitsChange record should exist.
ptc, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID)
require.NoError(t, err)
assert.Equal(t, identity.PendingTraitsChangeStatusPending, ptc.Status)
assert.Equal(t, "new@example.com", ptc.NewAddressValue)
assert.Equal(t, original.ID, ptc.IdentityID)
assert.Equal(t, identity.HashTraits(json.RawMessage(original.Traits)), ptc.OriginalTraitsHash)
require.True(t, ptc.SessionID.Valid, "pending change should record the session id")
assert.Equal(t, sess.ID, ptc.SessionID.UUID)
require.True(t, ptc.OriginSettingsFlowID.Valid, "pending traits change should record the originating settings flow ID")
assert.Equal(t, f.ID, ptc.OriginSettingsFlowID.UUID, "originating settings flow ID should match the flow the hook ran in")
// Verification flow should exist and be in StateEmailSent.
vf, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, vfID)
require.NoError(t, err)
assert.Equal(t, flow.StateEmailSent, vf.State)
// A verification code message should have been sent.
messages, err := reg.CourierPersister().NextMessages(ctx, 12)
require.NoError(t, err)
require.Len(t, messages, 1)
assert.Equal(t, "new@example.com", messages[0].Recipient)
})
t.Run("case=previous pending changes are deleted on new submission", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
original := createIdentity(t, ctx, reg, "old@example.com", true)
// First change: old@example.com -> new1@example.com
f1 := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f1))
proposed1 := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"new1@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "new1@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: original,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f1,
Identity: proposed1,
Session: sess,
})
require.True(t, errors.Is(err, settings.ErrHookAbortFlow))
vfID1 := f1.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID
ptc1, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID1)
require.NoError(t, err)
assert.Equal(t, "new1@example.com", ptc1.NewAddressValue)
// Drain courier messages from first change.
_, _ = reg.CourierPersister().NextMessages(ctx, 12)
// Second change: old@example.com -> new2@example.com
f2 := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f2))
proposed2 := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"new2@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "new2@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
},
}
w2 := httptest.NewRecorder()
err = h.ExecuteSettingsPrePersistHook(w2, r, settings.PostHookPrePersistExecutorParams{
Flow: f2,
Identity: proposed2,
Session: sess,
})
require.True(t, errors.Is(err, settings.ErrHookAbortFlow))
vfID2 := f2.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID
ptc2, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID2)
require.NoError(t, err)
assert.Equal(t, "new2@example.com", ptc2.NewAddressValue)
// The first pending change should have been deleted.
_, err = reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID1)
require.Error(t, err, "first pending change should have been deleted")
})
t.Run("case=correctly detects change with two email addresses", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_two_emails.schema.json")
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"email":"primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, i))
// Verify both addresses.
i = verifyAllAddresses(t, ctx, reg, i)
require.Len(t, i.VerifiableAddresses, 2, "identity should have two verifiable addresses")
// Change only the primary email; recovery email stays the same.
f := newSettingsFlow(t, *i)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
proposed := &identity.Identity{
ID: i.ID,
Traits: identity.Traits(`{"email":"new-primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "new-primary@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID},
{Value: "recovery@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: i,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
// Should abort with exactly one changed address (not two, not zero).
require.Error(t, err)
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
// Verify only one verification was triggered (for the changed address).
assert.Len(t, f.ContinueWith(), 1)
cw := f.ContinueWith()[0]
assert.IsType(t, &flow.ContinueWithVerificationUI{}, cw)
vfID := cw.(*flow.ContinueWithVerificationUI).Flow.ID
ptc, err := reg.PendingTraitsChangePersister().GetPendingTraitsChangeByVerificationFlow(ctx, vfID)
require.NoError(t, err)
assert.Equal(t, "new-primary@example.com", ptc.NewAddressValue, "pending change should be for the changed address only")
})
t.Run("case=no-op with two unchanged email addresses", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_two_emails.schema.json")
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"email":"primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, i))
i = verifyAllAddresses(t, ctx, reg, i)
f := newSettingsFlow(t, *i)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
proposed := &identity.Identity{
ID: i.ID,
Traits: identity.Traits(`{"email":"primary@example.com","recovery_email":"recovery@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "primary@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID},
{Value: "recovery@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID},
},
}
sess := &session.Session{ID: x.NewUUID(), Identity: i, AuthenticatedAt: time.Now()}
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.NoError(t, err, "hook should be a no-op when no addresses changed")
assert.Empty(t, f.ContinueWith())
})
t.Run("case=rejects change when new address is already owned by another identity", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
_ = createIdentity(t, ctx, reg, "existing@example.com", true)
original := createIdentity(t, ctx, reg, "other@example.com", true)
f := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
proposed := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"existing@example.com","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "existing@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: original,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
// Flow should carry the duplicate credentials error — no redirect to verification.
assert.Empty(t, f.ContinueWith(), "should not start a verification flow")
require.NotEmpty(t, f.UI.Messages)
assert.Contains(t, f.UI.Messages[0].Text, "exists already")
})
t.Run("case=rejects change when new phone number is already owned by another identity", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_email_and_phone.schema.json")
existing := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
existing.Traits = identity.Traits(`{"email":"phone-existing@example.com","phone":"+19999999999","name":"Existing"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, existing))
verifyAllAddresses(t, ctx, reg, existing)
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"phone-original@example.com","phone":"+10000000000","name":"Original"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, original))
original = verifyAllAddresses(t, ctx, reg, original)
f := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
// Proposed: email unchanged, phone changed to the existing's number.
proposed := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"phone-original@example.com","phone":"+19999999999","name":"Original"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "phone-original@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
{Value: "+19999999999", Via: identity.AddressTypeSMS, IdentityID: original.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: original,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
// Flow should carry the duplicate credentials error — no redirect to verification.
assert.Empty(t, f.ContinueWith(), "should not start a verification flow")
require.NotEmpty(t, f.UI.Messages)
assert.Contains(t, f.UI.Messages[0].Text, "exists already")
})
t.Run("case=returns error when multiple addresses change at once", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_email_and_phone.schema.json")
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"email":"old@example.com","phone":"+12345678901","name":"Test"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, i))
i = verifyAllAddresses(t, ctx, reg, i)
f := newSettingsFlow(t, *i)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
proposed := &identity.Identity{
ID: i.ID,
Traits: identity.Traits(`{"email":"new@example.com","phone":"+19876543210","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "new@example.com", Via: identity.AddressTypeEmail, IdentityID: i.ID},
{Value: "+19876543210", Via: "sms", IdentityID: i.ID},
},
}
sess := &session.Session{ID: x.NewUUID(), Identity: i, AuthenticatedAt: time.Now()}
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.Error(t, err)
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
// The flow should contain the too-many-address-changes error message.
require.NotEmpty(t, f.UI.Messages)
})
t.Run("case=SMS verification flow shows the phone code-sent message, not the email one", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/verify_email_and_phone.schema.json")
// Configure an SMS courier channel so the verification code can be queued for a phone number.
ctx = contextx.WithConfigValue(ctx, config.ViperKeyCourierChannels, []map[string]any{
{"id": "sms", "type": "http", "request_config": map[string]any{
"url": "http://localhost:1234/sms",
"method": "POST",
"body": "base64://ZnVuY3Rpb24oY3R4KSBjdHg=",
}},
})
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"sms-msg@example.com","phone":"+10000000000","name":"Test"}`)
require.NoError(t, reg.IdentityManager().Create(ctx, original))
original = verifyAllAddresses(t, ctx, reg, original)
f := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
// Proposed: email unchanged, phone changed to a new, unowned number.
const newPhone = "+19876543210"
proposed := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"sms-msg@example.com","phone":"` + newPhone + `","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: "sms-msg@example.com", Via: identity.AddressTypeEmail, IdentityID: original.ID},
{Value: newPhone, Via: identity.AddressTypeSMS, IdentityID: original.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: original,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
require.NotEmpty(t, f.ContinueWith(), "settings flow should redirect to verification")
vfID := f.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID
vf, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, vfID)
require.NoError(t, err)
// An SMS verification must show only the phone "code has been sent" message,
// never the email wording.
require.Len(t, vf.UI.Messages, 1, "expected exactly one code-sent message")
assert.Equal(t, text.InfoSelfServiceVerificationPhoneWithCodeSent, vf.UI.Messages[0].ID)
})
t.Run("case=link strategy verification flow does not claim a code was sent", func(t *testing.T) {
t.Parallel()
ctx, h, reg := setup(t)
// Use the legacy link strategy as the primary verification method. It
// delivers a link, not a code, and sets no "code sent" message.
ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true)
ctx = contextx.WithConfigValue(ctx, config.ViperKeySelfServiceVerificationUse, "link")
original := createIdentity(t, ctx, reg, "link-msg@example.com", true)
f := newSettingsFlow(t, *original)
require.NoError(t, reg.SettingsFlowPersister().CreateSettingsFlow(ctx, f))
const newEmail = "link-msg-new@example.com"
proposed := &identity.Identity{
ID: original.ID,
Traits: identity.Traits(`{"email":"` + newEmail + `","name":"Test"}`),
VerifiableAddresses: []identity.VerifiableAddress{
{Value: newEmail, Via: identity.AddressTypeEmail, IdentityID: original.ID},
},
}
sess := &session.Session{
ID: x.NewUUID(),
Identity: original,
Token: randx.MustString(12, randx.AlphaLowerNum),
LogoutToken: randx.MustString(12, randx.AlphaLowerNum),
AuthenticatedAt: time.Now(),
}
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
r := &http.Request{URL: urlx.ParseOrPanic("https://www.ory.sh/")}
r = r.WithContext(ctx)
w := httptest.NewRecorder()
err := h.ExecuteSettingsPrePersistHook(w, r, settings.PostHookPrePersistExecutorParams{
Flow: f,
Identity: proposed,
Session: sess,
})
require.True(t, errors.Is(err, settings.ErrHookAbortFlow), "expected ErrHookAbortFlow, got: %v", err)
require.NotEmpty(t, f.ContinueWith(), "settings flow should redirect to verification")
vfID := f.ContinueWith()[0].(*flow.ContinueWithVerificationUI).Flow.ID
vf, err := reg.VerificationFlowPersister().GetVerificationFlow(ctx, vfID)
require.NoError(t, err)
// A link was sent, not a code — the flow must not carry a "code has
// been sent" message.
require.Empty(t, vf.UI.Messages, "link strategy flows must not claim a code was sent")
})
}