Files
upstream-mirror c60cc7ca75
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

685 lines
28 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package profile_test
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/strategy/code"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/selfservice/strategy/passkey"
"github.com/ory/kratos/selfservice/strategy/password"
"github.com/ory/kratos/selfservice/strategy/webauthn"
"github.com/ory/kratos/selfservice/strategy/profile"
"github.com/ory/x/jsonx"
kratos "github.com/ory/kratos/pkg/httpclient"
"github.com/ory/kratos/corpx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/pkg"
"github.com/ory/kratos/pkg/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/x"
"github.com/ory/x/assertx"
"github.com/ory/x/httpx"
"github.com/ory/x/ioutilx"
"github.com/ory/x/sqlxx"
)
func init() {
corpx.RegisterFakes()
}
func newIdentityWithPassword(email string) *identity.Identity {
return &identity.Identity{
ID: x.NewUUID(),
Credentials: map[identity.CredentialsType]identity.Credentials{
"password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)},
},
Traits: identity.Traits(`{"email":"` + email + `","stringy":"foobar","booly":false,"numby":2.5,"should_long_string":"asdfasdfasdfasdfasfdasdfasdfasdf","should_big_number":2048}`),
SchemaID: config.DefaultIdentityTraitsSchemaID,
State: identity.StateActive,
VerifiableAddresses: []identity.VerifiableAddress{{Value: email, Via: identity.AddressTypeEmail}},
// TO ADD - RECOVERY EMAIL,
}
}
func TestStrategyTraits(t *testing.T) {
ctx := context.Background()
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.com/")
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true)
setPrivilegedTime := func(t *testing.T, duration time.Duration) {
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, duration.String())
t.Cleanup(func() {
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1ns")
})
}
setPrivileged := func(t *testing.T) {
setPrivilegedTime(t, time.Minute*10)
}
setUnprivileged := func(t *testing.T) {
setPrivilegedTime(t, time.Nanosecond)
}
ui := testhelpers.NewSettingsUIEchoServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
apiIdentity1 := newIdentityWithPassword("john-api@doe.com")
browserID2 := x.NewUUID()
browserIdentity2 := &identity.Identity{ID: browserID2, Traits: identity.Traits(`{}`), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{browserID2.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)},
}}
apiID2 := x.NewUUID()
apiIdentity2 := &identity.Identity{ID: apiID2, Traits: identity.Traits(`{}`), State: identity.StateActive, Credentials: map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {Type: "password", Identifiers: []string{apiID2.String()}, Config: []byte(`{"hashed_password":"$2a$04$zvZz1zV"}`)},
}}
browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, browserIdentity1)
browserUser1.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL))
browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(ctx, t, reg, browserIdentity2)
browserUser2.Jar.SetCookies(nosurfx.WithFakeCSRFCookie(t, reg, publicTS.URL))
apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, apiIdentity1)
apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(ctx, t, reg, apiIdentity2)
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
setUnprivileged(t)
t.Run("type=browser", func(t *testing.T) {
res, err := testhelpers.NewTestClient(t).Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(url.Values{"foo": {"bar"}}.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode, "%+v", res.Request)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI))
})
t.Run("type=api/spa", func(t *testing.T) {
res, err := testhelpers.NewTestClient(t).Do(httpx.MustNewRequest("POST", publicTS.URL+settings.RouteSubmitFlow, strings.NewReader(`{"foo":"bar"}`), "application/json"))
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
})
})
t.Run("description=should fail to post data if CSRF is invalid/type=browser", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser1,
url.Values{"traits.booly": {"true"}, "csrf_token": {"invalid"}, "method": {"profile"}}.Encode())
assert.EqualValues(t, http.StatusOK, res.StatusCode, "should return a 400 error because CSRF token is not set\n\t%s", actual)
assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch(), json.RawMessage(actual), "%s", actual)
})
t.Run("description=should fail to post data if CSRF is invalid/type=spa", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS)
actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser1,
testhelpers.EncodeFormAsJSON(t, true, url.Values{"traits.booly": {"true"}, "csrf_token": {"invalid"}, "method": {"profile"}}))
assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "should return a 400 error because CSRF token is not set\n\t%s", actual)
assertx.EqualAsJSON(t, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch(), json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual)
})
t.Run("description=should not fail because of CSRF token but because of unprivileged/type=api", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser1, `{"traits.booly":true,"method":"profile","csrf_token":"`+nosurfx.FakeCSRFToken+`"}`)
assert.EqualValues(t, http.StatusForbidden, res.StatusCode, "should return a 403 error because the session is unprivileged\n\t%s", actual)
require.Len(t, res.Cookies(), 1)
assert.Equal(t, "ory_kratos_continuity", res.Cookies()[0].Name)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "login session is too old", actual)
})
t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
setPrivileged(t)
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.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
req := testhelpers.NewPostRequest(t, true, f.Ui.Action, bytes.NewBufferString(`{"traits.booly":true,"method":"profile","csrf_token":"invalid"}`))
tc.mod(req.Header)
res, err := apiUser1.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, "%s", actual)
})
}
})
t.Run("description=hydrate the proper fields", func(t *testing.T) {
setPrivileged(t)
run := func(t *testing.T, id *identity.Identity, payload *kratos.SettingsFlow, route string) {
assert.NotEmpty(t, payload.Identity)
assert.Equal(t, id.ID.String(), string(payload.Identity.Id))
assert.JSONEq(t, string(id.Traits), x.MustEncodeJSON(t, payload.Identity.Traits))
assert.Equal(t, id.SchemaID, payload.Identity.SchemaId)
assert.Equal(t, publicTS.URL+route, payload.RequestUrl)
actual := jsonx.TestMarshalJSONString(t, payload.Ui)
assert.EqualValues(t, payload.Identity.Traits.(map[string]interface{})["email"], gjson.Get(actual, "nodes.#(attributes.name==traits.email).attributes.value").String())
assert.NotEmpty(t, gjson.Get(actual, "nodes.#(attributes.name==csrf_token).attributes.value").String(), "csrf token missing")
testhelpers.SnapshotTExcept(t, payload.Ui, []string{"action", "nodes.0.attributes.value", "nodes.1.attributes.value"})
}
t.Run("type=api", func(t *testing.T) {
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, apiUser1).FrontendAPI.CreateNativeSettingsFlow(context.Background()).Execute()
require.NoError(t, err)
run(t, apiIdentity1, pr, settings.RouteInitAPIFlow)
})
t.Run("type=spa", func(t *testing.T) {
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendAPI.CreateBrowserSettingsFlow(context.Background()).Execute()
require.NoError(t, err)
run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow)
})
t.Run("type=browser", func(t *testing.T) {
res, err := browserUser1.Get(publicTS.URL + settings.RouteInitBrowserFlow)
require.NoError(t, err)
assert.Contains(t, res.Request.URL.String(), ui.URL+"/settings?flow")
rid := res.Request.URL.Query().Get("flow")
require.NotEmpty(t, rid)
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendAPI.GetSettingsFlow(context.Background()).Id(res.Request.URL.Query().Get("flow")).Execute()
require.NoError(t, err, "%s", rid)
run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow)
})
})
expectValidationError := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values,
testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK),
testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String()))
}
t.Run("description=should come back with form errors if some profile data is invalid", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.NotEmpty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==csrf_token).attributes.value").String(), "%s", actual)
assert.Equal(t, "too-short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual)
assert.Equal(t, "bazbar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual)
assert.Equal(t, "2.5", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").String(), "%s", actual)
assert.Equal(t, "length must be >= 25, but got 9", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual)
}
payload := func(v url.Values) {
v.Set("method", "profile")
v.Set("traits.should_long_string", "too-short")
v.Set("traits.stringy", "bazbar")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=spa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("description=should not be able to make requests for another user", func(t *testing.T) {
setUnprivileged(t)
t.Run("type=api", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
actual, res := testhelpers.SettingsMakeRequest(t, true, false, f, apiUser2, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaAPI(t, browserUser1, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
actual, res := testhelpers.SettingsMakeRequest(t, false, true, f, browserUser2, testhelpers.EncodeFormAsJSON(t, true, values))
assert.Equal(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "error.reason").String(), "initiated by someone else", "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
actual, res := testhelpers.SettingsMakeRequest(t, false, false, f, browserUser2, values.Encode())
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Contains(t, gjson.Get(actual, "reason").String(), "initiated by someone else", "%s", actual)
})
})
t.Run("description=should end up at the login endpoint if trying to update protected field without sudo mode", func(t *testing.T) {
run := func(t *testing.T, config *kratos.SettingsFlow, isAPI bool, c *http.Client) *http.Response {
time.Sleep(time.Millisecond)
values := testhelpers.SDKFormFieldsToURLValues(config.Ui.Nodes)
values.Set("method", "profile")
values.Set("traits.email", "not-john-doe@foo.bar")
res, err := c.PostForm(config.Ui.Action, values)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
return res
}
t.Run("type=api", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiUser1, publicTS)
res := run(t, f, true, apiUser1)
assert.EqualValues(t, http.StatusForbidden, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
})
t.Run("type=sqa", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, true, publicTS)
res := run(t, f, true, browserUser1)
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI))
})
t.Run("type=browser", func(t *testing.T) {
setUnprivileged(t)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
res := run(t, f, false, browserUser1)
assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode)
assert.Contains(t, res.Request.URL.String(), conf.GetProvider(ctx).String(config.ViperKeySelfServiceLoginUI))
t.Run("should update when signed back in", func(t *testing.T) {
setPrivileged(t)
res, err := browserUser1.Get(f.Ui.Action)
require.NoError(t, err)
body := ioutilx.MustReadAll(res.Body)
defer func() { _ = res.Body.Close() }()
assert.EqualValues(t, http.StatusOK, res.StatusCode, "%s", body)
assert.EqualValues(t, flow.StateSuccess, gjson.GetBytes(body, "state").String(), "%s", body)
})
})
})
t.Run("flow=fail first update", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual)
assert.Equal(t, "1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual)
assert.Equal(t, "must be >= 1200 but found 1", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual)
assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here
}
payload := func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.should_big_number", "1")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=sqa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("flow=fail second update", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).messages.0.text").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").String(), "%s", actual)
assert.Equal(t, "short", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").String(), "%s", actual)
assert.Equal(t, "length must be >= 25, but got 5", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).messages.0.text").String(), "%s", actual)
assert.Equal(t, "this-is-not-a-number", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").String(), "%s", actual)
assert.Equal(t, "expected number, but got string", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).messages.0.text").String(), "%s", actual)
assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here
}
payload := func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Del("traits.should_big_number")
v.Set("traits.should_long_string", "short")
v.Set("traits.numby", "this-is-not-a-number")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=spa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
expectSuccess := func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
return testhelpers.SubmitSettingsForm(t, isAPI, isSPA, hc, publicTS, values,
http.StatusOK,
testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+settings.RouteSubmitFlow, conf.SelfServiceFlowSettingsUI(ctx).String()))
}
t.Run("flow=succeed with final request", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual)
assert.Equal(t, 15.0, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.value").Value(), "%s", actual)
assert.Equal(t, 9001.0, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.value").Value(), "%s", actual)
assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.value").Value(), "%s", actual)
}
payload := func(newEmail string) func(v url.Values) {
return func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.email", newEmail)
v.Set("traits.numby", "15")
v.Set("traits.should_big_number", "9001")
v.Set("traits.should_long_string", "this is such a long string, amazing stuff!")
}
}
t.Run("type=api", func(t *testing.T) {
actual := expectSuccess(t, true, false, apiUser1, payload("not-john-doe-api@mail.com"))
check(t, actual)
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=sqa", func(t *testing.T) {
actual := expectSuccess(t, false, true, browserUser1, payload("not-john-doe-browser@mail.com"))
check(t, actual)
assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), ui.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
actual := expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))
check(t, actual)
assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
t.Run("flow=try another update with invalid data", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, actual string) {
assert.EqualValues(t, flow.StateShowForm, gjson.Get(actual, "state").String(), "%s", actual)
}
payload := func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.should_long_string", "short")
}
t.Run("type=api", func(t *testing.T) {
check(t, expectValidationError(t, true, false, apiUser1, payload))
})
t.Run("type=sqa", func(t *testing.T) {
check(t, expectValidationError(t, false, true, browserUser1, payload))
})
t.Run("type=browser", func(t *testing.T) {
check(t, expectValidationError(t, false, false, browserUser1, payload))
})
})
t.Run("description=ensure that hooks are running", func(t *testing.T) {
setPrivileged(t)
var returned bool
router := http.NewServeMux()
router.HandleFunc("GET /return-ts", func(w http.ResponseWriter, r *http.Request) {
returned = true
})
rts := httptest.NewServer(router)
t.Cleanup(rts.Close)
testhelpers.SelfServiceHookSettingsSetDefaultRedirectTo(t, conf, rts.URL+"/return-ts")
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserUser1, false, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", settings.StrategyProfile)
values.Set("traits.should_big_number", "9001")
res, err := browserUser1.PostForm(f.Ui.Action, values)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.True(t, returned, "%d - %s", res.StatusCode, body)
})
// Update the login endpoint to auto-accept any incoming login request!
_ = testhelpers.NewSettingsLoginAcceptAPIServer(t, testhelpers.NewSDKCustomClient(publicTS, browserUser1), conf)
t.Run("description=should send email with verifiable address", func(t *testing.T) {
setPrivileged(t)
conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo:bar@irrelevant.com/")
t.Cleanup(func() {
conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceSettingsAfter, settings.StrategyProfile), nil)
})
check := func(t *testing.T, actual, newEmail string) {
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual)
assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual)
m, err := reg.CourierPersister().LatestQueuedMessage(context.Background())
require.NoError(t, err)
assert.Contains(t, m.Subject, "Use code")
}
payload := func(newEmail string) func(v url.Values) {
return func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.email", newEmail)
}
}
t.Run("type=api", func(t *testing.T) {
newEmail := "update-verify-api@mail.com"
actual := expectSuccess(t, true, false, apiUser1, payload(newEmail))
check(t, actual, newEmail)
})
t.Run("type=spa", func(t *testing.T) {
newEmail := "update-verify-browser@mail.com"
actual := expectSuccess(t, false, true, browserUser1, payload(newEmail))
check(t, actual, newEmail)
})
t.Run("type=browser", func(t *testing.T) {
newEmail := "update-verify-browser@mail.com"
actual := expectSuccess(t, false, false, browserUser1, payload(newEmail))
check(t, actual, newEmail)
})
})
t.Run("description=should update protected field with sudo mode", func(t *testing.T) {
setPrivileged(t)
check := func(t *testing.T, newEmail string, actual string) {
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.numby).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_big_number).attributes.errors").Value(), "%s", actual)
assert.Empty(t, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.should_long_string).attributes.errors").Value(), "%s", actual)
assert.Equal(t, newEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==traits.email).attributes.value").Value(), "%s", actual)
assert.Equal(t, "foobar", gjson.Get(actual, "ui.nodes.#(attributes.name==traits.stringy).attributes.value").String(), "%s", actual) // sanity check if original payload is still here
}
payload := func(email string) func(v url.Values) {
return func(v url.Values) {
v.Set("method", settings.StrategyProfile)
v.Set("traits.email", email)
}
}
t.Run("type=api", func(t *testing.T) {
email := "not-john-doe-api@mail.com"
actual := expectSuccess(t, true, false, apiUser1, payload(email))
check(t, email, actual)
})
t.Run("type=sqa", func(t *testing.T) {
email := "not-john-doe-browser@mail.com"
actual := expectSuccess(t, false, true, browserUser1, payload(email))
check(t, email, actual)
})
t.Run("type=browser", func(t *testing.T) {
email := "not-john-doe-browser@mail.com"
actual := expectSuccess(t, false, false, browserUser1, payload(email))
check(t, email, actual)
})
})
}
func TestDisabledEndpoint(t *testing.T) {
conf, reg := pkg.NewFastRegistryWithMocks(t)
testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, false)
errTS := testhelpers.NewErrorTestServer(t, reg)
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(context.Background(), t, reg, browserIdentity1)
t.Run("case=should not submit when profile method is disabled", func(t *testing.T) {
t.Run("method=GET", func(t *testing.T) {
t.Skip("GET is not supported here")
})
t.Run("method=POST", func(t *testing.T) {
b := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, func(v url.Values) {
v.Set("method", settings.StrategyProfile)
},
http.StatusOK,
testhelpers.ExpectURL(false, publicTS.URL+settings.RouteSubmitFlow, errTS.URL))
assert.Contains(t, string(b), "404")
assert.Contains(t, string(b), "This endpoint was disabled by system administrator")
})
})
}
func TestSortedForHydration(t *testing.T) {
_, reg := pkg.NewFastRegistryWithMocks(t)
// Get a reference to all registration strategies
allStrategies := []registration.Strategy{
password.NewStrategy(reg),
code.NewStrategy(reg),
oidc.NewStrategy(reg),
code.NewStrategy(reg),
passkey.NewStrategy(reg),
passkey.NewStrategy(reg),
profile.NewStrategy(reg),
webauthn.NewStrategy(reg),
}
var originalOrder []string
for _, s := range allStrategies {
if s.ID().String() == "profile" {
continue
}
originalOrder = append(originalOrder, s.ID().String())
}
var actual []string
for _, s := range profile.SortForHydration(allStrategies) {
actual = append(actual, s.ID().String())
}
assert.EqualValues(t, append([]string{"profile"}, originalOrder...), actual)
}