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
237 lines
7.6 KiB
Go
237 lines
7.6 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package relationtuple
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/keto/cmd/client/clienttest"
|
|
"github.com/ory/keto/internal/namespace"
|
|
"github.com/ory/keto/ketoapi"
|
|
)
|
|
|
|
func TestDeleteCmd(t *testing.T) {
|
|
nspace := &namespace.Namespace{Name: t.Name()}
|
|
nspaceUser := &namespace.Namespace{Name: "User"}
|
|
|
|
newCmd := func() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "keto",
|
|
}
|
|
RegisterCommandsRecursive(cmd)
|
|
return cmd
|
|
}
|
|
|
|
otherNspace := &namespace.Namespace{Name: "other"}
|
|
ts := clienttest.NewTestServer(t, []*namespace.Namespace{nspace, nspaceUser, otherNspace}, newCmd)
|
|
|
|
// createTuple is a helper that creates a tuple via the create command so we can then delete it.
|
|
createTuple := func(t *testing.T, tuple *ketoapi.RelationTuple) {
|
|
t.Helper()
|
|
tmpFile := filepath.Join(t.TempDir(), "tuple.json")
|
|
data, err := json.Marshal(tuple)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(tmpFile, data, 0o600))
|
|
ts.Cmd.ExecNoErr(t, "relation-tuple", "create", "-f="+tmpFile)
|
|
}
|
|
|
|
type getTuple func() *ketoapi.RelationTuple
|
|
|
|
fns := map[string]getTuple{
|
|
"subjectID": func() *ketoapi.RelationTuple {
|
|
return clienttest.RandomTupleWithSubjectID(nspace.Name)
|
|
},
|
|
"subjectSet": func() *ketoapi.RelationTuple {
|
|
return clienttest.RandomTupleWithSubjectSet(nspace.Name, nspaceUser.Name)
|
|
},
|
|
}
|
|
|
|
for name, makeTuple := range fns {
|
|
t.Run("case="+name, func(t *testing.T) {
|
|
t.Run("case=deletes single tuple from file", func(t *testing.T) {
|
|
tuple := makeTuple()
|
|
createTuple(t, tuple)
|
|
|
|
tmpFile := filepath.Join(t.TempDir(), "tuple.json")
|
|
data, err := json.Marshal(tuple)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(tmpFile, data, 0o600))
|
|
|
|
stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+tmpFile)
|
|
require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut)
|
|
|
|
// assert that the tuple is gone
|
|
res := ts.Cmd.ExecNoErr(t, "relation-tuple", "get", "--namespace", tuple.Namespace, "--object", tuple.Object)
|
|
require.Equal(t, renderTable(&responseOutput{
|
|
RelationTuples: NewAPICollection(nil),
|
|
IsLastPage: true,
|
|
}), res)
|
|
})
|
|
|
|
t.Run("case=deletes tuples from stdin", func(t *testing.T) {
|
|
tuple := makeTuple()
|
|
createTuple(t, tuple)
|
|
|
|
data, err := json.Marshal(tuple)
|
|
require.NoError(t, err)
|
|
|
|
stdOut, stdErr, err := ts.Cmd.Exec(bytes.NewReader(data), "relation-tuple", "delete", "-f=-")
|
|
require.NoError(t, err)
|
|
require.Empty(t, stdErr)
|
|
require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut)
|
|
})
|
|
|
|
t.Run("case=deletes tuples from directory", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
tuple1 := makeTuple()
|
|
tuple2 := makeTuple()
|
|
createTuple(t, tuple1)
|
|
createTuple(t, tuple2)
|
|
|
|
data1, err := json.Marshal(tuple1)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "tuple1.json"), data1, 0o600))
|
|
|
|
data2, err := json.Marshal(tuple2)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "tuple2.json"), data2, 0o600))
|
|
|
|
stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+dir)
|
|
require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple1, tuple2})), stdOut)
|
|
})
|
|
|
|
t.Run("case=deletes inline tuple", func(t *testing.T) {
|
|
tuple := makeTuple()
|
|
createTuple(t, tuple)
|
|
|
|
subject := ""
|
|
if tuple.SubjectID != nil {
|
|
subject = *tuple.SubjectID
|
|
} else {
|
|
subject = tuple.SubjectSet.String()
|
|
}
|
|
|
|
stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", subject, tuple.Relation, tuple.Namespace+":"+tuple.Object)
|
|
require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut)
|
|
})
|
|
})
|
|
}
|
|
|
|
t.Run("case=fails on invalid JSON", func(t *testing.T) {
|
|
tmpFile := filepath.Join(t.TempDir(), "invalid.json")
|
|
require.NoError(t, os.WriteFile(tmpFile, []byte("not valid json"), 0o600))
|
|
|
|
_, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "delete", "-f="+tmpFile)
|
|
require.Error(t, err)
|
|
require.Contains(t, stdErr, "could not decode")
|
|
})
|
|
|
|
t.Run("case=fails on nonexistent file", func(t *testing.T) {
|
|
stdErr := ts.Cmd.ExecExpectedErr(t, "relation-tuple", "delete", "-f=/nonexistent/file.json")
|
|
require.Contains(t, stdErr, "error getting stats")
|
|
})
|
|
|
|
t.Run("case=fails on json file arguments without -f", func(t *testing.T) {
|
|
_, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "delete", "file1.json", "file2.json")
|
|
require.Error(t, err)
|
|
require.NotEmpty(t, stdErr)
|
|
})
|
|
|
|
t.Run("case=missing colon between ns and obj", func(t *testing.T) {
|
|
_, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "delete", "sns:so1", "rel", "nsobj")
|
|
require.Error(t, err)
|
|
require.Contains(t, stdErr, "expected <object_namespace>:<object_id> format, got \"nsobj\"")
|
|
})
|
|
|
|
t.Run("case=handles JSON with leading whitespace and BOM", func(t *testing.T) {
|
|
// Test with various prefixes: whitespace, BOM, and combination
|
|
prefixes := [][]byte{
|
|
{'\n', '\n', ' ', ' ', '\t'},
|
|
{0xEF, 0xBB, 0xBF},
|
|
{0xEF, 0xBB, 0xBF, '\n', ' ', ' '},
|
|
}
|
|
|
|
for _, prefix := range prefixes {
|
|
// Test single tuple
|
|
tuple := clienttest.RandomTupleWithSubjectID(nspace.Name)
|
|
createTuple(t, tuple)
|
|
|
|
tmpFile := filepath.Join(t.TempDir(), "tuple.json")
|
|
data, err := json.Marshal(tuple)
|
|
require.NoError(t, err)
|
|
dataWithPrefix := append(prefix, data...)
|
|
require.NoError(t, os.WriteFile(tmpFile, dataWithPrefix, 0o600))
|
|
|
|
stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+tmpFile)
|
|
require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut)
|
|
|
|
// Test array
|
|
tuple1 := clienttest.RandomTupleWithSubjectID(nspace.Name)
|
|
tuple2 := clienttest.RandomTupleWithSubjectID(nspace.Name)
|
|
createTuple(t, tuple1)
|
|
createTuple(t, tuple2)
|
|
|
|
tmpFile2 := filepath.Join(t.TempDir(), "tuples.json")
|
|
data2, err := json.Marshal([]*ketoapi.RelationTuple{tuple1, tuple2})
|
|
require.NoError(t, err)
|
|
dataWithPrefix2 := append(prefix, data2...)
|
|
require.NoError(t, os.WriteFile(tmpFile2, dataWithPrefix2, 0o600))
|
|
|
|
stdOut2 := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+tmpFile2)
|
|
require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple1, tuple2})), stdOut2)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReadTuplesFromDir(t *testing.T) {
|
|
writeTuple := func(t *testing.T, dir string, name string) *ketoapi.RelationTuple {
|
|
t.Helper()
|
|
tuple := clienttest.RandomTupleWithSubjectID(t.Name())
|
|
data, err := json.Marshal(tuple)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, name), data, 0o600))
|
|
return tuple
|
|
}
|
|
|
|
mkdirs := func(t *testing.T, root string, depth int) string {
|
|
t.Helper()
|
|
dir := root
|
|
for i := range depth {
|
|
dir = filepath.Join(dir, fmt.Sprintf("d%d", i))
|
|
}
|
|
require.NoError(t, os.MkdirAll(dir, 0o750))
|
|
return dir
|
|
}
|
|
|
|
t.Run("case=succeeds at MaxDirDepth nested directories", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
deepest := mkdirs(t, root, MaxDirDepth)
|
|
|
|
// Place a file inside the second-to-last directory so its
|
|
// path-component depth equals MaxDirDepth.
|
|
tuple := writeTuple(t, filepath.Dir(deepest), "tuple.json")
|
|
|
|
tuples, err := readTuplesFromDir(root, decodeTuples)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []*ketoapi.RelationTuple{tuple}, tuples)
|
|
})
|
|
|
|
t.Run("case=fails at MaxDirDepth+1 nested directories", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirs(t, root, MaxDirDepth+1)
|
|
|
|
_, err := readTuplesFromDir(root, decodeTuples)
|
|
require.ErrorContains(t, err, "maximum directory depth")
|
|
})
|
|
}
|