// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package e2e import ( "cmp" "slices" "strconv" "testing" "time" "github.com/ory/x/uuidx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/keto/internal/namespace" "github.com/ory/keto/ketoapi" ) func runTransactionCases(c transactClient, m *namespaceTestManager) func(*testing.T) { return func(t *testing.T) { t.Parallel() t.Run("case=create and delete", func(t *testing.T) { n := &namespace.Namespace{Name: t.Name()} m.add(t, n) tuples := []*ketoapi.RelationTuple{ { Namespace: n.Name, Object: "o", Relation: "rel", SubjectSet: &ketoapi.SubjectSet{ Namespace: n.Name, Object: "o", Relation: "rel", }, }, { Namespace: n.Name, Object: "o", Relation: "rel", SubjectID: new("sid"), }, } c.transactTuples(t, tuples, nil) resp := c.queryTuple(t, &ketoapi.RelationQuery{ Namespace: &n.Name, }) for i := range tuples { assert.Contains(t, resp.RelationTuples, tuples[i]) } c.transactTuples(t, nil, tuples) resp = c.queryTuple(t, &ketoapi.RelationQuery{ Namespace: &n.Name, }) assert.Len(t, resp.RelationTuples, 0) }) t.Run("case=duplicate string representations", func(t *testing.T) { n := &namespace.Namespace{Name: t.Name()} m.add(t, n) c.transactTuples(t, []*ketoapi.RelationTuple{ { Namespace: n.Name, Object: "o", Relation: "rel", SubjectID: new("sid"), }, { Namespace: n.Name, Object: "o", Relation: "rel", SubjectID: new("sid"), }, }, nil) }) t.Run("case=large inserts and deletes", func(t *testing.T) { if testing.Short() { t.Skip("This test is fairly expensive, especially the deletion.") } ns := []*namespace.Namespace{ {Name: uuidx.NewV4().String()}, {Name: uuidx.NewV4().String()}, } m.add(t, ns...) var tuples []*ketoapi.RelationTuple for i := range 1200 { tuples = append(tuples, &ketoapi.RelationTuple{ Namespace: ns[0].Name, Object: "o" + strconv.Itoa(i), Relation: "rela", SubjectSet: &ketoapi.SubjectSet{ Namespace: ns[1].Name, Object: "o" + strconv.Itoa(i), Relation: "relx", }, }, &ketoapi.RelationTuple{ Namespace: ns[0].Name, Object: "o" + strconv.Itoa(i), Relation: "relb", SubjectID: new("sid"), }, ) } t0 := time.Now() c.transactTuples(t, tuples, nil) t.Logf("insert took %s", time.Since(t0)) t0 = time.Now() var resp []*ketoapi.RelationTuple var pt string for { r := c.queryTuple(t, &ketoapi.RelationQuery{ Namespace: &ns[0].Name, }, withSize(1000), withToken(pt)) resp = append(resp, r.RelationTuples...) pt = r.NextPageToken if pt == "" { break } } t.Logf("query took %s", time.Since(t0)) sort := func(a, b *ketoapi.RelationTuple) int { return cmp.Or( cmp.Compare(a.Namespace, b.Namespace), cmp.Compare(a.Object, b.Object), cmp.Compare(a.Relation, b.Relation), ) } slices.SortFunc(resp, sort) slices.SortFunc(tuples, sort) require.Equal(t, tuples, resp) t0 = time.Now() c.transactTuples(t, nil, tuples) t.Logf("delete took %s", time.Since(t0)) resp = c.queryTuple(t, &ketoapi.RelationQuery{ Namespace: &ns[0].Name, }).RelationTuples assert.Len(t, resp, 0) }) t.Run("case=expand-api-display-access docs code sample", func(t *testing.T) { files := &namespace.Namespace{Name: t.Name() + "files"} directories := &namespace.Namespace{Name: t.Name() + "directories"} m.add(t, files, directories) tuples := []*ketoapi.RelationTuple{ { Namespace: directories.Name, Object: "/photos", Relation: "owner", SubjectID: new("maureen"), }, { Namespace: files.Name, Object: "/photos/beach.jpg", Relation: "owner", SubjectID: new("maureen"), }, { Namespace: files.Name, Object: "/photos/mountains.jpg", Relation: "owner", SubjectID: new("laura"), }, { Namespace: directories.Name, Object: "/photos", Relation: "access", SubjectID: new("laura"), }, } for _, o := range []struct{ n, o string }{ {files.Name, "/photos/beach.jpg"}, {files.Name, "/photos/mountains.jpg"}, {directories.Name, "/photos"}, } { tuples = append(tuples, &ketoapi.RelationTuple{ Namespace: o.n, Object: o.o, Relation: "access", SubjectSet: &ketoapi.SubjectSet{ Namespace: o.n, Object: o.o, Relation: "owner", }, }) } for _, obj := range []string{"/photos/beach.jpg", "/photos/mountains.jpg"} { tuples = append(tuples, &ketoapi.RelationTuple{ Namespace: files.Name, Object: obj, Relation: "access", SubjectSet: &ketoapi.SubjectSet{ Namespace: directories.Name, Object: "/photos", Relation: "access", }, }) } c.transactTuples(t, tuples, nil) fileTuples := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(files.Name)}) directoryTuples := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(directories.Name)}) allTuples := append(fileTuples.RelationTuples, directoryTuples.RelationTuples...) assert.ElementsMatch(t, allTuples, tuples) c.transactTuples(t, nil, tuples) fileTuples = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(files.Name)}) directoryTuples = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(directories.Name)}) allTuples = append(fileTuples.RelationTuples, directoryTuples.RelationTuples...) assert.Len(t, allTuples, 0) }) } }