Files
upstream-ory-keto/internal/relationtuple/read_server_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

346 lines
10 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package relationtuple_test
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"testing"
"github.com/ory/x/httprouterx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"google.golang.org/grpc"
rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2"
"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/internal/testhelpers"
"github.com/ory/keto/ketoapi"
)
func TestReadHandlers(t *testing.T) {
ctx := context.Background()
r := httprouterx.NewRouterPublic()
reg := driver.NewSqliteTestRegistry(t)
h := relationtuple.NewReadHandler(reg)
h.RegisterReadRoutes(r)
ts, clientTLS := driver.HTTP2TestServer(t, r)
var newNamespace func(*testing.T) *namespace.Namespace
{
nspaces := 0
newNamespace = func(t *testing.T) *namespace.Namespace {
n := &namespace.Namespace{Name: fmt.Sprintf("relation tuple read test %d", nspaces)}
nspaces++
require.NoError(t, reg.Config(ctx).Set(config.KeyNamespaces, []*namespace.Namespace{n}))
return n
}
}
t.Run("method=get", func(t *testing.T) {
t.Run("case=empty response is not nil", func(t *testing.T) {
nspace := newNamespace(t)
resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"namespace": {nspace.Name},
}.Encode())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "[]", gjson.GetBytes(body, "relation_tuples").Raw)
var respMsg ketoapi.GetResponse
require.NoError(t, json.Unmarshal(body, &respMsg))
assert.Equal(t, ketoapi.GetResponse{
RelationTuples: []*ketoapi.RelationTuple{},
NextPageToken: "",
}, respMsg)
})
t.Run("case=returns tuples", func(t *testing.T) {
nspace := newNamespace(t)
tuples := []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "o1",
Relation: "r1",
SubjectID: new("s1"),
},
{
Namespace: nspace.Name,
Object: "o2",
Relation: "r2",
SubjectSet: &ketoapi.SubjectSet{
Namespace: nspace.Name,
Object: "o1",
Relation: "r1",
},
},
}
testhelpers.MapAndInsertTuples(t, reg, tuples...)
resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"namespace": {nspace.Name},
}.Encode())
require.NoError(t, err)
var respMsg ketoapi.GetResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&respMsg))
assert.ElementsMatch(t, tuples, respMsg.RelationTuples)
assert.Equal(t, "", respMsg.NextPageToken)
})
t.Run("case=return tuples without namespace", func(t *testing.T) {
nspace := newNamespace(t)
tuples := []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "obj",
Relation: "r1",
SubjectID: new("s1"),
},
}
testhelpers.MapAndInsertTuples(t, reg, tuples...)
resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"object": {"obj"},
}.Encode())
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
var respMsg ketoapi.GetResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&respMsg))
assert.Equal(t, 1, len(respMsg.RelationTuples))
assert.Containsf(t, tuples, respMsg.RelationTuples[0], "expected to find %q in %q", respMsg.RelationTuples[0].String(), tuples)
assert.Equal(t, "", respMsg.NextPageToken)
})
t.Run("case=returns bad request on malformed subject", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"subject": {"not#a valid subject"},
}.Encode())
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("case=paginates", func(t *testing.T) {
nspace := newNamespace(t)
tuples := []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "o1",
Relation: "r1",
SubjectID: new("s1"),
},
{
Namespace: nspace.Name,
Object: "o2",
Relation: "r2",
SubjectID: new("s2"),
},
}
testhelpers.MapAndInsertTuples(t, reg, tuples...)
var firstResp ketoapi.GetResponse
resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"namespace": {nspace.Name},
"page_size": {"1"},
}.Encode())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, json.NewDecoder(resp.Body).Decode(&firstResp))
require.Len(t, firstResp.RelationTuples, 1)
assert.Contains(t, tuples, firstResp.RelationTuples[0])
assert.NotEqual(t, "", firstResp.NextPageToken)
// second page
resp, err = ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"namespace": {nspace.Name},
"page_size": {"1"},
"page_token": {firstResp.NextPageToken},
}.Encode())
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
secondResp := ketoapi.GetResponse{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&secondResp))
require.Len(t, secondResp.RelationTuples, 1)
assert.NotEqual(t, firstResp.RelationTuples, secondResp.RelationTuples)
assert.Contains(t, tuples, secondResp.RelationTuples[0])
assert.Equal(t, "", secondResp.NextPageToken)
})
t.Run("case=returs bad request on invalid page size", func(t *testing.T) {
resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{
"page_size": {"foo"},
}.Encode())
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Contains(t, string(body), "invalid syntax")
})
})
t.Run("method=grpc", func(t *testing.T) {
type requestEnhancer = func(req *rts.ListRelationTuplesRequest, query *ketoapi.RelationQuery)
withRelationQuery := func(req *rts.ListRelationTuplesRequest, query *ketoapi.RelationQuery) {
req.RelationQuery = query.ToProto()
}
withDeprecatedQuery := func(req *rts.ListRelationTuplesRequest, query *ketoapi.RelationQuery) {
pq := query.ToProto()
//nolint:staticcheck
req.Query = &rts.ListRelationTuplesRequest_Query{ //lint:ignore SA1019 backwards compatibility
Subject: pq.Subject,
}
if pq.Namespace != nil {
//nolint:staticcheck
req.Query.Namespace = *pq.Namespace //lint:ignore SA1019 backwards compatibility
}
if pq.Object != nil {
//nolint:staticcheck
req.Query.Object = *pq.Object //lint:ignore SA1019 backwards compatibility
}
if pq.Relation != nil {
//nolint:staticcheck
req.Query.Relation = *pq.Relation //lint:ignore SA1019 backwards compatibility
}
}
apiTuplesFromProto := func(t *testing.T, pts ...*rts.RelationTuple) []*ketoapi.RelationTuple {
actual := make([]*ketoapi.RelationTuple, len(pts))
for i, rt := range pts {
var err error
actual[i], err = (&ketoapi.RelationTuple{}).FromDataProvider(rt)
require.NoError(t, err)
}
return actual
}
con, err := grpc.NewClient(ts.Listener.Addr().String(), grpc.WithTransportCredentials(clientTLS))
require.NoError(t, err)
t.Run("method=list", func(t *testing.T) {
client := rts.NewReadServiceClient(con)
for key, enhancer := range map[string]requestEnhancer{"relation query": withRelationQuery, "deprecated query": withDeprecatedQuery} {
t.Run("enhancer="+key, func(t *testing.T) {
t.Run("case=returns empty list on no tuples", func(t *testing.T) {
nspace := newNamespace(t)
req := &rts.ListRelationTuplesRequest{}
enhancer(req, &ketoapi.RelationQuery{
Namespace: &nspace.Name,
})
resp, err := client.ListRelationTuples(ctx, req)
require.NoError(t, err)
assert.Len(t, resp.RelationTuples, 0)
})
t.Run("case=gets tuples", func(t *testing.T) {
nspace := newNamespace(t)
tuples := []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "o1",
Relation: "rel",
SubjectID: new("s1"),
},
{
Namespace: nspace.Name,
Object: "o2",
Relation: "rel",
SubjectSet: &ketoapi.SubjectSet{
Namespace: nspace.Name,
Object: "o1",
Relation: "r1",
},
},
}
testhelpers.MapAndInsertTuples(t, reg, tuples...)
req := &rts.ListRelationTuplesRequest{}
enhancer(req, &ketoapi.RelationQuery{
Namespace: &nspace.Name,
})
resp, err := client.ListRelationTuples(ctx, req)
require.NoError(t, err)
assert.Len(t, resp.RelationTuples, 2)
assert.ElementsMatch(t, tuples, apiTuplesFromProto(t, resp.RelationTuples...))
})
t.Run("case=paginates", func(t *testing.T) {
nspace := newNamespace(t)
tuples := []*ketoapi.RelationTuple{
{
Namespace: nspace.Name,
Object: "o1",
Relation: "rel",
SubjectID: new("s1"),
},
{
Namespace: nspace.Name,
Object: "o2",
Relation: "rel",
SubjectID: new("s2"),
},
{
Namespace: nspace.Name,
Object: "o3",
Relation: "rel",
SubjectID: new("s3"),
},
}
testhelpers.MapAndInsertTuples(t, reg, tuples...)
query := &ketoapi.RelationQuery{
Namespace: &nspace.Name,
}
firstReq := &rts.ListRelationTuplesRequest{}
enhancer(firstReq, query)
firstReq.PageSize = int32(2)
firstResp, err := client.ListRelationTuples(ctx, firstReq)
require.NoError(t, err)
secondReq := &rts.ListRelationTuplesRequest{}
enhancer(secondReq, query)
secondReq.PageSize = int32(2)
secondReq.PageToken = firstResp.NextPageToken
secondResp, err := client.ListRelationTuples(ctx, secondReq)
require.NoError(t, err)
assert.Len(t, firstResp.RelationTuples, 2)
assert.Len(t, secondResp.RelationTuples, 1)
assert.Zero(t, secondResp.NextPageToken)
assert.ElementsMatch(t, tuples, apiTuplesFromProto(t, append(firstResp.RelationTuples, secondResp.RelationTuples...)...))
})
})
}
})
})
}