// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package registrationhelpers import ( "bytes" "context" _ "embed" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "slices" "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/driver/config" "github.com/ory/kratos/pkg" kratos "github.com/ory/kratos/pkg/httpclient" "github.com/ory/kratos/pkg/testhelpers" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/x" "github.com/ory/kratos/x/nosurfx" "github.com/ory/x/assertx" "github.com/ory/x/clock" "github.com/ory/x/configx" "github.com/ory/x/httpx" "github.com/ory/x/ioutilx" ) func setupServer(t *testing.T, reg *driver.RegistryDefault) *httptest.Server { publicTS, _ := testhelpers.NewKratosServer(t, reg) redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) conf := reg.Config() conf.MustSet(t.Context(), config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/default-return-to") //nolint:staticcheck conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, redirTS.URL+"/registration-return-ts") //nolint:staticcheck return publicTS } func ExpectValidationError(ctx context.Context, t *testing.T, ts *httptest.Server, conf *config.Config, flow string, values func(url.Values)) string { isSPA := flow == "spa" isAPI := flow == "api" return testhelpers.SubmitRegistrationFormCtx(ctx, t, isAPI, nil, ts, values, isSPA, testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), testhelpers.ExpectURL(isAPI || isSPA, ts.URL+registration.RouteSubmitFlow, conf.SelfServiceFlowRegistrationUI(ctx).String())) } func CheckFormContent(t *testing.T, body []byte, requiredFields ...string) { FieldNameSet(t, body, requiredFields) OutdatedFieldsDoNotExist(t, body) FormMethodIsPOST(t, body) } // FieldNameSet checks if the fields have the right "name" set. func FieldNameSet(t *testing.T, body []byte, fields []string) { for _, f := range fields { assert.Equal(t, f, gjson.GetBytes(body, fmt.Sprintf("ui.nodes.#(attributes.name==%s).attributes.name", f)).String(), "%s", body) } } // checks if some keys are not set, this should be used to catch regression issues func OutdatedFieldsDoNotExist(t *testing.T, body []byte) { for _, k := range []string{"request"} { assert.Equal(t, false, gjson.GetBytes(body, fmt.Sprintf("ui.nodes.fields.#(name==%s)", k)).Exists()) } } func FormMethodIsPOST(t *testing.T, body []byte) { assert.Equal(t, "POST", gjson.GetBytes(body, "ui.method").String()) } //go:embed stub/basic.schema.json var basicSchema []byte //go:embed stub/multifield.schema.json var multifieldSchema []byte var skipIfNotEnabled = func(t *testing.T, flows []string, flow string) { if !slices.Contains(flows, flow) { t.Skipf("Skipping for %s flow because it was not included in the list of flows to be executed.", flow) } } func AssertSchemaDoesNotExist(t *testing.T, reg *driver.RegistryDefault, flows []string, payload func(v url.Values)) { conf := reg.Config() _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) apiClient := testhelpers.NewDebugClient(t) errTS := testhelpers.NewErrorTestServer(t, reg) reset := func() { testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) } reset() t.Run("case=should fail because schema does not exist", func(t *testing.T) { check := func(t *testing.T, actual string) { assert.Equal(t, int64(http.StatusInternalServerError), gjson.Get(actual, "code").Int(), "%s", actual) assert.Equal(t, "Internal Server Error", gjson.Get(actual, "status").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "reason").String(), "no such file or directory", "%s", actual) } values := url.Values{ "traits.username": {testhelpers.RandomEmail()}, "traits.foobar": {"bar"}, "csrf_token": {nosurfx.FakeCSRFToken}, } payload(values) t.Run("type=api", func(t *testing.T) { skipIfNotEnabled(t, flows, "api") f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/i-do-not-exist.schema.json") t.Cleanup(reset) body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, apiClient, values.Encode()) assert.Contains(t, res.Request.URL.String(), publicTS.URL) check(t, gjson.Get(body, "error").Raw) }) t.Run("type=spa", func(t *testing.T) { skipIfNotEnabled(t, flows, "spa") browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true, false, false) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/i-do-not-exist.schema.json") t.Cleanup(reset) body, res := testhelpers.RegistrationMakeRequest(t, false, true, f, apiClient, values.Encode()) assert.Contains(t, res.Request.URL.String(), publicTS.URL) check(t, gjson.Get(body, "error").Raw) }) t.Run("type=browser", func(t *testing.T) { skipIfNotEnabled(t, flows, "browser") browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/i-do-not-exist.schema.json") t.Cleanup(reset) body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, apiClient, values.Encode()) assert.Contains(t, res.Request.URL.String(), errTS.URL) check(t, body) }) }) } func AssertCSRFFailures(t *testing.T, reg *driver.RegistryDefault, flows []string, payload func(v url.Values)) { conf := reg.Config() testhelpers.SetDefaultIdentitySchemaFromRaw(conf, multifieldSchema) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) apiClient := testhelpers.NewDebugClient(t) _ = testhelpers.NewErrorTestServer(t, reg) values := url.Values{ "csrf_token": {"invalid_token"}, "traits.username": {testhelpers.RandomEmail()}, "traits.foobar": {"bar"}, } payload(values) t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) { skipIfNotEnabled(t, flows, "browser") browserClient := testhelpers.NewClientWithCookies(t) browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false, false, false) actual, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, values.Encode()) assert.EqualValues(t, http.StatusOK, res.StatusCode) assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), json.RawMessage(actual), "%s", actual) }) t.Run("case=should fail because of missing CSRF token/type=spa", func(t *testing.T) { skipIfNotEnabled(t, flows, "spa") browserClient := testhelpers.NewClientWithCookies(t) browserClient.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL)) f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true, false, false) actual, res := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, values.Encode()) assert.EqualValues(t, http.StatusForbidden, res.StatusCode) assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual) }) t.Run("case=should pass even without CSRF token/type=api", func(t *testing.T) { skipIfNotEnabled(t, flows, "api") f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) actual, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values)) assert.EqualValues(t, http.StatusOK, res.StatusCode) assert.NotEmpty(t, gjson.Get(actual, "identity.id").Raw, "%s", actual) // registration successful }) t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) { skipIfNotEnabled(t, flows, "api") for k, tc := range []struct { mod func(http.Header) exp string }{ { mod: func(h http.Header) { h.Add("Cookie", "name=bar") }, exp: "The HTTP Request Header included the \\\"Cookie\\\" key", }, { mod: func(h http.Header) { h.Add("Origin", "www.bar.com") }, exp: "The HTTP Request Header included the \\\"Origin\\\" key", }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) c := f.Ui req := testhelpers.NewPostRequest(t, true, c.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values))) tc.mod(req.Header) res, err := apiClient.Do(req) require.NoError(t, err) defer func() { _ = res.Body.Close() }() actual := string(ioutilx.MustReadAll(res.Body)) assert.EqualValues(t, http.StatusBadRequest, res.StatusCode) assert.Contains(t, actual, tc.exp) }) } }) } func AssertRegistrationRespectsValidation(t *testing.T, reg *driver.RegistryDefault, flows []string, payload func(url.Values)) { conf := reg.Config() testhelpers.SetDefaultIdentitySchemaFromRaw(conf, multifieldSchema) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) t.Run("case=should return an error because not passing validation", func(t *testing.T) { email := testhelpers.RandomEmail() check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username", "traits.foobar") assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.foobar).messages.0").String(), `Property foobar is missing`, "%s", actual) assert.Equal(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.username).attributes.value").String(), "%s", actual) } values := func(v url.Values) { v.Set("traits.username", email) v.Del("traits.foobar") payload(v) } for _, f := range flows { t.Run("type="+f, func(t *testing.T) { check(t, ExpectValidationError(t.Context(), t, publicTS, conf, f, values)) }) } }) } func AssertCommonErrorCases(t *testing.T, flows []string) { conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true), configx.WithValues(testhelpers.DefaultRawIdentitySchemaConfig(basicSchema)), ) uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) apiClient := testhelpers.NewDebugClient(t) errTS := testhelpers.NewErrorTestServer(t, reg) t.Run("description=can call endpoints only without session", func(t *testing.T) { values := url.Values{} t.Run("type=browser", func(t *testing.T) { res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t.Context(), t, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) require.NoError(t, err) defer func() { _ = res.Body.Close() }() assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) }) t.Run("type=api", func(t *testing.T) { res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t.Context(), t, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json")) require.NoError(t, err) assert.Len(t, res.Cookies(), 0) defer func() { _ = res.Body.Close() }() assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(ioutilx.MustReadAll(res.Body), "error").Raw)) }) }) t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) { t.Run("type=api", func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`) }) t.Run("type=spa", func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, apiClient, publicTS, true, false, false) body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`) }) t.Run("type=browser", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts") assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body) assert.Equal(t, "email", gjson.Get(body, "ui.nodes.#(attributes.name==\"traits.email\").attributes.type").String(), "%s", body) }) }) t.Run("description=can call endpoints only without session", func(t *testing.T) { values := url.Values{} t.Run("type=browser", func(t *testing.T) { res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t.Context(), t, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) require.NoError(t, err) defer func() { _ = res.Body.Close() }() assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) assert.Contains(t, res.Request.URL.String(), conf.GetProvider(t.Context()).String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) }) t.Run("type=api", func(t *testing.T) { res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t.Context(), t, reg). Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json")) require.NoError(t, err) assert.Len(t, res.Cookies(), 0) defer func() { _ = res.Body.Close() }() assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn(), json.RawMessage(gjson.GetBytes(ioutilx.MustReadAll(res.Body), "error").Raw)) }) }) t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) { t.Run("type=api", func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`) }) t.Run("type=spa", func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, apiClient, publicTS, true, false, false) body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`) }) t.Run("type=browser", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ") assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts") assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body) assert.Equal(t, "email", gjson.Get(body, "ui.nodes.#(attributes.name==\"traits.email\").attributes.type").String(), "%s", body) }) }) t.Run("case=should show the error ui because the method is missing in payload", func(t *testing.T) { t.Run("type=api", func(t *testing.T) { f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) body, res := testhelpers.RegistrationMakeRequest(t, true, false, f, apiClient, "{}}") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find a strategy to sign you up with. Did you fill out the form correctly?", "%s", body) }) t.Run("type=spa", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, true, false, false) body, res := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, "{}}") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find a strategy to sign you up with. Did you fill out the form correctly?", "%s", body) }) t.Run("type=browser", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) body, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "foo=bar") assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts") assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "Could not find a strategy to sign you up with. Did you fill out the form correctly?", "%s", body) }) }) t.Run("case=should show the error ui because the request id is missing", func(t *testing.T) { check := func(t *testing.T, actual string) { assert.Equal(t, int64(http.StatusNotFound), gjson.Get(actual, "code").Int(), "%s", actual) assert.Equal(t, "Not Found", gjson.Get(actual, "status").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "message").String(), "Unable to locate the resource", "%s", actual) } fakeFlow := &kratos.RegistrationFlow{Ui: kratos.UiContainer{ Action: publicTS.URL + registration.RouteSubmitFlow + "?flow=" + x.NewUUID().String(), }} t.Run("type=api", func(t *testing.T) { actual, res := testhelpers.RegistrationMakeRequest(t, true, false, fakeFlow, apiClient, "{}") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) check(t, gjson.Get(actual, "error").Raw) }) t.Run("type=api", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) actual, res := testhelpers.RegistrationMakeRequest(t, false, true, fakeFlow, browserClient, "{}") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) check(t, gjson.Get(actual, "error").Raw) }) t.Run("type=browser", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) actual, res := testhelpers.RegistrationMakeRequest(t, false, false, fakeFlow, browserClient, "") assert.Contains(t, res.Request.URL.String(), errTS.URL) check(t, actual) }) }) t.Run("case=should return an error because the request is expired", func(t *testing.T) { lifespan := 500 * time.Millisecond conf.MustSet(t.Context(), config.ViperKeySelfServiceRegistrationRequestLifespan, lifespan) t.Cleanup(func() { conf.MustSet(context.Background(), config.ViperKeySelfServiceRegistrationRequestLifespan, "10m") }) // Freeze the clock so flow expiry is deterministic and the "expired N minutes ago" reason does not depend on // the request round-trip. Each subtest creates a flow, then advances the clock past its lifespan instead of // sleeping, so the handler and the expected error read the same instant. mock := clock.NewMock(time.Now().UTC()) reg.SetClock(mock) t.Cleanup(func() { reg.SetClock(clock.New()) }) t.Run("type=api", func(t *testing.T) { expiresAt := mock.Now().Add(lifespan) f := testhelpers.InitializeRegistrationFlowViaAPICtx(t.Context(), t, apiClient, publicTS) mock.Set(expiresAt.Add(time.Minute)) actual, res := testhelpers.RegistrationMakeRequestCtx(t.Context(), t, true, false, f, apiClient, "{}") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expiresAt), json.RawMessage(actual), []string{"use_flow_id", "expired_at", "since"}, "") }) t.Run("type=spa", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) expiresAt := mock.Now().Add(lifespan) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, true, false, false) mock.Set(expiresAt.Add(time.Minute)) actual, res := testhelpers.RegistrationMakeRequest(t, false, true, f, browserClient, "{}") assert.Contains(t, res.Request.URL.String(), publicTS.URL+registration.RouteSubmitFlow) assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String()) assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(reg.Clock(), expiresAt), json.RawMessage(actual), []string{"use_flow_id", "expired_at", "since"}, "expired", "%s", actual) }) t.Run("type=browser", func(t *testing.T) { browserClient := testhelpers.NewClientWithCookies(t) expiresAt := mock.Now().Add(lifespan) f := testhelpers.InitializeRegistrationFlowViaBrowserCtx(t.Context(), t, browserClient, publicTS, false, false, false) mock.Set(expiresAt.Add(time.Minute)) actual, res := testhelpers.RegistrationMakeRequest(t, false, false, f, browserClient, "") assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/registration-ts") assert.NotEqual(t, f.Id, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "expired", "%s", actual) }) }) t.Run("case=should fail because the password was used in databreaches", func(t *testing.T) { // This subtest needs the multifield schema, whereas the rest of the // suite uses the basic schema. Swapping the shared config's default // schema and restoring it in a cleanup would let that mutation race // in-flight requests of sibling subtests reading the same config. It // therefore owns its own registry, config, and server, preconfigured // with the multifield schema. See .claude/rules/go.md: "subtests that // need parallelization must own their resources". conf, reg := pkg.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySelfServiceRegistrationEnableLegacyOneStep, true), configx.WithValues(testhelpers.DefaultRawIdentitySchemaConfig(multifieldSchema)), ) _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) publicTS := setupServer(t, reg) email := testhelpers.RandomEmail() check := func(t *testing.T, actual string) { assert.NotEmpty(t, gjson.Get(actual, "id").String(), "%s", actual) assert.Contains(t, gjson.Get(actual, "ui.action").String(), publicTS.URL+registration.RouteSubmitFlow, "%s", actual) CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username", "traits.foobar") assert.Contains(t, gjson.Get(actual, "ui.nodes.#(attributes.name==password).messages.0").String(), "data breaches and must no longer be used.", "%s", actual) // but the method is still set assert.Equal(t, "password", gjson.Get(actual, "ui.nodes.#(attributes.name==method).attributes.value").String(), "%s", actual) } values := func(v url.Values) { v.Set("traits.username", email) v.Set("password", "password") v.Set("traits.foobar", "bar") } for _, f := range flows { t.Run("type="+f, func(t *testing.T) { check(t, ExpectValidationError(t.Context(), t, publicTS, conf, f, values)) }) } }) }