From 5c1f11b2ae65dd73d572e456b522a7d83ac1f473 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 3 Mar 2022 11:19:56 +0100 Subject: [PATCH] test: extract common registration helpers to library --- internal/registrationhelpers/helpers.go | 345 ++++++++++++++++++ .../stub/basic.schema.json | 28 ++ .../stub/multifield.schema.json | 35 ++ internal/testhelpers/config.go | 9 + internal/testhelpers/fake.go | 7 + selfservice/strategy/password/login_test.go | 9 +- .../strategy/password/registration_test.go | 271 +------------- 7 files changed, 446 insertions(+), 258 deletions(-) create mode 100644 internal/registrationhelpers/helpers.go create mode 100644 internal/registrationhelpers/stub/basic.schema.json create mode 100644 internal/registrationhelpers/stub/multifield.schema.json create mode 100644 internal/testhelpers/fake.go diff --git a/internal/registrationhelpers/helpers.go b/internal/registrationhelpers/helpers.go new file mode 100644 index 00000000000..2ab2e8a92e6 --- /dev/null +++ b/internal/registrationhelpers/helpers.go @@ -0,0 +1,345 @@ +package registrationhelpers + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + kratos "github.com/ory/kratos-client-go" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/internal/testhelpers" + "github.com/ory/kratos/selfservice/flow" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/x" + "github.com/ory/x/assertx" + "github.com/ory/x/httpx" + "github.com/ory/x/ioutilx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" +) + +func setupServer(t *testing.T, reg *driver.RegistryDefault) *httptest.Server { + conf := reg.Config(context.Background()) + router := x.NewRouterPublic() + admin := x.NewRouterAdmin() + + publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, admin) + redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) + conf.MustSet(config.ViperKeySelfServiceBrowserDefaultReturnTo, redirTS.URL+"/default-return-to") + conf.MustSet(config.ViperKeySelfServiceRegistrationAfter+"."+config.DefaultBrowserReturnURL, redirTS.URL+"/registration-return-ts") + return publicTS +} + +func ExpectValidationError(t *testing.T, ts *httptest.Server, conf *config.Config, flow string, values func(url.Values)) string { + isSPA := flow == "spa" + isAPI := flow == "api" + return testhelpers.SubmitRegistrationForm(t, isAPI, nil, ts, values, + isSPA, + testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), + testhelpers.ExpectURL(isAPI || isSPA, ts.URL+registration.RouteSubmitFlow, conf.SelfServiceFlowRegistrationUI().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 + +func AssertRegistrationRespectsValidation(t *testing.T, flows []string, payload func(url.Values)) { + conf, reg := internal.NewFastRegistryWithMocks(t) + 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() + var 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) + } + + var 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, publicTS, conf, f, values)) + }) + } + }) +} + +func AssertCommonErrorCases(t *testing.T, flows []string) { + conf, reg := internal.NewFastRegistryWithMocks(t) + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, 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, reg). + Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) + require.NoError(t, err) + defer res.Body.Close() + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), conf.Source().String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) + }) + + t.Run("type=api", func(t *testing.T) { + res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(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 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.InitializeRegistrationFlowViaAPI(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.InitializeRegistrationFlowViaBrowser(t, apiClient, publicTS, true) + 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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, 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, reg). + Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) + require.NoError(t, err) + defer res.Body.Close() + assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) + assert.Contains(t, res.Request.URL.String(), conf.Source().String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) + }) + + t.Run("type=api", func(t *testing.T) { + res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(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 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.InitializeRegistrationFlowViaAPI(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.InitializeRegistrationFlowViaBrowser(t, apiClient, publicTS, true) + 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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, 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.InitializeRegistrationFlowViaAPI(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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true) + 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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, 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) { + var 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.SelfServiceRegistrationFlow{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) { + conf.MustSet(config.ViperKeySelfServiceRegistrationRequestLifespan, "500ms") + t.Cleanup(func() { + conf.MustSet(config.ViperKeySelfServiceRegistrationRequestLifespan, "10m") + }) + + t.Run("type=api", func(t *testing.T) { + f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) + + time.Sleep(time.Millisecond * 600) + actual, res := testhelpers.RegistrationMakeRequest(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(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual) + }) + + t.Run("type=spa", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true) + + time.Sleep(time.Millisecond * 600) + 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(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual) + }) + + t.Run("type=browser", func(t *testing.T) { + browserClient := testhelpers.NewClientWithCookies(t) + f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false) + + time.Sleep(time.Millisecond * 600) + 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 return_to url is not allowed", func(t *testing.T) { + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, multifieldSchema) + t.Cleanup(func() { + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, basicSchema) + }) + + email := testhelpers.RandomEmail() + var 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) + } + + var 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, publicTS, conf, f, values)) + }) + } + }) +} diff --git a/internal/registrationhelpers/stub/basic.schema.json b/internal/registrationhelpers/stub/basic.schema.json new file mode 100644 index 00000000000..829c04caf80 --- /dev/null +++ b/internal/registrationhelpers/stub/basic.schema.json @@ -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 +} diff --git a/internal/registrationhelpers/stub/multifield.schema.json b/internal/registrationhelpers/stub/multifield.schema.json new file mode 100644 index 00000000000..2f355fb746d --- /dev/null +++ b/internal/registrationhelpers/stub/multifield.schema.json @@ -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 +} diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go index 7eef009b6c6..e69a3141ab1 100644 --- a/internal/testhelpers/config.go +++ b/internal/testhelpers/config.go @@ -1,6 +1,7 @@ package testhelpers import ( + "encoding/base64" "testing" "github.com/ory/kratos/driver/config" @@ -24,3 +25,11 @@ func SetDefaultIdentitySchema(conf *config.Config, url string) { {ID: "default", URL: url}, }) } + +// SetDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string. +func SetDefaultIdentitySchemaFromRaw(conf *config.Config, schema []byte) { + conf.MustSet(config.ViperKeyDefaultIdentitySchemaID, "default") + conf.MustSet(config.ViperKeyIdentitySchemas, config.Schemas{ + {ID: "default", URL: "base64://" + base64.URLEncoding.EncodeToString(schema)}, + }) +} diff --git a/internal/testhelpers/fake.go b/internal/testhelpers/fake.go new file mode 100644 index 00000000000..4a2d15f6deb --- /dev/null +++ b/internal/testhelpers/fake.go @@ -0,0 +1,7 @@ +package testhelpers + +import "github.com/ory/x/randx" + +func RandomEmail() string { + return randx.MustString(16, randx.Alpha) + "@ory.sh" +} diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index c08fc017ebb..4dc6688ba84 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -3,8 +3,10 @@ package password_test import ( "bytes" "context" + _ "embed" "encoding/json" "fmt" + "github.com/ory/kratos/internal/registrationhelpers" "io/ioutil" "net/http" "net/url" @@ -39,6 +41,9 @@ import ( "github.com/ory/kratos/x" ) +//go:embed stub/login.schema.json +var loginSchema []byte + func TestCompleteLogin(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) conf.MustSet(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), @@ -54,11 +59,11 @@ func TestCompleteLogin(t *testing.T) { conf.MustSet(config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts") conf.MustSet(config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts") - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/login.schema.json") + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, loginSchema) conf.MustSet(config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"}) ensureFieldsExist := func(t *testing.T, body []byte) { - checkFormContent(t, body, "identifier", + registrationhelpers.CheckFormContent(t, body, "identifier", "password", "csrf_token") } diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 259052cc370..7e376d0b852 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/ory/kratos/internal/registrationhelpers" "net/http" "net/http/httptest" "net/url" @@ -14,9 +15,6 @@ import ( "github.com/ory/kratos/text" "github.com/ory/kratos/ui/node" - "github.com/ory/kratos/selfservice/flow" - - kratos "github.com/ory/kratos-client-go" "github.com/ory/kratos/ui/container" "github.com/ory/x/ioutilx" @@ -24,41 +22,21 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/gjson" - "github.com/ory/x/assertx" - "github.com/ory/x/httpx" - "github.com/ory/kratos/driver/config" "github.com/ory/kratos/identity" "github.com/ory/kratos/internal" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/x/assertx" + _ "embed" "github.com/ory/kratos/x" ) -func checkFormContent(t *testing.T, body []byte, requiredFields ...string) { - fieldNameSet(t, body, requiredFields) - outdatedFieldsDoNotExist(t, body) - formMethodIsPOST(t, body) -} +var flows = []string{"spa", "api", "browser"} -// 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/registration.schema.json +var registrationSchema []byte func TestRegistration(t *testing.T) { t.Run("case=registration", func(t *testing.T) { @@ -70,7 +48,7 @@ func TestRegistration(t *testing.T) { publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, admin) errTS := testhelpers.NewErrorTestServer(t, reg) - uiTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg) + _ = testhelpers.NewRegistrationUIFlowEchoServer(t, reg) redirTS := testhelpers.NewRedirSessionEchoTS(t, reg) redirNoSessionTS := testhelpers.NewRedirNoSessionTS(t, reg) @@ -82,236 +60,17 @@ func TestRegistration(t *testing.T) { } useReturnToFromTS(redirTS) - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") + testhelpers.SetDefaultIdentitySchemaFromRaw(conf, registrationSchema) apiClient := testhelpers.NewDebugClient(t) - t.Run("description=can call endpoints only without session", func(t *testing.T) { - // Needed to set up the mock IDs... - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/profile.schema.json") - t.Cleanup(func() { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") - }) - - values := url.Values{} - - t.Run("type=browser", func(t *testing.T) { - res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg). - Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded")) - require.NoError(t, err) - defer res.Body.Close() - assert.EqualValues(t, http.StatusOK, res.StatusCode, "%+v", res.Request) - assert.Contains(t, res.Request.URL.String(), conf.Source().String(config.ViperKeySelfServiceBrowserDefaultReturnTo)) - }) - - t.Run("type=api", func(t *testing.T) { - res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(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 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) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/profile.schema.json") - t.Cleanup(func() { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") - }) - - t.Run("type=api", func(t *testing.T) { - f := testhelpers.InitializeRegistrationFlowViaAPI(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.InitializeRegistrationFlowViaBrowser(t, apiClient, publicTS, true) - 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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, 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) { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/profile.schema.json") - t.Cleanup(func() { - testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/registration.schema.json") - }) - - t.Run("type=api", func(t *testing.T) { - f := testhelpers.InitializeRegistrationFlowViaAPI(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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true) - 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.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, 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) { - var 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.SelfServiceRegistrationFlow{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("AssertCommonErrorCases", func(t *testing.T) { + registrationhelpers.AssertCommonErrorCases(t, flows) }) - t.Run("case=should return an error because the request is expired", func(t *testing.T) { - conf.MustSet(config.ViperKeySelfServiceRegistrationRequestLifespan, "500ms") - t.Cleanup(func() { - conf.MustSet(config.ViperKeySelfServiceRegistrationRequestLifespan, "10m") - }) - - t.Run("type=api", func(t *testing.T) { - f := testhelpers.InitializeRegistrationFlowViaAPI(t, apiClient, publicTS) - - time.Sleep(time.Millisecond * 600) - actual, res := testhelpers.RegistrationMakeRequest(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(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual) - }) - - t.Run("type=spa", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookies(t) - f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, true) - - time.Sleep(time.Millisecond * 600) - 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(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since"}, "expired", "%s", actual) - }) - - t.Run("type=browser", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookies(t) - f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, publicTS, false) - - time.Sleep(time.Millisecond * 600) - 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) - }) - }) - - var expectValidationError = func(t *testing.T, isAPI, isSPA bool, values func(url.Values)) string { - return testhelpers.SubmitRegistrationForm(t, isAPI, nil, publicTS, values, - isSPA, - testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK), - testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+registration.RouteSubmitFlow, conf.SelfServiceFlowRegistrationUI().String())) - } - - t.Run("case=should fail because the return_to url is not allowed", func(t *testing.T) { - var 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) - } - - var values = func(v url.Values) { - v.Set("traits.username", "registration-identifier-4") - v.Set("password", "password") - v.Set("traits.foobar", "bar") - } - - t.Run("type=api", func(t *testing.T) { - check(t, expectValidationError(t, true, false, values)) - }) - - t.Run("type=spa", func(t *testing.T) { - check(t, expectValidationError(t, false, false, values)) - }) - - t.Run("type=browser", func(t *testing.T) { - check(t, expectValidationError(t, false, false, values)) - }) - }) - - t.Run("case=should return an error because not passing validation", func(t *testing.T) { - var 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) - } - - var values = func(v url.Values) { - v.Set("traits.username", "registration-identifier-5") - v.Set("password", x.NewUUID().String()) + t.Run("AssertRegistrationRespectsValidation", func(t *testing.T) { + registrationhelpers.AssertRegistrationRespectsValidation(t, flows, func(v url.Values) { v.Del("traits.foobar") - } - - t.Run("type=api", func(t *testing.T) { - check(t, expectValidationError(t, true, false, values)) - }) - - t.Run("type=spa", func(t *testing.T) { - check(t, expectValidationError(t, false, true, values)) - }) - - t.Run("type=browser", func(t *testing.T) { - check(t, expectValidationError(t, false, false, values)) }) }) @@ -588,7 +347,7 @@ func TestRegistration(t *testing.T) { } _ = expectSuccessfulLogin(t, false, true, nil, values) - body := expectValidationError(t, false, true, applyTransform(values, transform)) + body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "spa", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "An account with the same identifier (email, phone, username, ...) exists already.", "%s", body) }) @@ -600,7 +359,7 @@ func TestRegistration(t *testing.T) { } _ = expectSuccessfulLogin(t, false, false, nil, values) - body := expectValidationError(t, false, false, applyTransform(values, transform)) + body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "browser", applyTransform(values, transform)) assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "An account with the same identifier (email, phone, username, ...) exists already.", "%s", body) }) } @@ -636,7 +395,7 @@ func TestRegistration(t *testing.T) { var 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") + registrationhelpers.CheckFormContent(t, []byte(actual), "password", "csrf_token", "traits.username") } var checkFirst = func(t *testing.T, actual string) {