Files
upstream-ory-keto/internal/e2e/grpc_client_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

286 lines
8.4 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"encoding/json"
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
grpcHealthV1 "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
"github.com/ory/herodot"
opl "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1"
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
"github.com/ory/keto/ketoapi"
)
type grpcClient struct {
read, write, oplSyntax *grpc.ClientConn
ctx context.Context
}
func newGrpcClient(t testing.TB, ctx context.Context, readRemote, writeRemote, oplSyntaxRemote string) *grpcClient {
read, err := grpc.NewClient(readRemote, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, read.Close()) })
write, err := grpc.NewClient(writeRemote, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, write.Close()) })
oplSyntax, err := grpc.NewClient(oplSyntaxRemote, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, oplSyntax.Close()) })
ctx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)
return &grpcClient{
read: read,
write: write,
oplSyntax: oplSyntax,
ctx: ctx,
}
}
func (g *grpcClient) queryNamespaces(t testing.TB) (apiResponse ketoapi.GetNamespacesResponse) {
client := rts.NewNamespacesServiceClient(g.read)
res, err := client.ListNamespaces(g.ctx, &rts.ListNamespacesRequest{})
require.NoError(t, err)
require.NoError(t, convert(res, &apiResponse))
return
}
var _ transactClient = (*grpcClient)(nil)
func (g *grpcClient) createTuple(t testing.TB, r *ketoapi.RelationTuple) {
g.transactTuples(t, []*ketoapi.RelationTuple{r}, nil)
}
func (*grpcClient) createQuery(q *ketoapi.RelationQuery) *rts.RelationQuery {
query := &rts.RelationQuery{
Namespace: q.Namespace,
Object: q.Object,
Relation: q.Relation,
}
if q.SubjectID != nil {
query.Subject = rts.NewSubjectID(*q.SubjectID)
} else if q.SubjectSet != nil {
query.Subject = rts.NewSubjectSet(q.SubjectSet.Namespace, q.SubjectSet.Object, q.SubjectSet.Relation)
}
return query
}
func requireInt32WithinBounds(t testing.TB, v int) int32 {
require.GreaterOrEqual(t, v, math.MinInt32)
require.LessOrEqual(t, v, math.MaxInt32)
return int32(v) // #nosec G115 -- value validated above
}
func (g *grpcClient) queryTuple(t testing.TB, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) *ketoapi.GetResponse {
c := rts.NewReadServiceClient(g.read)
pagination := getPaginationOptions(opts...)
resp, err := c.ListRelationTuples(g.ctx, &rts.ListRelationTuplesRequest{
RelationQuery: g.createQuery(q),
PageToken: pagination.Token,
PageSize: requireInt32WithinBounds(t, pagination.Size),
})
require.NoError(t, err)
irs := make([]*ketoapi.RelationTuple, len(resp.RelationTuples))
for i := range irs {
irs[i], err = (&ketoapi.RelationTuple{}).FromDataProvider(resp.RelationTuples[i])
require.NoError(t, err)
}
return &ketoapi.GetResponse{
RelationTuples: irs,
NextPageToken: resp.NextPageToken,
}
}
func (g *grpcClient) queryTupleErr(t testing.TB, expected *herodot.DefaultError, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) {
c := rts.NewReadServiceClient(g.read)
pagination := getPaginationOptions(opts...)
_, err := c.ListRelationTuples(g.ctx, &rts.ListRelationTuplesRequest{
RelationQuery: g.createQuery(q),
PageToken: pagination.Token,
PageSize: requireInt32WithinBounds(t, pagination.Size),
})
require.Error(t, err)
s, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, expected.GRPCCodeField, s.Code(), "%+v", err)
}
func (g *grpcClient) check(t testing.TB, r *ketoapi.RelationTuple) bool {
c := rts.NewCheckServiceClient(g.read)
req := &rts.CheckRequest{
Tuple: &rts.RelationTuple{
Namespace: r.Namespace,
Object: r.Object,
Relation: r.Relation,
},
}
if r.SubjectID != nil {
req.Tuple.Subject = rts.NewSubjectID(*r.SubjectID)
} else {
req.Tuple.Subject = rts.NewSubjectSet(r.SubjectSet.Namespace, r.SubjectSet.Object, r.SubjectSet.Relation)
}
resp, err := c.Check(g.ctx, req)
require.NoError(t, err)
return resp.Allowed
}
type checkResponse struct {
allowed bool
errorMessage string
}
func (g *grpcClient) batchCheckErr(t testing.TB, requestTuples []*ketoapi.RelationTuple, expected *herodot.DefaultError) {
_, err := g.doBatchCheck(t, requestTuples)
require.Error(t, err)
s, ok := status.FromError(err)
require.True(t, ok)
assert.Equalf(t, expected.GRPCCodeField, s.Code(), "%+v", err)
assert.Contains(t, s.Message(), expected.Reason())
}
func (g *grpcClient) batchCheck(t testing.TB, requestTuples []*ketoapi.RelationTuple) []checkResponse {
resp, err := g.doBatchCheck(t, requestTuples)
require.NoError(t, err)
checkResponses := make([]checkResponse, len(resp.Results))
for i, r := range resp.Results {
checkResponses[i] = checkResponse{
allowed: r.Allowed,
errorMessage: r.Error,
}
}
return checkResponses
}
func (g *grpcClient) doBatchCheck(_ testing.TB, requestTuples []*ketoapi.RelationTuple) (*rts.BatchCheckResponse, error) {
c := rts.NewCheckServiceClient(g.read)
tuples := make([]*rts.RelationTuple, len(requestTuples))
for i, tuple := range requestTuples {
var subject *rts.Subject
if tuple.SubjectID != nil {
subject = rts.NewSubjectID(*tuple.SubjectID)
} else {
subject = rts.NewSubjectSet(tuple.SubjectSet.Namespace, tuple.SubjectSet.Object, tuple.SubjectSet.Relation)
}
tuples[i] = &rts.RelationTuple{
Namespace: tuple.Namespace,
Object: tuple.Object,
Relation: tuple.Relation,
Subject: subject,
}
}
req := &rts.BatchCheckRequest{
Tuples: tuples,
}
return c.BatchCheck(g.ctx, req)
}
func (g *grpcClient) expand(t testing.TB, r *ketoapi.SubjectSet, depth int) *ketoapi.Tree[*ketoapi.RelationTuple] {
c := rts.NewExpandServiceClient(g.read)
resp, err := c.Expand(g.ctx, &rts.ExpandRequest{
Subject: rts.NewSubjectSet(r.Namespace, r.Object, r.Relation),
MaxDepth: requireInt32WithinBounds(t, depth),
})
require.NoError(t, err)
return ketoapi.TreeFromProto[*ketoapi.RelationTuple](resp.Tree)
}
func (g *grpcClient) waitUntilLive(t testing.TB) {
require.EventuallyWithT(t, func(t *assert.CollectT) {
c := grpcHealthV1.NewHealthClient(g.read)
res, err := c.Check(g.ctx, &grpcHealthV1.HealthCheckRequest{})
require.NoError(t, err)
assert.Equal(t, grpcHealthV1.HealthCheckResponse_SERVING, res.Status)
}, 2*time.Second, 10*time.Millisecond)
}
func (g *grpcClient) deleteTuple(t testing.TB, r *ketoapi.RelationTuple) {
g.transactTuples(t, nil, []*ketoapi.RelationTuple{r})
}
func (g *grpcClient) deleteAllTuples(t testing.TB, q *ketoapi.RelationQuery) {
c := rts.NewWriteServiceClient(g.write)
query := &rts.RelationQuery{
Namespace: q.Namespace,
Object: q.Object,
Relation: q.Relation,
}
if q.SubjectID != nil {
query.Subject = rts.NewSubjectID(*q.SubjectID)
}
if q.SubjectSet != nil {
query.Subject = rts.NewSubjectSet(q.SubjectSet.Namespace, q.SubjectSet.Object, q.SubjectSet.Relation)
}
_, err := c.DeleteRelationTuples(g.ctx, &rts.DeleteRelationTuplesRequest{
RelationQuery: query,
})
require.NoError(t, err)
}
func (g *grpcClient) transactTuples(t testing.TB, ins []*ketoapi.RelationTuple, del []*ketoapi.RelationTuple) {
c := rts.NewWriteServiceClient(g.write)
deltas := make([]*rts.RelationTupleDelta, len(ins)+len(del))
for i := range ins {
deltas[i] = &rts.RelationTupleDelta{
RelationTuple: ins[i].ToProto(),
Action: rts.RelationTupleDelta_ACTION_INSERT,
}
}
for i := range del {
deltas[len(ins)+i] = &rts.RelationTupleDelta{
RelationTuple: del[i].ToProto(),
Action: rts.RelationTupleDelta_ACTION_DELETE,
}
}
_, err := c.TransactRelationTuples(g.ctx, &rts.TransactRelationTuplesRequest{
RelationTupleDeltas: deltas,
})
require.NoError(t, err)
}
func (g *grpcClient) oplCheckSyntax(t testing.TB, content []byte) (parseErrors []*ketoapi.ParseError) {
c := opl.NewSyntaxServiceClient(g.oplSyntax)
res, err := c.Check(g.ctx, &opl.CheckRequest{Content: content})
require.NoError(t, err)
raw, err := json.Marshal(res.ParseErrors)
require.NoError(t, err)
err = json.Unmarshal(raw, &parseErrors)
require.NoError(t, err)
return
}