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
356 lines
14 KiB
Go
356 lines
14 KiB
Go
// Copyright © 2025 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package profile_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tidwall/gjson"
|
|
|
|
"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/registration"
|
|
"github.com/ory/kratos/selfservice/strategy/oidc"
|
|
"github.com/ory/kratos/ui/node"
|
|
"github.com/ory/x/assertx"
|
|
"github.com/ory/x/contextx"
|
|
"github.com/ory/x/snapshotx"
|
|
)
|
|
|
|
func TestTwoStepRegistration(t *testing.T) {
|
|
ctx := context.Background()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
|
|
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
|
|
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true)
|
|
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true)
|
|
conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{
|
|
{
|
|
ID: "google",
|
|
Provider: "google",
|
|
ClientID: "1234",
|
|
ClientSecret: "1234",
|
|
},
|
|
}})
|
|
|
|
conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true)
|
|
conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "localhost")
|
|
conf.MustSet(ctx, config.ViperKeyPasskeyRPDisplayName, "localhost")
|
|
conf.MustSet(ctx, config.ViperKeyWebAuthnRPID, "localhost")
|
|
conf.MustSet(ctx, config.ViperKeyWebAuthnRPDisplayName, "localhost")
|
|
conf.MustSet(ctx, fmt.Sprintf("%s.%s.passwordless_enabled", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeCodeAuth), true)
|
|
|
|
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFlowStyle, "profile_first")
|
|
|
|
_ = testhelpers.NewErrorTestServer(t, reg)
|
|
publicTS, _ := testhelpers.NewKratosServer(t, reg)
|
|
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
|
|
ui := testhelpers.NewRegistrationUIFlowEchoServer(t, reg)
|
|
|
|
t.Run("initial form is populated with identity traits", func(t *testing.T) {
|
|
t.Run("type=browser", func(t *testing.T) {
|
|
client := testhelpers.NewClientWithCookies(t)
|
|
|
|
t.Run("empty_flow", func(t *testing.T) {
|
|
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false)
|
|
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
|
|
"1.attributes.value",
|
|
"8.attributes.nonce",
|
|
"8.attributes.src",
|
|
"10.attributes.value",
|
|
))
|
|
})
|
|
|
|
t.Run("case=multi-schema-empty_flow", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
})
|
|
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
|
|
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
|
|
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
|
|
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
|
|
})
|
|
|
|
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false, testhelpers.InitFlowWithIdentitySchema("not-default"))
|
|
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
|
|
"1.attributes.value",
|
|
"8.attributes.nonce",
|
|
"8.attributes.src",
|
|
"10.attributes.value",
|
|
))
|
|
})
|
|
|
|
t.Run("case=multi-schema-invalid-form-data", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
})
|
|
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
|
|
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
|
|
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
|
|
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
|
|
})
|
|
|
|
body := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
|
|
v.Set("traits.email", "invalidemail")
|
|
v.Set("traits.booly", "true")
|
|
v.Set("traits.numby", "1")
|
|
v.Set("traits.stringy", "string")
|
|
v.Set("traits.should_big_number", "1000000")
|
|
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
|
|
|
|
v.Set("method", "profile")
|
|
}, false, http.StatusOK, ui.URL, testhelpers.InitFlowWithIdentitySchema("not-default"))
|
|
|
|
fmt.Println(body)
|
|
|
|
require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body)
|
|
require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body)
|
|
})
|
|
|
|
t.Run("select_credentials", func(t *testing.T) {
|
|
res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
|
|
v.Set("traits.email", "browser-1@example.org")
|
|
v.Set("traits.booly", "true")
|
|
v.Set("traits.numby", "1")
|
|
v.Set("traits.stringy", "string")
|
|
v.Set("traits.should_big_number", "1000000")
|
|
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
|
|
|
|
v.Set("method", "profile")
|
|
}, false, http.StatusOK, ui.URL)
|
|
snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths(
|
|
"1.attributes.value",
|
|
"8.attributes.nonce",
|
|
"8.attributes.src",
|
|
"11.attributes.value",
|
|
))
|
|
})
|
|
|
|
t.Run("return_to_profile", func(t *testing.T) {
|
|
res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
|
|
v.Set("traits.email", "browser-1-1@example.org")
|
|
v.Set("traits.booly", "true")
|
|
v.Set("traits.numby", "1")
|
|
v.Set("traits.stringy", "string")
|
|
v.Set("traits.should_big_number", "1000000")
|
|
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
|
|
|
|
v.Set("screen", "previous")
|
|
}, false, http.StatusOK, ui.URL)
|
|
snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths(
|
|
"1.attributes.value",
|
|
"8.attributes.nonce",
|
|
"8.attributes.src",
|
|
"10.attributes.value",
|
|
))
|
|
})
|
|
|
|
t.Run("select_credentials_again", func(t *testing.T) {
|
|
res := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
|
|
v.Set("traits.email", "browser-1-1@example.org")
|
|
v.Set("method", "profile")
|
|
}, false, http.StatusOK, ui.URL)
|
|
snapshotx.SnapshotT(t, json.RawMessage(gjson.Get(res, "ui.nodes").Raw), snapshotx.ExceptPaths(
|
|
"1.attributes.value",
|
|
"8.attributes.nonce",
|
|
"8.attributes.src",
|
|
"11.attributes.value",
|
|
))
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestOneStepRegistration(t *testing.T) {
|
|
ctx := context.Background()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
|
|
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/")
|
|
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFlowStyle, "unified")
|
|
|
|
_ = testhelpers.NewErrorTestServer(t, reg)
|
|
publicTS, _ := testhelpers.NewKratosServer(t, reg)
|
|
_ = testhelpers.NewRedirSessionEchoTS(t, reg)
|
|
ui := testhelpers.NewRegistrationUIFlowEchoServer(t, reg)
|
|
|
|
t.Run("initial form is populated with identity traits", func(t *testing.T) {
|
|
t.Run("type=browser", func(t *testing.T) {
|
|
client := testhelpers.NewClientWithCookies(t)
|
|
|
|
t.Run("empty_flow", func(t *testing.T) {
|
|
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false)
|
|
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
|
|
"0.attributes.value",
|
|
))
|
|
})
|
|
|
|
t.Run("case=multi-schema-empty_flow", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
})
|
|
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
|
|
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
|
|
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
|
|
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
|
|
})
|
|
|
|
f := testhelpers.InitializeRegistrationFlowViaBrowser(t, client, publicTS, false, false, false, testhelpers.InitFlowWithIdentitySchema("not-default"))
|
|
snapshotx.SnapshotT(t, f.Ui.Nodes, snapshotx.ExceptPaths(
|
|
"0.attributes.value",
|
|
))
|
|
})
|
|
|
|
t.Run("case=multi-schema-invalid-form-data", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
})
|
|
conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
|
|
conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
|
|
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
|
|
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
|
|
})
|
|
|
|
body := testhelpers.SubmitRegistrationForm(t, false, client, publicTS, func(v url.Values) {
|
|
v.Set("traits.email", "invalidemail")
|
|
v.Set("traits.booly", "true")
|
|
v.Set("traits.numby", "1")
|
|
v.Set("traits.stringy", "string")
|
|
v.Set("traits.should_big_number", "1000000")
|
|
v.Set("traits.should_long_string", "1111111111111111111111111111111111111111111111111111111111")
|
|
v.Set("password", "password")
|
|
|
|
v.Set("method", "password")
|
|
}, false, http.StatusOK, ui.URL, testhelpers.InitFlowWithIdentitySchema("not-default"))
|
|
|
|
fmt.Println(body)
|
|
|
|
require.Equal(t, int64(4000040), gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.id").Int(), "%s", body)
|
|
require.Equal(t, "Enter a valid email address", gjson.Get(body, "ui.nodes.#(attributes.name==traits.email).messages.0.text").String(), "%s", body)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPopulateRegistrationMethod(t *testing.T) {
|
|
ctx := context.Background()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/identity.schema.json")
|
|
|
|
s, err := reg.AllRegistrationStrategies().Strategy(identity.CredentialsTypeProfile)
|
|
require.NoError(t, err)
|
|
fh, ok := s.(registration.FormHydrator)
|
|
require.True(t, ok)
|
|
|
|
toSnapshot := func(t *testing.T, f node.Nodes) {
|
|
t.Helper()
|
|
// The CSRF token has a unique value that messes with the snapshot - ignore it.
|
|
f.ResetNodes("csrf_token")
|
|
f.ResetNodes("passkey_challenge")
|
|
snapshotx.SnapshotT(t, f, snapshotx.ExceptNestedKeys("nonce", "src"))
|
|
}
|
|
|
|
newFlowInternal := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *registration.Flow) {
|
|
query := ""
|
|
if identitySchema != "" {
|
|
query = "?identity_schema=" + identitySchema
|
|
}
|
|
|
|
r := httptest.NewRequest("GET", "/self-service/registration/browser"+query, nil)
|
|
r = r.WithContext(ctx)
|
|
t.Helper()
|
|
f, err := registration.NewFlow(reg, r, flow.TypeBrowser)
|
|
f.UI.Nodes = make(node.Nodes, 0)
|
|
require.NoError(t, err)
|
|
return r, f
|
|
}
|
|
newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *registration.Flow) {
|
|
return newFlowInternal(ctx, t, "")
|
|
}
|
|
newFlowWithIdentitySchema := func(ctx context.Context, t *testing.T, identitySchema string) (*http.Request, *registration.Flow) {
|
|
return newFlowInternal(ctx, t, identitySchema)
|
|
}
|
|
|
|
t.Run("method=PopulateRegistrationMethod", func(t *testing.T) {
|
|
r, f := newFlow(ctx, t)
|
|
require.NoError(t, fh.PopulateRegistrationMethod(r, f))
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("method=PopulateRegistrationMethodProfile", func(t *testing.T) {
|
|
r, f := newFlow(ctx, t)
|
|
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("method=PopulateRegistrationMethodCredentials", func(t *testing.T) {
|
|
r, f := newFlow(ctx, t)
|
|
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("method=idempotency", func(t *testing.T) {
|
|
r, f := newFlow(ctx, t)
|
|
|
|
var snapshots []node.Nodes
|
|
|
|
t.Run("case=1", func(t *testing.T) {
|
|
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
|
|
snapshots = append(snapshots, f.UI.Nodes)
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("case=2", func(t *testing.T) {
|
|
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
|
|
snapshots = append(snapshots, f.UI.Nodes)
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("case=3", func(t *testing.T) {
|
|
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
|
|
snapshots = append(snapshots, f.UI.Nodes)
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("case=4", func(t *testing.T) {
|
|
require.NoError(t, fh.PopulateRegistrationMethodCredentials(r, f))
|
|
snapshots = append(snapshots, f.UI.Nodes)
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
|
|
t.Run("case=evaluate", func(t *testing.T) {
|
|
assertx.EqualAsJSON(t, snapshots[0], snapshots[2])
|
|
assertx.EqualAsJSON(t, snapshots[1], snapshots[3])
|
|
})
|
|
})
|
|
|
|
t.Run("case=multi-schema-method=PopulateRegistrationMethodProfile", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
|
|
})
|
|
multiSchema := contextx.WithConfigValue(ctx, config.ViperKeyDefaultIdentitySchemaID, "default")
|
|
multiSchema = contextx.WithConfigValue(multiSchema, config.ViperKeyIdentitySchemas, config.Schemas{
|
|
{ID: "default", URL: "file://./stub/identity-doesnotexist.schema.json"},
|
|
{ID: "not-default", URL: "file://./stub/identity.schema.json", SelfserviceSelectable: true},
|
|
})
|
|
|
|
r, f := newFlowWithIdentitySchema(multiSchema, t, "not-default")
|
|
require.NoError(t, fh.PopulateRegistrationMethodProfile(r, f))
|
|
toSnapshot(t, f.UI.Nodes)
|
|
})
|
|
}
|