snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
535
pkg/registrationhelpers/helpers.go
Normal file
535
pkg/registrationhelpers/helpers.go
Normal file
@@ -0,0 +1,535 @@
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
28
pkg/registrationhelpers/stub/basic.schema.json
Normal file
28
pkg/registrationhelpers/stub/basic.schema.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$id": "https://example.com/person.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
},
|
||||
"webauthn": {
|
||||
"identifier": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
35
pkg/registrationhelpers/stub/multifield.schema.json
Normal file
35
pkg/registrationhelpers/stub/multifield.schema.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$id": "https://example.com/person.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foobar": {
|
||||
"type": "string",
|
||||
"minLength": 2
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
},
|
||||
"webauthn": {
|
||||
"identifier": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foobar",
|
||||
"username"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
Reference in New Issue
Block a user