From 7e724b5032f25300544807e8e8ea8314a195a896 Mon Sep 17 00:00:00 2001 From: kindlyturnips Date: Thu, 15 Feb 2024 15:46:17 -0600 Subject: [PATCH] feat: support csv in tuple delete --- cmd/tuple/delete.go | 15 +-- cmd/tuple/delete_test.go | 193 ++++++++++++++++++++++++++++++++++ cmd/tuple/testdata/tuples.csv | 2 +- internal/tuplefile/read.go | 14 +++ 4 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 cmd/tuple/delete_test.go diff --git a/cmd/tuple/delete.go b/cmd/tuple/delete.go index ff5ab8e..3426d56 100644 --- a/cmd/tuple/delete.go +++ b/cmd/tuple/delete.go @@ -19,14 +19,13 @@ package tuple import ( "context" "fmt" - "os" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" "github.com/openfga/cli/internal/cmdutils" "github.com/openfga/cli/internal/output" + "github.com/openfga/cli/internal/tuplefile" ) // deleteCmd represents the delete command. @@ -46,19 +45,15 @@ var deleteCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to parse file name due to %w", err) } + if fileName != "" { - var tuples []client.ClientTupleKeyWithoutCondition - data, err := os.ReadFile(fileName) + clientTuples, err := tuplefile.ReadTupleFile(fileName) if err != nil { return fmt.Errorf("failed to read file %s due to %w", fileName, err) } - err = yaml.Unmarshal(data, &tuples) - if err != nil { - return fmt.Errorf("failed to parse input tuples due to %w", err) - } - + var openfgaTuples = tuplefile.ClientTupleKeyToTupleKeyWithoutCondition(clientTuples) maxTuplesPerWrite, err := cmd.Flags().GetInt("max-tuples-per-write") if err != nil { return fmt.Errorf("failed to parse max tuples per write due to %w", err) @@ -70,7 +65,7 @@ var deleteCmd = &cobra.Command{ } deleteRequest := client.ClientWriteRequest{ - Deletes: tuples, + Deletes: openfgaTuples, } response, err := ImportTuples(fgaClient, deleteRequest, maxTuplesPerWrite, maxParallelRequests) if err != nil { diff --git a/cmd/tuple/delete_test.go b/cmd/tuple/delete_test.go new file mode 100644 index 0000000..fbba164 --- /dev/null +++ b/cmd/tuple/delete_test.go @@ -0,0 +1,193 @@ +package tuple + +import ( + "testing" + + openfga "github.com/openfga/go-sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openfga/cli/internal/tuplefile" +) + +func TestDeleteTuplesFileData(t *testing.T) { //nolint:funlen + t.Parallel() + + tests := []struct { + name string + file string + expectedTuples []openfga.TupleKeyWithoutCondition + expectedError string + }{ + { + name: "it can correctly parse a csv file", + file: "testdata/tuples.csv", + expectedTuples: []openfga.TupleKeyWithoutCondition{ + { + User: "user:anne", + Relation: "owner", + Object: "folder:product", + }, + { + User: "folder:product", + Relation: "parent", + Object: "folder:product-2021", + }, + { + User: "team:fga#member", + Relation: "viewer", + Object: "folder:product-2021", + }, + }, + }, + { + name: "it can correctly parse a csv file regardless of columns order", + file: "testdata/tuples_other_columns_order.csv", + expectedTuples: []openfga.TupleKeyWithoutCondition{ + { + User: "user:anne", + Relation: "owner", + Object: "folder:product", + }, + { + User: "folder:product", + Relation: "parent", + Object: "folder:product-2021", + }, + { + User: "team:fga#member", + Relation: "viewer", + Object: "folder:product-2021", + }, + }, + }, + { + name: "it can correctly parse a csv file without optional fields", + file: "testdata/tuples_without_optional_fields.csv", + expectedTuples: []openfga.TupleKeyWithoutCondition{ + { + User: "user:anne", + Relation: "owner", + Object: "folder:product", + }, + { + User: "folder:product", + Relation: "parent", + Object: "folder:product-2021", + }, + }, + }, + { + name: "it can correctly parse a csv file with condition_name header but no condition_context header", + file: "testdata/tuples_with_condition_name_but_no_condition_context.csv", + expectedTuples: []openfga.TupleKeyWithoutCondition{ + { + User: "user:anne", + Relation: "owner", + Object: "folder:product", + }, + { + User: "folder:product", + Relation: "parent", + Object: "folder:product-2021", + }, + { + User: "team:fga#member", + Relation: "viewer", + Object: "folder:product-2021", + }, + }, + }, + { + name: "it can correctly parse a json file", + file: "testdata/tuples.json", + expectedTuples: []openfga.TupleKeyWithoutCondition{ + { + User: "user:anne", + Relation: "owner", + Object: "folder:product", + }, + { + User: "folder:product", + Relation: "parent", + Object: "folder:product-2021", + }, + { + User: "user:beth", + Relation: "viewer", + Object: "folder:product-2021", + }, + }, + }, + { + name: "it can correctly parse a yaml file", + file: "testdata/tuples.yaml", + expectedTuples: []openfga.TupleKeyWithoutCondition{ + { + User: "user:anne", + Relation: "owner", + Object: "folder:product", + }, + { + User: "folder:product", + Relation: "parent", + Object: "folder:product-2021", + }, + { + User: "user:beth", + Relation: "viewer", + Object: "folder:product-2021", + }, + }, + }, + { + name: "it fails to parse a non-supported file format", + file: "testdata/tuples.toml", + expectedError: "failed to parse input tuples: unsupported file format \".toml\"", + }, + { + name: "it fails to parse a csv file with wrong headers", + file: "testdata/tuples_wrong_headers.csv", + expectedError: "failed to parse input tuples: invalid header \"a\", valid headers are user_type,user_id,user_relation,relation,object_type,object_id,condition_name,condition_context", + }, + { + name: "it fails to parse a csv file with missing required headers", + file: "testdata/tuples_missing_required_headers.csv", + expectedError: "failed to parse input tuples: csv header missing (\"object_id\")", + }, + { + name: "it fails to parse a csv file with missing condition_name header when condition_context is present", + file: "testdata/tuples_missing_condition_name_header.csv", + expectedError: "failed to parse input tuples: missing \"condition_name\" header which is required when \"condition_context\" is present", + }, + { + name: "it fails to parse an empty csv file", + file: "testdata/tuples_empty.csv", + expectedError: "failed to parse input tuples: failed to read csv headers: EOF", + }, + { + name: "it fails to parse a csv file with invalid rows", + file: "testdata/tuples_with_invalid_rows.csv", + expectedError: "failed to parse input tuples: failed to read tuple from csv file: record on line 2: wrong number of fields", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actualTuples, err := tuplefile.ReadTupleFile(test.file) + deleteTuples := tuplefile.ClientTupleKeyToTupleKeyWithoutCondition(actualTuples) + + if test.expectedError != "" { + require.EqualError(t, err, test.expectedError) + + return + } + + require.NoError(t, err) + assert.Equal(t, test.expectedTuples, deleteTuples) + }) + } +} diff --git a/cmd/tuple/testdata/tuples.csv b/cmd/tuple/testdata/tuples.csv index b2f7810..ad40474 100644 --- a/cmd/tuple/testdata/tuples.csv +++ b/cmd/tuple/testdata/tuples.csv @@ -1,4 +1,4 @@ user_type,user_id,user_relation,relation,object_type,object_id,condition_name,condition_context user,anne,,owner,folder,product,inOfficeIP, folder,product,,parent,folder,product-2021,inOfficeIP,"{""ip_addr"":""10.0.0.1""}" -team,fga,member,viewer,folder,product-2021,, +team,fga,member,viewer,folder,product-2021,, \ No newline at end of file diff --git a/internal/tuplefile/read.go b/internal/tuplefile/read.go index 041d43c..565d9a3 100644 --- a/internal/tuplefile/read.go +++ b/internal/tuplefile/read.go @@ -5,6 +5,7 @@ import ( "os" "path" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "gopkg.in/yaml.v3" ) @@ -32,3 +33,16 @@ func ReadTupleFile(fileName string) ([]client.ClientTupleKey, error) { return tuples, nil } + +func ClientTupleKeyToTupleKeyWithoutCondition(clientTupleKey []client.ClientTupleKey) []openfga.TupleKeyWithoutCondition { + var tuples []openfga.TupleKeyWithoutCondition + for _, tuple := range clientTupleKey { + convertedTuple := openfga.TupleKeyWithoutCondition{ + User: tuple.User, + Relation: tuple.Relation, + Object: tuple.Object, + } + tuples = append(tuples, convertedTuple) + } + return tuples +}