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:
571
selfservice/flow/recovery/error_test.go
Normal file
571
selfservice/flow/recovery/error_test.go
Normal file
@@ -0,0 +1,571 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package recovery_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/x/clock"
|
||||
"github.com/ory/x/configx"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/pkg"
|
||||
"github.com/ory/kratos/pkg/testhelpers"
|
||||
"github.com/ory/kratos/schema"
|
||||
"github.com/ory/kratos/selfservice/flow"
|
||||
"github.com/ory/kratos/selfservice/flow/recovery"
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/ui/node"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
"github.com/ory/x/assertx"
|
||||
"github.com/ory/x/ioutilx"
|
||||
"github.com/ory/x/jsonx"
|
||||
"github.com/ory/x/snapshotx"
|
||||
"github.com/ory/x/urlx"
|
||||
)
|
||||
|
||||
func TestHandleError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
conf, reg := pkg.NewFastRegistryWithMocks(t,
|
||||
configx.WithValues(map[string]any{
|
||||
config.ViperKeySelfServiceRecoveryEnabled: true,
|
||||
config.ViperKeySelfServiceRecoveryUse: "code",
|
||||
}),
|
||||
)
|
||||
|
||||
public, _ := testhelpers.NewKratosServer(t, reg)
|
||||
|
||||
router := http.NewServeMux()
|
||||
ts := httptest.NewServer(router)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
testhelpers.NewRecoveryUIFlowEchoServer(t, reg)
|
||||
testhelpers.NewErrorTestServer(t, reg)
|
||||
|
||||
h := reg.RecoveryFlowErrorHandler()
|
||||
sdk := testhelpers.NewSDKClient(public)
|
||||
|
||||
var recoveryFlow *recovery.Flow
|
||||
var flowError error
|
||||
var methodName node.UiNodeGroup
|
||||
router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) {
|
||||
h.WriteFlowError(w, r, recoveryFlow, methodName, flowError)
|
||||
})
|
||||
|
||||
reset := func() {
|
||||
recoveryFlow = nil
|
||||
flowError = nil
|
||||
methodName = ""
|
||||
}
|
||||
|
||||
newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type) *recovery.Flow {
|
||||
req := &http.Request{URL: urlx.ParseOrPanic("/")}
|
||||
s, _, err := reg.GetActiveRecoveryStrategies(context.Background())
|
||||
require.NoError(t, err)
|
||||
f, err := recovery.NewFlow(reg, ttl, nosurfx.FakeCSRFToken, req, s, ft)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(context.Background(), f))
|
||||
f, err = reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), f.ID)
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) {
|
||||
res, err := ts.Client().Get(ts.URL + "/error")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=")
|
||||
|
||||
sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
return sse.Error, nil
|
||||
}
|
||||
|
||||
anHourAgo := time.Now().Add(-time.Hour)
|
||||
|
||||
t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
sse, _ := expectErrorUI(t)
|
||||
assertx.EqualAsJSON(t, flowError, sse)
|
||||
})
|
||||
|
||||
t.Run("case=error with nil flow detects application/json", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
assert.Contains(t, res.Header.Get("Content-Type"), "application/json")
|
||||
assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=")
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "system error")
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
n string
|
||||
t flow.Type
|
||||
}{
|
||||
{"api", flow.TypeAPI},
|
||||
{"spa", flow.TypeBrowser},
|
||||
} {
|
||||
t.Run("flow="+tc.n, func(t *testing.T) {
|
||||
t.Run("case=expired error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Contains(t, res.Request.URL.String(), public.URL+recovery.RouteGetFlow)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "%+v", res.Request)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), string(body))
|
||||
assert.NotEqual(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String())
|
||||
})
|
||||
|
||||
t.Run("case=validation error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = schema.NewInvalidCredentialsError()
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body)
|
||||
assert.Equal(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String())
|
||||
})
|
||||
|
||||
t.Run("case=generic error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw)
|
||||
})
|
||||
|
||||
t.Run("case=fails if active strategy is disabled", func(t *testing.T) {
|
||||
c, reg := pkg.NewVeryFastRegistryWithoutDB(t)
|
||||
require.NoError(t, c.Set(context.Background(), "selfservice.methods.code.enabled", false))
|
||||
require.NoError(t, c.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code"))
|
||||
_, _, err := reg.GetActiveRecoveryStrategies(context.Background())
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = err
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("id", "expires_at", "issued_at", "ui.action", "ui.nodes.0.attributes.value"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("flow=browser", func(t *testing.T) {
|
||||
expectRecoveryUI := func(t *testing.T) (*recovery.Flow, *http.Response) {
|
||||
res, err := ts.Client().Get(ts.URL + "/error")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()+"?flow=")
|
||||
|
||||
rf, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
return rf, res
|
||||
}
|
||||
|
||||
t.Run("case=expired error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser}
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
methodName = node.LinkGroup
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID))
|
||||
})
|
||||
|
||||
t.Run("case=validation error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser)
|
||||
flowError = schema.NewInvalidCredentialsError()
|
||||
methodName = node.LinkGroup
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf))
|
||||
require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf))
|
||||
})
|
||||
|
||||
t.Run("case=generic error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser)
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.LinkGroup
|
||||
|
||||
sse, _ := expectErrorUI(t)
|
||||
assertx.EqualAsJSON(t, flowError, sse)
|
||||
})
|
||||
|
||||
t.Run("case=new flow uses strategy of old flow", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "code"}
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID))
|
||||
assert.Equal(t, recoveryFlow.Active.String(), lf.Active.String())
|
||||
})
|
||||
|
||||
t.Run("case=new flow uses current strategy if strategy of old flow does not exist", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "not-valid"}
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID))
|
||||
assert.Equal(t, "code", lf.Active.String())
|
||||
})
|
||||
|
||||
t.Run("case=fails to retry flow if recovery strategy id is not valid", func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
reset()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "code")
|
||||
})
|
||||
|
||||
recoveryFlow = newFlow(t, 0, flow.TypeBrowser)
|
||||
recoveryFlow.Active = "not-valid"
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "not-valid")
|
||||
sse, _ := expectErrorUI(t)
|
||||
snapshotx.SnapshotT(t, sse)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleError_WithContinueWith(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
conf, reg := pkg.NewFastRegistryWithMocks(t,
|
||||
configx.WithValues(map[string]any{
|
||||
config.ViperKeyUseContinueWithTransitions: true,
|
||||
config.ViperKeySelfServiceRecoveryEnabled: true,
|
||||
config.ViperKeySelfServiceRecoveryUse: "code",
|
||||
}),
|
||||
)
|
||||
|
||||
public, _ := testhelpers.NewKratosServer(t, reg)
|
||||
|
||||
router := http.NewServeMux()
|
||||
ts := httptest.NewServer(router)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
testhelpers.NewRecoveryUIFlowEchoServer(t, reg)
|
||||
testhelpers.NewErrorTestServer(t, reg)
|
||||
|
||||
h := reg.RecoveryFlowErrorHandler()
|
||||
sdk := testhelpers.NewSDKClient(public)
|
||||
|
||||
var recoveryFlow *recovery.Flow
|
||||
var flowError error
|
||||
var methodName node.UiNodeGroup
|
||||
router.HandleFunc("GET /error", func(w http.ResponseWriter, r *http.Request) {
|
||||
h.WriteFlowError(w, r, recoveryFlow, methodName, flowError)
|
||||
})
|
||||
|
||||
reset := func() {
|
||||
recoveryFlow = nil
|
||||
flowError = nil
|
||||
methodName = ""
|
||||
}
|
||||
|
||||
newFlow := func(t *testing.T, ttl time.Duration, ft flow.Type) *recovery.Flow {
|
||||
req := &http.Request{URL: urlx.ParseOrPanic("/")}
|
||||
s, _, err := reg.GetActiveRecoveryStrategies(context.Background())
|
||||
require.NoError(t, err)
|
||||
f, err := recovery.NewFlow(reg, ttl, nosurfx.FakeCSRFToken, req, s, ft)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reg.RecoveryFlowPersister().CreateRecoveryFlow(context.Background(), f))
|
||||
f, err = reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), f.ID)
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
expectErrorUI := func(t *testing.T) (map[string]interface{}, *http.Response) {
|
||||
res, err := ts.Client().Get(ts.URL + "/error")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=")
|
||||
|
||||
sse, _, err := sdk.FrontendAPI.GetFlowError(context.Background()).Id(res.Request.URL.Query().Get("id")).Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
return sse.Error, nil
|
||||
}
|
||||
|
||||
anHourAgo := time.Now().Add(-time.Hour)
|
||||
|
||||
t.Run("case=error with nil flow defaults to error ui redirect", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
sse, _ := expectErrorUI(t)
|
||||
assertx.EqualAsJSON(t, flowError, sse)
|
||||
})
|
||||
|
||||
t.Run("case=error with nil flow detects application/json", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
assert.Contains(t, res.Header.Get("Content-Type"), "application/json")
|
||||
assert.NotContains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()+"?id=")
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), "system error")
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
n string
|
||||
t flow.Type
|
||||
}{
|
||||
{"api", flow.TypeAPI},
|
||||
{"spa", flow.TypeBrowser},
|
||||
} {
|
||||
t.Run("flow="+tc.n, func(t *testing.T) {
|
||||
t.Run("case=expired error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
body := ioutilx.MustReadAll(res.Body)
|
||||
switch tc.t {
|
||||
case flow.TypeAPI:
|
||||
require.Equal(t, http.StatusGone, res.StatusCode, "%s", body)
|
||||
require.Len(t, gjson.GetBytes(body, "error.details.continue_with").Array(), 1, "%s", body)
|
||||
require.Equal(t, "show_recovery_ui", gjson.GetBytes(body, "error.details.continue_with.0.action").String(), "%s", body)
|
||||
id := gjson.GetBytes(body, "error.details.continue_with.0.flow.id").String()
|
||||
res, err = ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, public.URL+recovery.RouteGetFlow+"?id="+id))
|
||||
require.NoError(t, err)
|
||||
body = ioutilx.MustReadAll(res.Body)
|
||||
case flow.TypeBrowser:
|
||||
require.Contains(t, res.Request.URL.String(), public.URL+recovery.RouteGetFlow, "%s", body)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "%+v", res.Request)
|
||||
}
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), string(body))
|
||||
assert.NotEqual(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String())
|
||||
})
|
||||
|
||||
t.Run("case=validation error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = schema.NewInvalidCredentialsError()
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(gjson.GetBytes(body, "ui.messages.0.id").Int()), "%s", body)
|
||||
assert.Equal(t, recoveryFlow.ID.String(), gjson.GetBytes(body, "id").String())
|
||||
})
|
||||
|
||||
t.Run("case=generic error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, x.MustEncodeJSON(t, flowError), gjson.GetBytes(body, "error").Raw)
|
||||
})
|
||||
|
||||
t.Run("case=fails if active strategy is disabled", func(t *testing.T) {
|
||||
c, reg := pkg.NewVeryFastRegistryWithoutDB(t)
|
||||
require.NoError(t, c.Set(context.Background(), "selfservice.methods.code.enabled", false))
|
||||
require.NoError(t, c.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code"))
|
||||
_, _, err := reg.GetActiveRecoveryStrategies(context.Background())
|
||||
recoveryFlow = newFlow(t, time.Minute, tc.t)
|
||||
flowError = err
|
||||
methodName = node.UiNodeGroup(recovery.RecoveryStrategyLink)
|
||||
|
||||
res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
snapshotx.SnapshotTJSON(t, body, snapshotx.ExceptPaths("id", "expires_at", "issued_at", "ui.action", "ui.nodes.0.attributes.value"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("flow=browser", func(t *testing.T) {
|
||||
expectRecoveryUI := func(t *testing.T) (*recovery.Flow, *http.Response) {
|
||||
res, err := ts.Client().Get(ts.URL + "/error")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowRecoveryUI(ctx).String()+"?flow=")
|
||||
|
||||
rf, err := reg.RecoveryFlowPersister().GetRecoveryFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow")))
|
||||
require.NoError(t, err)
|
||||
return rf, res
|
||||
}
|
||||
|
||||
t.Run("case=expired error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser}
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
methodName = node.LinkGroup
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID))
|
||||
})
|
||||
|
||||
t.Run("case=validation error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser)
|
||||
flowError = schema.NewInvalidCredentialsError()
|
||||
methodName = node.LinkGroup
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf))
|
||||
require.Len(t, lf.UI.Messages, 1, x.MustEncodeJSON(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationInvalidCredentials), int(lf.UI.Messages[0].ID), x.MustEncodeJSON(t, lf))
|
||||
})
|
||||
|
||||
t.Run("case=generic error", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser)
|
||||
flowError = herodot.ErrInternalServerError().WithReason("system error")
|
||||
methodName = node.LinkGroup
|
||||
|
||||
sse, _ := expectErrorUI(t)
|
||||
assertx.EqualAsJSON(t, flowError, sse)
|
||||
})
|
||||
|
||||
t.Run("case=new flow uses strategy of old flow", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "code"}
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID))
|
||||
assert.Equal(t, recoveryFlow.Active.String(), lf.Active.String())
|
||||
})
|
||||
|
||||
t.Run("case=new flow uses current strategy if strategy of old flow does not exist", func(t *testing.T) {
|
||||
t.Cleanup(reset)
|
||||
|
||||
recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser, Active: "not-valid"}
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
|
||||
lf, _ := expectRecoveryUI(t)
|
||||
require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
|
||||
assert.Equal(t, int(text.ErrorValidationRecoveryFlowExpired), int(lf.UI.Messages[0].ID))
|
||||
assert.Equal(t, "code", lf.Active.String())
|
||||
})
|
||||
|
||||
t.Run("case=fails to retry flow if recovery strategy id is not valid", func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
reset()
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "code")
|
||||
})
|
||||
|
||||
recoveryFlow = newFlow(t, 0, flow.TypeBrowser)
|
||||
recoveryFlow.Active = "not-valid"
|
||||
flowError = flow.NewFlowExpiredError(clock.New(), anHourAgo)
|
||||
|
||||
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryUse, "not-valid")
|
||||
sse, _ := expectErrorUI(t)
|
||||
snapshotx.SnapshotT(t, sse)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user