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
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:
486
internal/relationtuple/transact_server_test.go
Normal file
486
internal/relationtuple/transact_server_test.go
Normal file
@@ -0,0 +1,486 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package relationtuple_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/x/httprouterx"
|
||||
keysetpagination "github.com/ory/x/pagination/keysetpagination_v2"
|
||||
"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/internal/testhelpers"
|
||||
"github.com/ory/keto/ketoapi"
|
||||
)
|
||||
|
||||
func TestWriteHandlers(t *testing.T) {
|
||||
r := httprouterx.NewRouterAdmin()
|
||||
reg := driver.NewSqliteTestRegistry(t)
|
||||
|
||||
var nspaces []*namespace.Namespace
|
||||
addNamespace := func(t *testing.T) *namespace.Namespace {
|
||||
n := &namespace.Namespace{
|
||||
Name: t.Name(),
|
||||
}
|
||||
nspaces = append(nspaces, n)
|
||||
|
||||
require.NoError(t, reg.Config(t.Context()).Set(config.KeyNamespaces, nspaces))
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
relationtuple.NewWriteHandler(reg).RegisterWriteRoutes(r)
|
||||
relationtuple.NewReadHandler(reg).RegisterReadRoutes(r.ToPublic())
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
t.Run("method=create", func(t *testing.T) {
|
||||
doCreate := func(raw []byte) *http.Response {
|
||||
req, err := http.NewRequest(http.MethodPut, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(raw))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
t.Run("case=creates tuple", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
rt := &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "obj",
|
||||
Relation: "rel",
|
||||
SubjectID: new("subj"),
|
||||
}
|
||||
payload, err := json.Marshal(rt)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := doCreate(payload)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(payload), string(body))
|
||||
|
||||
t.Run("check=is contained in the manager", func(t *testing.T) {
|
||||
mapped, err := reg.Mapper().FromTuple(t.Context(), rt)
|
||||
require.NoError(t, err)
|
||||
// set a size > 1 just to make sure it gets all
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), mapped[0].ToQuery(), keysetpagination.WithSize(10))
|
||||
require.NoError(t, err)
|
||||
actual, err := reg.Mapper().ToTuple(t.Context(), actualRTs...)
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, []*ketoapi.RelationTuple{rt}, actual, "want: %s\ngot: %s", rt.String(), actual[0].String())
|
||||
})
|
||||
|
||||
t.Run("check=is gettable with the returned URL", func(t *testing.T) {
|
||||
resp, err := ts.Client().Get(ts.URL + resp.Header.Get("Location"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
respDec := ketoapi.GetResponse{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&respDec))
|
||||
assert.Equal(t, []*ketoapi.RelationTuple{rt}, respDec.RelationTuples)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=returns bad request on JSON parse error", func(t *testing.T) {
|
||||
resp := doCreate([]byte("foo"))
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("case=special chars", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
rts := []*ketoapi.RelationTuple{
|
||||
{
|
||||
Namespace: nspace.Name,
|
||||
Object: "group:B",
|
||||
Relation: "member",
|
||||
SubjectSet: &ketoapi.SubjectSet{
|
||||
Namespace: nspace.Name,
|
||||
Object: "group:A",
|
||||
Relation: "member",
|
||||
},
|
||||
},
|
||||
{
|
||||
Namespace: nspace.Name,
|
||||
Object: "@all",
|
||||
Relation: "member",
|
||||
SubjectID: new("this:could#be interpreted:as a@subject set"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, rt := range rts {
|
||||
payload, err := json.Marshal(rt)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := doCreate(payload)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
}
|
||||
|
||||
actual, next, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{
|
||||
Namespace: &nspace.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
actualMapped, err := reg.Mapper().ToTuple(t.Context(), actual...)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, next.IsLast())
|
||||
assert.ElementsMatch(t, rts, actualMapped)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("method=delete", func(t *testing.T) {
|
||||
t.Run("case=deletes a tuple", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
rt := &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "deleted obj",
|
||||
Relation: "deleted rel",
|
||||
SubjectID: new("deleted subj"),
|
||||
}
|
||||
testhelpers.MapAndInsertTuples(t, reg, rt)
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.WriteRouteBase+"?"+rt.ToURLQuery().Encode(), nil)
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
// set a size > 1 just to make sure it gets all
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace.Name}, keysetpagination.WithSize(10))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs)
|
||||
})
|
||||
|
||||
t.Run("case=deletes multiple tuples", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
rts := []*ketoapi.RelationTuple{
|
||||
{
|
||||
Namespace: nspace.Name,
|
||||
Object: "deleted obj",
|
||||
Relation: "deleted rel",
|
||||
SubjectID: new("deleted subj 1"),
|
||||
},
|
||||
{
|
||||
Namespace: nspace.Name,
|
||||
Object: "deleted obj",
|
||||
Relation: "deleted rel",
|
||||
SubjectID: new("deleted subj 2"),
|
||||
},
|
||||
}
|
||||
|
||||
testhelpers.MapAndInsertTuples(t, reg, rts...)
|
||||
|
||||
q := url.Values{
|
||||
"namespace": {nspace.Name},
|
||||
"object": {"deleted obj"},
|
||||
"relation": {"deleted rel"},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.WriteRouteBase+"?"+q.Encode(), nil)
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
query, err := (&ketoapi.RelationQuery{}).FromURLQuery(q)
|
||||
require.NoError(t, err)
|
||||
mappedQuery, err := reg.Mapper().FromQuery(t.Context(), query)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), mappedQuery, keysetpagination.WithSize(10))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs)
|
||||
})
|
||||
|
||||
t.Run("suite=bad requests", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
rts := []*ketoapi.RelationTuple{
|
||||
{
|
||||
Namespace: nspace.Name,
|
||||
Object: "deleted obj",
|
||||
Relation: "deleted rel",
|
||||
SubjectID: new("deleted subj 1"),
|
||||
},
|
||||
{
|
||||
Namespace: nspace.Name,
|
||||
Object: "deleted obj",
|
||||
Relation: "deleted rel",
|
||||
SubjectID: new("deleted subj 2"),
|
||||
},
|
||||
}
|
||||
|
||||
testhelpers.MapAndInsertTuples(t, reg, rts...)
|
||||
|
||||
assertBadRequest := func(t *testing.T, req *http.Request) {
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
}
|
||||
|
||||
assertTuplesExist := func(t *testing.T) {
|
||||
mappedQuery, err := reg.Mapper().FromQuery(t.Context(), &ketoapi.RelationQuery{
|
||||
Namespace: &nspace.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), mappedQuery, keysetpagination.WithSize(10))
|
||||
require.NoError(t, err)
|
||||
mappedRTs, err := reg.Mapper().ToTuple(t.Context(), actualRTs...)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, rts, mappedRTs)
|
||||
}
|
||||
|
||||
t.Run("case=bad request if body sent", func(t *testing.T) {
|
||||
q := url.Values{
|
||||
"namespace": {nspace.Name},
|
||||
"object": {"deleted obj"},
|
||||
"relation": {"deleted rel"},
|
||||
}
|
||||
req, err := http.NewRequest(
|
||||
http.MethodDelete,
|
||||
ts.URL+relationtuple.WriteRouteBase+"?"+q.Encode(),
|
||||
strings.NewReader("some body"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assertBadRequest(t, req)
|
||||
assertTuplesExist(t)
|
||||
})
|
||||
|
||||
t.Run("case=bad request query param misspelled", func(t *testing.T) {
|
||||
req, err := http.NewRequest(
|
||||
http.MethodDelete,
|
||||
ts.URL+relationtuple.WriteRouteBase+"?invalid=param",
|
||||
nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertBadRequest(t, req)
|
||||
assertTuplesExist(t)
|
||||
})
|
||||
|
||||
t.Run("case=bad request if query params misssing", func(t *testing.T) {
|
||||
req, err := http.NewRequest(
|
||||
http.MethodDelete,
|
||||
ts.URL+relationtuple.WriteRouteBase,
|
||||
nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertBadRequest(t, req)
|
||||
assertTuplesExist(t)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("method=patch", func(t *testing.T) {
|
||||
t.Run("case=create and delete", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
relation := t.Name()
|
||||
|
||||
deltas := []*ketoapi.PatchDelta{
|
||||
{
|
||||
Action: ketoapi.ActionInsert,
|
||||
RelationTuple: &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "create obj",
|
||||
Relation: relation,
|
||||
SubjectID: new("create sub"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: ketoapi.ActionDelete,
|
||||
RelationTuple: &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "delete obj",
|
||||
Relation: relation,
|
||||
SubjectID: new("delete sub"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testhelpers.MapAndInsertTuples(t, reg, deltas[1].RelationTuple)
|
||||
|
||||
body, err := json.Marshal(deltas)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{
|
||||
Namespace: &nspace.Name,
|
||||
Relation: &relation,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
mapped, err := reg.Mapper().ToTuple(t.Context(), actualRTs...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*ketoapi.RelationTuple{deltas[0].RelationTuple}, mapped)
|
||||
})
|
||||
|
||||
t.Run("case=ignores rest on err", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
deltas := []*ketoapi.PatchDelta{
|
||||
{
|
||||
Action: ketoapi.ActionInsert,
|
||||
RelationTuple: &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "create obj",
|
||||
Relation: t.Name(),
|
||||
SubjectID: new("create sub"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Action: ketoapi.ActionDelete,
|
||||
RelationTuple: &ketoapi.RelationTuple{
|
||||
Namespace: "not " + nspace.Name,
|
||||
Object: "o",
|
||||
Relation: "r",
|
||||
SubjectID: new("s"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(deltas)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// set a size > 1 just to make sure it gets all
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace.Name}, keysetpagination.WithSize(10))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, actualRTs, 0)
|
||||
})
|
||||
|
||||
t.Run("case=only create", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
deltas := []*ketoapi.PatchDelta{
|
||||
{
|
||||
Action: ketoapi.ActionInsert,
|
||||
RelationTuple: &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "create obj",
|
||||
Relation: "rel",
|
||||
SubjectID: new("create sub"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(deltas)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{
|
||||
Namespace: &nspace.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
mapped, err := reg.Mapper().ToTuple(t.Context(), actualRTs...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*ketoapi.RelationTuple{deltas[0].RelationTuple}, mapped)
|
||||
})
|
||||
|
||||
t.Run("case=only delete", func(t *testing.T) {
|
||||
nspace := addNamespace(t)
|
||||
|
||||
deltas := []*ketoapi.PatchDelta{
|
||||
{
|
||||
Action: ketoapi.ActionDelete,
|
||||
RelationTuple: &ketoapi.RelationTuple{
|
||||
Namespace: nspace.Name,
|
||||
Object: "delete obj",
|
||||
Relation: "rel",
|
||||
SubjectID: new("delete sub"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(deltas)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{
|
||||
Namespace: &nspace.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs)
|
||||
})
|
||||
|
||||
t.Run("case=valid JSON, invalid content", func(t *testing.T) {
|
||||
rawJSON := `
|
||||
[
|
||||
{
|
||||
"action": "insert",
|
||||
"namespace":"role",
|
||||
"object":"super-admin",
|
||||
"relation":"member",
|
||||
"subject":"role:company-admin"
|
||||
}
|
||||
]`
|
||||
req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBufferString(rawJSON))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
errContent, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(errContent), "relation_tuple is missing")
|
||||
})
|
||||
|
||||
t.Run("case=unknown action", func(t *testing.T) {
|
||||
rawJSON := `
|
||||
[
|
||||
{
|
||||
"action": "unknown_action_foo",
|
||||
"relation_tuple": {
|
||||
"namespace":"role",
|
||||
"object":"super-admin",
|
||||
"relation":"member",
|
||||
"subject_id":"role:company-admin"
|
||||
}
|
||||
}
|
||||
]`
|
||||
req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBufferString(rawJSON))
|
||||
require.NoError(t, err)
|
||||
resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL.
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
errContent, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(errContent), "unknown_action_foo")
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user