Files
upstream-ory-kratos/ui/node/node_test.go
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

561 lines
16 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package node_test
import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"path/filepath"
"testing"
"github.com/ory/x/snapshotx"
"github.com/ory/kratos/text"
"github.com/ory/x/assertx"
"github.com/ory/kratos/corpx"
"github.com/ory/kratos/ui/container"
"github.com/go-faker/faker/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/jsonschema/v3"
"github.com/ory/kratos/ui/node"
)
func init() {
corpx.RegisterFakes()
}
//go:embed fixtures/sort/*
var sortFixtures embed.FS
var ctx = context.Background()
func TestNodesSort(t *testing.T) {
// use a schema compiler that disables identifiers
schemaCompiler := jsonschema.NewCompiler()
schemaPath := "fixtures/identity.schema.json"
f, err := container.NewFromJSONSchema(ctx, "/foo", node.DefaultGroup, schemaPath, "", schemaCompiler, false)
require.NoError(t, err)
f.UpdateNodeValuesFromJSON(json.RawMessage(`{}`), "traits", node.DefaultGroup)
f.SetCSRF("csrf_token")
inputs, err := sortFixtures.ReadDir("fixtures/sort/input")
require.NoError(t, err)
options := map[string][]node.SortOption{
"1.json": {
node.SortUseOrder([]string{"password_identifier"}),
node.SortUpdateOrder(node.PasswordLoginOrder),
node.SortByGroups([]node.UiNodeGroup{
node.DefaultGroup,
node.ProfileGroup,
node.OpenIDConnectGroup,
node.PasswordGroup,
node.LinkGroup,
node.LinkGroup,
}),
},
"2.json": {
node.SortBySchema(filepath.Join("fixtures/sort/schema", "2.json")),
node.SortUpdateOrder(node.PasswordLoginOrder),
node.SortByGroups([]node.UiNodeGroup{
node.DefaultGroup,
node.OpenIDConnectGroup,
node.PasswordGroup,
}),
},
"3.json": {
node.SortBySchema(filepath.Join("fixtures/sort/schema", "3.json")),
node.SortByGroups([]node.UiNodeGroup{
node.DefaultGroup,
node.OpenIDConnectGroup,
node.PasswordGroup,
}),
},
"4.json": {
node.SortBySchema(filepath.Join("fixtures/sort/schema", "4.json")),
node.SortByGroups([]node.UiNodeGroup{
node.DefaultGroup,
node.ProfileGroup,
node.PasswordGroup,
node.OpenIDConnectGroup,
node.LookupGroup,
node.WebAuthnGroup,
node.TOTPGroup,
}),
node.SortUseOrderAppend([]string{
// Lookup
node.LookupReveal,
node.LookupRegenerate,
node.LookupCodes,
node.LookupConfirm,
// Lookup
node.WebAuthnRemove,
node.WebAuthnRegisterDisplayName,
node.WebAuthnRegister,
// TOTP
node.TOTPSecretKey,
node.TOTPQR,
node.TOTPUnlink,
node.TOTPCode,
}),
},
}
for _, in := range inputs {
t.Run("file="+in.Name(), func(t *testing.T) {
if in.IsDir() {
return
}
fi, err := sortFixtures.Open(filepath.Join("fixtures/sort/input", in.Name()))
require.NoError(t, err)
defer func() { _ = fi.Close() }()
var nodes node.Nodes
require.NoError(t, json.NewDecoder(fi).Decode(&nodes))
require.NotEmpty(t, nodes)
require.NoError(t, nodes.SortBySchema(ctx, options[in.Name()]...))
fe, err := sortFixtures.ReadFile(filepath.Join("fixtures/sort/expected", in.Name()))
require.NoError(t, err)
assertx.EqualAsJSON(t, json.RawMessage(fe), nodes)
})
}
}
func TestNodesSortStableGroups(t *testing.T) {
t.Parallel()
// Build count nodes in the same group, sharing an input name (as a list of
// remove buttons does). Insertion order is 0..count-1, recorded in each
// node's label; the value is the reverse, so a value tiebreak would flip the
// order. count exceeds Go's insertion-sort threshold (12) so the sort takes
// the quicksort path, where an unstable sort could otherwise reshuffle
// same-group nodes.
const count = 20
build := func() node.Nodes {
var n node.Nodes
for i := range count {
value := fmt.Sprintf("%02d", count-1-i)
n = append(n, node.NewInputField(node.DeviceAuthnRemove, value, node.DeviceAuthnGroup, node.InputAttributeTypeSubmit).
WithMetaLabel(&text.Message{Text: fmt.Sprintf("%02d", i)}))
}
return n
}
labels := func(n node.Nodes) []string {
got := make([]string, 0, len(n))
for _, nn := range n {
got = append(got, nn.Meta.Label.Text)
}
return got
}
insertionOrder := make([]string, count)
valueOrder := make([]string, count)
for i := range count {
insertionOrder[i] = fmt.Sprintf("%02d", i)
valueOrder[i] = fmt.Sprintf("%02d", count-1-i)
}
t.Run("case=stable group preserves insertion order", func(t *testing.T) {
t.Parallel()
n := build()
require.NoError(t, n.SortBySchema(ctx,
node.SortByGroups([]node.UiNodeGroup{node.DefaultGroup, node.PasswordGroup}),
node.SortStableGroups(node.DeviceAuthnGroup),
))
assert.Equal(t, insertionOrder, labels(n))
})
t.Run("case=without the option the value tiebreak reorders", func(t *testing.T) {
t.Parallel()
n := build()
require.NoError(t, n.SortBySchema(ctx,
node.SortByGroups([]node.UiNodeGroup{node.DefaultGroup, node.PasswordGroup}),
))
assert.Equal(t, valueOrder, labels(n))
})
}
func TestNodesUpsert(t *testing.T) {
var nodes node.Nodes
nodes.Upsert(node.NewCSRFNode("foo"))
require.Len(t, nodes, 1)
nodes.Upsert(node.NewCSRFNode("bar"))
require.Len(t, nodes, 1)
assert.EqualValues(t, "bar", nodes[0].Attributes.GetValue())
}
func TestNodesRemove(t *testing.T) {
var nodes node.Nodes
nodes.Append(node.NewInputField("other", "foo", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
nodes.Append(node.NewInputField("link", "foo", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
nodes.Append(node.NewInputField("link", "bar", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
nodes.Append(node.NewInputField("unlink", "baz", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
require.Len(t, nodes, 4)
nodes.Remove("link", "unlink")
require.Len(t, nodes, 1)
}
func TestNodesRemoveInGroup(t *testing.T) {
samlLink := node.NewInputField("link", "saml-provider", node.SAMLGroup, node.InputAttributeTypeSubmit)
nodes := node.Nodes{
node.NewInputField("link", "oidc-provider", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit),
samlLink,
node.NewInputField("unlink", "oidc-provider", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit),
node.NewCSRFNode("token"),
}
// Remove only the OIDC group's link/unlink nodes.
nodes.RemoveInGroup(node.OpenIDConnectGroup, "link", "unlink")
require.Len(t, nodes, 2)
// The SAML link node shares the "link" name but a different group: it survives.
assert.Contains(t, nodes, samlLink)
// The CSRF node (DefaultGroup) is untouched.
var sawCSRF bool
for _, n := range nodes {
if n.Group == node.DefaultGroup {
sawCSRF = true
}
}
assert.True(t, sawCSRF, "CSRF node must not be removed")
}
func TestNodeJSON(t *testing.T) {
t.Run("idempotent decode", func(t *testing.T) {
nodes := make(node.Nodes, 5)
for k := range nodes {
nodes[k] = new(node.Node)
expected := nodes[k]
require.NoError(t, faker.FakeData(expected))
var b bytes.Buffer
require.NoError(t, json.NewEncoder(&b).Encode(expected))
var actual node.Node
require.NoError(t, json.NewDecoder(&b).Decode(&actual))
assert.EqualValues(t, *expected, actual)
assert.NotEmpty(t, actual.Type)
assert.NotNil(t, actual.Attributes)
}
})
t.Run("type mismatch", func(t *testing.T) {
n := &node.Node{Type: node.Image, Attributes: new(node.InputAttributes)}
var b bytes.Buffer
require.EqualError(t, json.NewEncoder(&b).Encode(n), "json: error calling MarshalJSON for type *node.Node: node type and node attributes mismatch: *node.InputAttributes != img")
})
t.Run("type empty", func(t *testing.T) {
n := &node.Node{Attributes: new(node.InputAttributes)}
var b bytes.Buffer
require.NoError(t, json.NewEncoder(&b).Encode(n))
assert.EqualValues(t, node.Input, gjson.GetBytes(b.Bytes(), "type").String())
})
t.Run("type decode unknown", func(t *testing.T) {
var n node.Node
require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo")
})
}
func TestMatchesNode(t *testing.T) {
// Test when ID is different
node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
assert.False(t, node1.Matches(node2))
// Test when Type is different
node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
assert.False(t, node1.Matches(node2))
// Test when Group is different
node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}}
assert.False(t, node1.Matches(node2))
// Test when all fields are the same
node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
assert.True(t, node1.Matches(node2))
}
func TestRemoveMatchingNodes(t *testing.T) {
nodes := node.Nodes{
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}},
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}},
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}},
}
// Test when node to remove is present
nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
nodes.RemoveMatching(nodeToRemove)
assert.Len(t, nodes, 2)
for _, n := range nodes {
assert.NotEqual(t, nodeToRemove.ID(), n.ID())
}
// Test when node to remove is not present
nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}}
nodes.RemoveMatching(nodeToRemove)
assert.Len(t, nodes, 2) // length should remain the same
// Test when node to remove is present
nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}
ui := &container.Container{
Nodes: nodes,
}
ui.GetNodes().RemoveMatching(nodeToRemove)
assert.Len(t, *ui.GetNodes(), 1)
for _, n := range *ui.GetNodes() {
assert.NotEqual(t, "bar", n.ID())
assert.NotEqual(t, "baz", n.ID())
}
ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
assert.NotNil(t, ui.Nodes.Find("method"))
ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit))
assert.Nil(t, ui.Nodes.Find("method"))
}
func TestNodeMarshalJSON(t *testing.T) {
tests := []struct {
name string
node *node.Node
wantErr bool
errMsg string
}{
{
name: "text node",
node: &node.Node{
Type: node.Text,
Group: node.DefaultGroup,
Attributes: &node.TextAttributes{
NodeType: node.Text,
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "input node",
node: &node.Node{
Type: node.Input,
Group: node.DefaultGroup,
Attributes: &node.InputAttributes{
NodeType: node.Input,
Name: "password",
Type: "password",
FieldValue: "secret",
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "anchor node",
node: &node.Node{
Type: node.Anchor,
Group: node.DefaultGroup,
Attributes: &node.AnchorAttributes{
NodeType: node.Anchor,
HREF: "https://example.com",
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "image node",
node: &node.Node{
Type: node.Image,
Group: node.DefaultGroup,
Attributes: &node.ImageAttributes{
NodeType: node.Image,
Source: "image.jpg",
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "script node",
node: &node.Node{
Type: node.Script,
Group: node.DefaultGroup,
Attributes: &node.ScriptAttributes{
NodeType: node.Script,
Source: "script.js",
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "division node",
node: &node.Node{
Type: node.Division,
Group: node.DefaultGroup,
Attributes: &node.DivisionAttributes{
NodeType: node.Division,
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "type mismatch",
node: &node.Node{
Type: node.Image,
Group: node.DefaultGroup,
Attributes: &node.InputAttributes{NodeType: node.Input},
},
wantErr: true,
errMsg: "node type and node attributes mismatch",
},
{
name: "empty type inferred from attributes",
node: &node.Node{
Group: node.DefaultGroup,
Attributes: &node.InputAttributes{
NodeType: node.Input,
Name: "email",
},
Messages: text.Messages{},
Meta: &node.Meta{},
},
},
{
name: "nil attributes",
node: &node.Node{
Type: node.Image,
Group: node.DefaultGroup,
Attributes: nil,
Messages: text.Messages{},
},
wantErr: true,
errMsg: "node type and node attributes mismatch",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.node)
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
return
}
require.NoError(t, err)
// Use snapshotx for testing serialization
snapshotx.SnapshotT(t, json.RawMessage(data))
// Verify roundtrip
var unmarshalled node.Node
err = json.Unmarshal(data, &unmarshalled)
require.NoError(t, err)
// Re-marshal for comparison
remarshalled, err := json.Marshal(&unmarshalled)
require.NoError(t, err)
assert.JSONEq(t, string(data), string(remarshalled))
})
}
}
func TestNodes_FindAll(t *testing.T) {
nodes := node.Nodes{
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "a"}},
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "b"}},
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "a"}},
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "c"}},
}
t.Run("find matches multiple nodes", func(t *testing.T) {
got := nodes.FindAll("a")
assert.Equal(t, []node.Node{*nodes[0], *nodes[2]}, got)
})
t.Run("find matches one node", func(t *testing.T) {
got := nodes.FindAll("b")
assert.Equal(t, []node.Node{*nodes[1]}, got)
})
t.Run("no match returns empty slice", func(t *testing.T) {
got := nodes.FindAll("x")
assert.Empty(t, got)
})
}
func TestNodes_RemoveGroup(t *testing.T) {
nodes := node.Nodes{
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "default"}},
&node.Node{Type: node.Input, Group: node.CaptchaGroup, Attributes: &node.InputAttributes{Name: "captcha1"}},
&node.Node{Type: node.Input, Group: node.IdentifierFirstGroup, Attributes: &node.InputAttributes{Name: "idfirst"}},
&node.Node{Type: node.Input, Group: node.CaptchaGroup, Attributes: &node.InputAttributes{Name: "captcha2"}},
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "password"}},
nil,
}
t.Run("removes single node", func(t *testing.T) {
n := append(node.Nodes(nil), nodes...)
n.RemoveGroup(node.IdentifierFirstGroup)
assert.Equal(t, node.Nodes{nodes[0], nodes[1], nodes[3], nodes[4]}, n)
})
t.Run("removes multiple nodes", func(t *testing.T) {
n := append(node.Nodes(nil), nodes...)
n.RemoveGroup(node.CaptchaGroup)
assert.Equal(t, node.Nodes{nodes[0], nodes[2], nodes[4]}, n)
})
t.Run("removes no nodes", func(t *testing.T) {
n := append(node.Nodes(nil), nodes...)
n.RemoveGroup(node.PasskeyGroup)
assert.Equal(t, nodes[:5], n)
})
t.Run("removes multiple groups", func(t *testing.T) {
n := append(node.Nodes(nil), nodes...)
n.RemoveGroups(node.CaptchaGroup, node.PasswordGroup)
assert.Equal(t, node.Nodes{nodes[0], nodes[2]}, n)
})
t.Run("removes multiple groups empty", func(t *testing.T) {
n := append(node.Nodes(nil), nodes...)
n.RemoveGroups()
assert.Equal(t, nodes, n)
})
t.Run("removes multiple groups no matching", func(t *testing.T) {
n := append(node.Nodes(nil), nodes...)
n.RemoveGroups(node.PasskeyGroup)
assert.Equal(t, nodes[:5], n)
})
}