Files
upstream-ory-keto/internal/relationtuple/uuid_mapping_test.go
upstream-mirror 9456a4e922
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (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/keto.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:08:34 +05:00

403 lines
10 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package relationtuple_test
import (
"context"
"fmt"
"slices"
"testing"
"github.com/ory/herodot"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/keto/internal/driver"
"github.com/ory/keto/internal/driver/config"
"github.com/ory/keto/internal/namespace"
"github.com/ory/keto/internal/relationtuple"
"github.com/ory/keto/ketoapi"
)
func runWithMappers(t *testing.T, p relationtuple.MapperProvider, runner func(t *testing.T, m *relationtuple.Mapper)) {
t.Run("mapper=readwrite", func(t *testing.T) { runner(t, p.Mapper()) })
t.Run("mapper=readonly", func(t *testing.T) { runner(t, p.ReadOnlyMapper()) })
}
func TestMapper(t *testing.T) {
ctx := context.Background()
reg := driver.NewSqliteTestRegistry(t)
nspace := namespace.Namespace{
Name: "test",
}
nspace2 := namespace.Namespace{
Name: "test2",
}
require.NoError(t, reg.Config(ctx).Set(config.KeyNamespaces, []*namespace.Namespace{&nspace, &nspace2}))
t.Run("items=relationships", func(t *testing.T) {
for _, tc := range []struct {
name string
rts []*ketoapi.RelationTuple
err error
}{
{
name: "no relationships",
rts: []*ketoapi.RelationTuple{},
},
{
name: "one relation tuple",
rts: []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "object",
Relation: "relation",
SubjectID: new("subject"),
},
},
},
{
name: "relation tuple without subject",
rts: []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "object",
Relation: "relation",
},
},
err: ketoapi.ErrNilSubject(),
},
{
name: "many relationships",
rts: func() []*ketoapi.RelationTuple {
rts := make([]*ketoapi.RelationTuple, 10)
for i := range rts {
rts[i] = &ketoapi.RelationTuple{
Namespace: nspace.Name,
Object: fmt.Sprintf("object %d", i),
Relation: "relation",
SubjectID: new("subject"),
}
}
return rts
}(),
},
{
name: "unknown namespace",
rts: []*ketoapi.RelationTuple{
{
Namespace: "unknown",
Object: "object",
Relation: "relation",
SubjectID: new("subject"),
},
},
err: herodot.ErrNotFound(),
},
} {
t.Run(tc.name, func(t *testing.T) {
runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) {
mapped, err := m.FromTuple(ctx, tc.rts...)
require.ErrorIs(t, err, tc.err)
if tc.err != nil {
// the rest only makes sense if we have no error
return
}
assert.Len(t, mapped, len(tc.rts))
actual, err := m.ToTuple(ctx, mapped...)
require.NoError(t, err)
assert.ElementsMatch(t, tc.rts, actual)
})
})
}
})
t.Run("item=relation query", func(t *testing.T) {
for _, tc := range []struct {
name string
query *ketoapi.RelationQuery
err error
}{
{
name: "all fields nil",
query: &ketoapi.RelationQuery{},
},
{
name: "all fields set",
query: &ketoapi.RelationQuery{
Namespace: new(nspace.Name),
Object: new("object"),
Relation: new("relation"),
SubjectID: new("subject"),
},
},
{
name: "subject set",
query: &ketoapi.RelationQuery{
SubjectSet: &ketoapi.SubjectSet{
Namespace: nspace.Name,
Object: "object",
Relation: "relation",
},
},
},
{
name: "non-mapped fields",
query: &ketoapi.RelationQuery{
Relation: new("relation"),
SubjectSet: &ketoapi.SubjectSet{
Namespace: nspace.Name,
Relation: "relation",
Object: "object",
},
},
},
{
name: "unknown namespace",
query: &ketoapi.RelationQuery{
Namespace: new("unknown"),
},
err: herodot.ErrNotFound(),
},
} {
t.Run(tc.name, func(t *testing.T) {
runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) {
mapped, err := m.FromQuery(ctx, tc.query)
require.ErrorIs(t, err, tc.err)
if tc.err != nil {
// the rest only makes sense if we have no error
return
}
actual, err := m.ToQuery(ctx, mapped)
require.NoError(t, err)
assert.Equal(t, tc.query, actual)
})
})
}
})
t.Run("item=subject set", func(t *testing.T) {
for _, tc := range []struct {
name string
set *ketoapi.SubjectSet
err error
}{
{
name: "basic subject set",
set: &ketoapi.SubjectSet{
Namespace: nspace.Name,
Object: "object",
Relation: "relation",
},
},
{
name: "unknown namespace",
set: &ketoapi.SubjectSet{
Namespace: "unknown",
Object: "object",
Relation: "relation",
},
err: herodot.ErrNotFound(),
},
} {
t.Run(tc.name, func(t *testing.T) {
runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) {
mapped, err := reg.Mapper().FromSubjectSet(ctx, tc.set)
require.ErrorIs(t, err, tc.err)
if tc.err != nil {
// the rest only makes sense if we have no error
return
}
assert.NotZero(t, mapped.Object)
assert.Equal(t, nspace.Name, mapped.Namespace)
assert.Equal(t, tc.set.Relation, mapped.Relation)
})
})
}
})
t.Run("item=expand tree", func(t *testing.T) {
strs := make([]string, 3)
for i := range strs {
strs[i] = fmt.Sprintf("foo %d", i)
}
uuids, err := reg.MappingManager().MapStringsToUUIDs(ctx, strs...)
require.NoError(t, err)
for _, tc := range []struct {
name string
tree *relationtuple.Tree
err error
}{
{
name: "basic tree",
tree: &relationtuple.Tree{
Type: ketoapi.TreeNodeLeaf,
Subject: &relationtuple.SubjectID{ID: uuids[0]},
},
},
{
name: "basic tree with children",
tree: &relationtuple.Tree{
Type: ketoapi.TreeNodeUnion,
Subject: &relationtuple.SubjectSet{
Namespace: nspace.Name,
Object: uuids[0],
Relation: "members",
},
Children: []*relationtuple.Tree{
{
Type: ketoapi.TreeNodeLeaf,
Subject: &relationtuple.SubjectID{ID: uuids[1]},
},
{
Type: ketoapi.TreeNodeLeaf,
Subject: &relationtuple.SubjectID{ID: uuids[2]},
},
},
},
},
{
name: "deeply nested tree",
tree: &relationtuple.Tree{
Type: ketoapi.TreeNodeUnion,
Subject: &relationtuple.SubjectSet{
Namespace: nspace.Name,
Object: uuids[0],
Relation: "members",
},
Children: []*relationtuple.Tree{
{
Type: ketoapi.TreeNodeUnion,
Subject: &relationtuple.SubjectSet{
Namespace: nspace.Name,
Object: uuids[1],
Relation: "members",
},
Children: []*relationtuple.Tree{
{
Type: ketoapi.TreeNodeLeaf,
Subject: &relationtuple.SubjectID{ID: uuids[2]},
},
},
},
},
},
},
{
name: "deeply nested tree with different namespaces",
tree: &relationtuple.Tree{
Type: ketoapi.TreeNodeUnion,
Subject: &relationtuple.SubjectSet{
Namespace: nspace.Name,
Object: uuids[0],
Relation: "members",
},
Children: []*relationtuple.Tree{
{
Type: ketoapi.TreeNodeUnion,
Subject: &relationtuple.SubjectSet{
Namespace: nspace2.Name,
Object: uuids[1],
Relation: "owners",
},
Children: []*relationtuple.Tree{
{
Type: ketoapi.TreeNodeLeaf,
Subject: &relationtuple.SubjectID{ID: uuids[2]},
},
},
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
mapped, err := reg.Mapper().ToTree(ctx, tc.tree)
require.ErrorIs(t, err, tc.err)
if tc.err != nil {
// the rest only makes sense if we have no error
return
}
var checkTree func(*ketoapi.Tree[*ketoapi.RelationTuple], *relationtuple.Tree)
checkTree = func(mapped *ketoapi.Tree[*ketoapi.RelationTuple], original *relationtuple.Tree) {
switch s := original.Subject.(type) {
case *relationtuple.SubjectID:
require.NotNil(t, mapped.Tuple.SubjectID)
assert.Nil(t, mapped.Tuple.SubjectSet)
assert.Equal(t, strs[slices.Index(uuids, s.ID)], *mapped.Tuple.SubjectID)
case *relationtuple.SubjectSet:
require.NotNil(t, mapped.Tuple.SubjectSet)
assert.Nil(t, mapped.Tuple.SubjectID)
assert.Equal(t, s.Namespace, mapped.Tuple.SubjectSet.Namespace)
assert.Equal(t, strs[slices.Index(uuids, s.Object)], mapped.Tuple.SubjectSet.Object)
assert.Equal(t, s.Relation, mapped.Tuple.SubjectSet.Relation)
default:
t.Fatalf("expected subject to be set: %+v", mapped)
}
assert.Equal(t, original.Type, mapped.Type)
require.Len(t, mapped.Children, len(original.Children))
for i := range original.Children {
checkTree(mapped.Children[i], original.Children[i])
}
}
checkTree(mapped, tc.tree)
})
}
})
t.Run("suite=ro and rw mappers", func(t *testing.T) {
roMapper := reg.ReadOnlyMapper()
rwMapper := reg.Mapper()
t.Run("case=ro manager does not insert into DB", func(t *testing.T) {
unmapped := &ketoapi.RelationQuery{Object: new("unmapped object")}
mapped, err := roMapper.FromQuery(ctx, unmapped)
require.NoError(t, err)
assert.NotNil(t, mapped.Object)
reUnmapped, err := roMapper.ToQuery(ctx, mapped)
require.NoError(t, err)
assert.Empty(t, reUnmapped.Object)
})
t.Run("case=rw manager inserts into DB", func(t *testing.T) {
unmapped := &ketoapi.RelationQuery{Object: new("another unmapped object")}
mapped, err := rwMapper.FromQuery(ctx, unmapped)
require.NoError(t, err)
assert.NotNil(t, mapped.Object)
reUnmapped, err := rwMapper.ToQuery(ctx, mapped)
require.NoError(t, err)
assert.NotEmpty(t, reUnmapped.Object)
})
})
}
func BenchmarkReadOnlyMapper(b *testing.B) {
ctx := context.Background()
reg := driver.NewSqliteTestRegistry(b, driver.WithNamespaces([]*namespace.Namespace{{Name: "test"}}))
m := reg.ReadOnlyMapper()
b.Run("FromTuple", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := m.FromTuple(ctx, &ketoapi.RelationTuple{
Namespace: "test",
Object: "object",
Relation: "relation",
SubjectSet: &ketoapi.SubjectSet{
Namespace: "test",
Object: "subject object",
Relation: "relation",
},
})
assert.NoError(b, err)
}
})
}