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
403 lines
10 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|