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