snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

View File

@@ -0,0 +1,401 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/herodot"
"github.com/ory/keto/internal/expand"
"github.com/ory/keto/internal/namespace"
"github.com/ory/keto/ketoapi"
)
func runCases(c client, m *namespaceTestManager) func(*testing.T) {
return func(t *testing.T) {
c.waitUntilLive(t)
t.Run("case=list namespaces", func(t *testing.T) {
first := namespace.Namespace{Name: "my namespace"}
second := namespace.Namespace{Name: "my other namespace"}
m.add(t, &first, &second)
resp := c.queryNamespaces(t)
assert.GreaterOrEqual(t, len(resp.Namespaces), 2)
assertNamespacesContains(t, resp.Namespaces, "my namespace")
assertNamespacesContains(t, resp.Namespaces, "my other namespace")
})
t.Run("case=gets empty namespace", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
assert.Len(t, resp.RelationTuples, 0)
})
t.Run("case=creates tuple and uses it then", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
tuple := &ketoapi.RelationTuple{
Namespace: n.Name,
Object: fmt.Sprintf("object for client %T", c),
Relation: "access",
SubjectID: new("client"),
}
c.createTuple(t, tuple)
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &tuple.Namespace})
require.Len(t, resp.RelationTuples, 1)
assert.Equal(t, tuple, resp.RelationTuples[0])
// try the check API to see whether the tuple is interpreted correctly
assert.True(t, c.check(t, tuple))
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{tuple})
require.Len(t, batchResult, 1)
assert.True(t, batchResult[0].allowed)
assert.Empty(t, batchResult[0].errorMessage)
})
t.Run("case=creates tuple with empty IDs", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
tuples := []*ketoapi.RelationTuple{{
Namespace: n.Name,
Object: "",
Relation: "access",
SubjectID: new(""),
}, {
Namespace: n.Name,
Object: "",
Relation: "access",
SubjectSet: &ketoapi.SubjectSet{
Namespace: n.Name,
Object: "",
Relation: "access",
},
}}
for _, tp := range tuples {
c.createTuple(t, tp)
// try the check API to see whether the tuple is interpreted correctly
assert.True(t, c.check(t, tp))
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{tp})
require.Len(t, batchResult, 1)
assert.True(t, batchResult[0].allowed)
assert.Empty(t, batchResult[0].errorMessage)
}
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
assert.ElementsMatch(t, tuples, resp.RelationTuples)
})
t.Run("case=check subjectSet relations", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
obj := fmt.Sprintf("obj for client %T", c)
rel := "check"
rt := &ketoapi.RelationTuple{
Namespace: n.Name,
Object: obj,
Relation: rel,
SubjectSet: &ketoapi.SubjectSet{
Namespace: n.Name,
Object: obj,
Relation: rel,
},
}
c.createTuple(t, rt)
assert.True(t, c.check(t, rt))
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{rt})
require.Len(t, batchResult, 1)
assert.True(t, batchResult[0].allowed)
assert.Empty(t, batchResult[0].errorMessage)
})
t.Run("case=batch check", func(t *testing.T) {
/*
Test batch check with four cases:
- Allowed single subject tuple
- Allowed subject set tuple
- Tuple with unknown namespace (will return an error for this case)
- Dis-allowed tuple
*/
namespace1 := &namespace.Namespace{Name: t.Name()}
namespace2 := &namespace.Namespace{Name: t.Name() + "-2"}
m.add(t, namespace1)
m.add(t, namespace2)
obj1 := fmt.Sprintf("obj for client %T", c)
obj2 := fmt.Sprintf("another obj for client %T", c)
rel1 := "check"
rel2 := "access"
tupleSubjectSet := &ketoapi.RelationTuple{
Namespace: namespace1.Name,
Object: obj1,
Relation: rel1,
SubjectSet: &ketoapi.SubjectSet{
Namespace: namespace1.Name,
Object: obj1,
Relation: rel1,
},
}
c.createTuple(t, tupleSubjectSet)
tupleSingleSubject := &ketoapi.RelationTuple{
Namespace: namespace2.Name,
Object: obj2,
Relation: rel2,
SubjectID: new("sub1"),
}
c.createTuple(t, tupleSingleSubject)
unknownNamespaceTuple := &ketoapi.RelationTuple{
Namespace: "unknown-namespace",
Object: obj1,
Relation: rel1,
SubjectID: new("sub1"),
}
unknownSubjectTuple := &ketoapi.RelationTuple{
Namespace: namespace1.Name,
Object: obj1,
Relation: rel1,
SubjectID: new("unknown-sub"),
}
batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{
tupleSubjectSet, tupleSingleSubject,
unknownNamespaceTuple, unknownSubjectTuple,
})
require.Len(t, batchResult, 4)
assert.True(t, batchResult[0].allowed)
assert.Empty(t, batchResult[0].errorMessage)
assert.True(t, batchResult[1].allowed)
assert.Empty(t, batchResult[1].errorMessage)
assert.False(t, batchResult[2].allowed)
assert.Contains(t, batchResult[2].errorMessage, "The requested resource could not be found")
assert.False(t, batchResult[3].allowed)
assert.Empty(t, batchResult[3].errorMessage)
// Verify a call with no tuples returns successfully with no results
emptyResults := c.batchCheck(t, []*ketoapi.RelationTuple{})
require.Empty(t, emptyResults)
})
t.Run("case=batch check validation errors", func(t *testing.T) {
// Pass in 11 tuples to check, more than the default limit of 10, and verify the request is rejected
tuples := make([]*ketoapi.RelationTuple, 11)
for i := range tuples {
tuples[i] = &ketoapi.RelationTuple{
Namespace: "namespace-name",
Object: "obj",
Relation: "rel",
SubjectID: new("sub"),
}
}
c.batchCheckErr(t, tuples, herodot.ErrBadRequest())
})
t.Run("case=expand API", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
obj := fmt.Sprintf("tree for client %T", c)
rel := "expand"
subjects := []string{"s1", "s2"}
expectedTree := &ketoapi.Tree[*ketoapi.RelationTuple]{
Type: ketoapi.TreeNodeUnion,
Tuple: &ketoapi.RelationTuple{
SubjectSet: &ketoapi.SubjectSet{
Namespace: n.Name,
Object: obj,
Relation: rel,
},
},
Children: make([]*ketoapi.Tree[*ketoapi.RelationTuple], len(subjects)),
}
for i, subjectID := range subjects {
c.createTuple(t, &ketoapi.RelationTuple{
Namespace: n.Name,
Object: obj,
Relation: rel,
SubjectID: &subjectID,
})
expectedTree.Children[i] = &ketoapi.Tree[*ketoapi.RelationTuple]{
Type: ketoapi.TreeNodeLeaf,
Tuple: &ketoapi.RelationTuple{
SubjectID: &subjectID,
},
}
}
actualTree := c.expand(t, expectedTree.Tuple.SubjectSet, 100)
assert.Equal(t, expectedTree.Type, actualTree.Type)
assert.Equal(t, expectedTree.Tuple, actualTree.Tuple)
assert.Len(t, actualTree.Children, len(expectedTree.Children))
expand.AssertExternalTreesAreEqual(t, expectedTree, actualTree)
})
t.Run("case=gets result paginated", func(t *testing.T) {
const nTuples = 10
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
rel := fmt.Sprintf("some unique relation %T", c)
for i := 0; i < nTuples; i++ {
c.createTuple(t, &ketoapi.RelationTuple{
Namespace: n.Name,
Object: "o" + strconv.Itoa(i),
Relation: rel,
SubjectID: new("s" + strconv.Itoa(i)),
})
}
var (
resp ketoapi.GetResponse
nPages int
)
// do ... while resp.NextPageToken != ""
for ok := true; ok; ok = resp.NextPageToken != "" {
resp = *c.queryTuple(t,
&ketoapi.RelationQuery{
Namespace: &n.Name,
Relation: &rel,
},
withToken(resp.NextPageToken),
withSize(1),
)
nPages++
assert.Len(t, resp.RelationTuples, 1)
}
assert.Equal(t, nTuples, nPages)
})
t.Run("case=deletes tuple", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
for _, rt := range []*ketoapi.RelationTuple{
{
SubjectID: new("s"),
},
{
SubjectSet: &ketoapi.SubjectSet{
Namespace: n.Name,
Object: "so",
Relation: "rel",
},
},
} {
t.Run(fmt.Sprintf("subject_id=%v", rt.SubjectID == nil), func(t *testing.T) {
rt.Namespace = n.Name
rt.Object = "o"
rt.Relation = "rel"
c.createTuple(t, rt)
resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
assert.Equal(t, []*ketoapi.RelationTuple{rt}, resp.RelationTuples)
c.deleteTuple(t, rt)
resp = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name})
assert.Len(t, resp.RelationTuples, 0)
})
}
})
t.Run("case=deletes tuples based on relation query", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
rts := []*ketoapi.RelationTuple{
{
Namespace: n.Name,
Object: "o1",
Relation: "rel",
SubjectID: new("s1"),
},
{
Namespace: n.Name,
Object: "o2",
Relation: "rel",
SubjectID: new("s2"),
},
}
for i := 0; i < len(rts); i++ {
c.createTuple(t, rts[i])
}
q := &ketoapi.RelationQuery{
Namespace: &n.Name,
Relation: new("rel"),
}
resp := c.queryTuple(t, q)
require.ElementsMatch(t, resp.RelationTuples, rts)
c.deleteAllTuples(t, q)
resp = c.queryTuple(t, q)
assert.Equal(t, 0, len(resp.RelationTuples))
})
t.Run("case=returns error with status code on unknown namespace", func(t *testing.T) {
c.queryTupleErr(t, herodot.ErrNotFound(), &ketoapi.RelationQuery{Namespace: new("unknown namespace")})
})
t.Run("case=still serves tuples from deleted namespace", func(t *testing.T) {
n := &namespace.Namespace{Name: t.Name()}
m.add(t, n)
tuple := &ketoapi.RelationTuple{
Namespace: n.Name,
Object: "o",
Relation: "rel",
SubjectID: new("s"),
}
c.createTuple(t, tuple)
m.remove(t, n.Name)
resp := c.queryTuple(t, &ketoapi.RelationQuery{})
assert.Equal(t, []*ketoapi.RelationTuple{tuple}, resp.RelationTuples)
// Add the namespace again here, so that we can clean up properly.
m.add(t, n)
})
t.Run("case=OPL syntax check", func(t *testing.T) {
parseErrors := c.oplCheckSyntax(t, []byte("/* unclosed comment"))
require.Len(t, parseErrors, 1)
assert.Contains(t, parseErrors[0].Message, "unclosed comment")
})
}
}
func assertNamespacesContains(t *testing.T, list []ketoapi.Namespace, name string) {
t.Helper()
for _, n := range list {
if n.Name == name {
return
}
}
t.Errorf("Could not find %q in %+v", name, list)
}