diff --git a/README.md b/README.md index 6ce8c5d..595e777 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ fga tuples **changes** --type --store-id= ##### Check ###### Command -fga query **check** [--contextual-tuple ]* --store-id= [--model-id=] +fga query **check** [--contextual-tuple " "]* --store-id= [--model-id=] ###### Parameters * `--store-id`: Specifies the store id @@ -384,7 +384,7 @@ fga query **check** [--contextual-tuple [--contextual-tuple [--contextual-tuple ]* --store-id= [--model-id=] +fga query **list-objects** [--contextual-tuple " "]* --store-id= [--model-id=] ###### Parameters * `--store-id`: Specifies the store id @@ -404,7 +404,7 @@ fga query **list-objects** [--contextual-tuple < * `--contextual-tuple`: Contextual tuples (optional) (can be multiple) ###### Example -`fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --contextual-tuple user:anne can_view folder:product --contextual-tuple folder:product parent document:roadmap` +`fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --contextual-tuple "user:anne can_view folder:product" --contextual-tuple "folder:product parent document:roadmap"` ###### JSON Response ```json5 @@ -419,7 +419,7 @@ fga query **list-objects** [--contextual-tuple < ##### List Relations ###### Command -fga query **list-objects** [--relation ]* [--contextual-tuple ]* --store-id= [--model-id=] +fga query **list-objects** [--relation ]* [--contextual-tuple " "]* --store-id= [--model-id=] ###### Parameters * `--store-id`: Specifies the store id @@ -443,12 +443,11 @@ fga query **list-objects** [--relation ]* [--contextua ##### Expand ###### Command -fga query **expand** [--contextual-tuple ]* --store-id= [--model-id=] +fga query **expand** --store-id= [--model-id=] ###### Parameters * `--store-id`: Specifies the store id * `--model-id`: Specifies the model id to target (optional) -* `--contextual-tuple`: Contextual tuples (optional) (can be multiple) ###### Example `fga query expand --store-id=01H0H015178Y2V4CX10C2KGHF4 can_view document:roadmap` diff --git a/cmd/models/models.go b/cmd/models/models.go index c196ebd..0967763 100644 --- a/cmd/models/models.go +++ b/cmd/models/models.go @@ -34,8 +34,7 @@ func init() { ModelsCmd.AddCommand(getCmd) ModelsCmd.PersistentFlags().String("store-id", "", "Store ID") - ModelsCmd.Flags().String("store-id", "", "Store ID") - err := ModelsCmd.MarkFlagRequired("store-id") + err := ModelsCmd.MarkPersistentFlagRequired("store-id") if err != nil { //nolint:wsl fmt.Print(err) os.Exit(1) diff --git a/cmd/query/check.go b/cmd/query/check.go index a046f11..ee3ec0d 100644 --- a/cmd/query/check.go +++ b/cmd/query/check.go @@ -25,11 +25,18 @@ import ( "github.com/spf13/cobra" ) -func check(fgaClient client.SdkClient, user string, relation string, object string) (string, error) { +func check( + fgaClient client.SdkClient, + user string, + relation string, + object string, + contextualTuples []client.ClientTupleKey, +) (string, error) { body := &client.ClientCheckRequest{ - User: user, - Relation: relation, - Object: object, + User: user, + Relation: relation, + Object: object, + ContextualTuples: &contextualTuples, } options := &client.ClientCheckOptions{} @@ -50,7 +57,7 @@ func check(fgaClient client.SdkClient, user string, relation string, object stri var checkCmd = &cobra.Command{ Use: "check", Short: "Check", - Long: "Check if a user has a particular relation with an object.", + Long: "Check if a user has a particular relation with an object. E.g. \"check user:anne can_view document:roadmap\"", Args: cobra.ExactArgs(3), //nolint:gomnd RunE: func(cmd *cobra.Command, args []string) error { clientConfig := cmdutils.GetClientConfig(cmd) @@ -59,9 +66,14 @@ var checkCmd = &cobra.Command{ return fmt.Errorf("failed to initialize FGA Client due to %w", err) } - output, err := check(fgaClient, args[0], args[1], args[2]) + contextualTuples, err := cmdutils.ParseContextualTuples(cmd) if err != nil { - return err + return fmt.Errorf("error parsing contextual tuples for check: %w", err) + } + + output, err := check(fgaClient, args[0], args[1], args[2], contextualTuples) + if err != nil { + return fmt.Errorf("error calling check: %w", err) } fmt.Print(output) diff --git a/cmd/query/check_test.go b/cmd/query/check_test.go index 389e9ef..038781b 100644 --- a/cmd/query/check_test.go +++ b/cmd/query/check_test.go @@ -31,17 +31,21 @@ func TestCheckWithError(t *testing.T) { mockRequest.EXPECT().Options(options).Return(mockExecute) mockBody := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl) + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } body := client.ClientCheckRequest{ - User: "user:foo", - Relation: "writer", - Object: "doc:doc1", + User: "user:foo", + Relation: "writer", + Object: "doc:doc1", + ContextualTuples: &contextualTuples, } mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().Check(context.Background()).Return(mockBody) - _, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1") + _, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples) if err == nil { t.Error("Expect error but there is none") } @@ -71,16 +75,20 @@ func TestCheckWithNoError(t *testing.T) { mockBody := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl) + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } body := client.ClientCheckRequest{ - User: "user:foo", - Relation: "writer", - Object: "doc:doc1", + User: "user:foo", + Relation: "writer", + Object: "doc:doc1", + ContextualTuples: &contextualTuples, } mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().Check(context.Background()).Return(mockBody) - output, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1") + output, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples) if err != nil { t.Error(err) } diff --git a/cmd/query/list-objects.go b/cmd/query/list-objects.go index 8c198fe..7c7db7c 100644 --- a/cmd/query/list-objects.go +++ b/cmd/query/list-objects.go @@ -26,11 +26,18 @@ import ( ) // listObjects in the internal function for calling SDK for list objects. -func listObjects(fgaClient client.SdkClient, user string, relation string, types string) (string, error) { +func listObjects( + fgaClient client.SdkClient, + user string, + relation string, + objectType string, + contextualTuples []client.ClientTupleKey, +) (string, error) { body := &client.ClientListObjectsRequest{ - User: user, - Relation: relation, - Type: types, + User: user, + Relation: relation, + Type: objectType, + ContextualTuples: &contextualTuples, } options := &client.ClientListObjectsOptions{} @@ -61,9 +68,14 @@ var listObjectsCmd = &cobra.Command{ return fmt.Errorf("failed to initialize FGA Client due to %w", err) } - output, err := listObjects(fgaClient, args[0], args[1], args[2]) + contextualTuples, err := cmdutils.ParseContextualTuples(cmd) if err != nil { - return err + return fmt.Errorf("error parsing contextual tuples for listObjects: %w", err) + } + + output, err := listObjects(fgaClient, args[0], args[1], args[2], contextualTuples) + if err != nil { + return fmt.Errorf("error listing objects: %w", err) } fmt.Print(output) diff --git a/cmd/query/list-objects_test.go b/cmd/query/list-objects_test.go index b9b7a9f..ba098cc 100644 --- a/cmd/query/list-objects_test.go +++ b/cmd/query/list-objects_test.go @@ -31,16 +31,20 @@ func TestListObjectsWithError(t *testing.T) { mockBody := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl) + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } body := client.ClientListObjectsRequest{ - User: "user:foo", - Relation: "writer", - Type: "doc", + User: "user:foo", + Relation: "writer", + Type: "doc", + ContextualTuples: &contextualTuples, } mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody) - _, err := listObjects(mockFgaClient, "user:foo", "writer", "doc") + _, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples) if err == nil { t.Error("Expect error but there is none") } @@ -67,16 +71,20 @@ func TestListObjectsWithNoError(t *testing.T) { mockBody := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl) + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } body := client.ClientListObjectsRequest{ - User: "user:foo", - Relation: "writer", - Type: "doc", + User: "user:foo", + Relation: "writer", + Type: "doc", + ContextualTuples: &contextualTuples, } mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody) - output, err := listObjects(mockFgaClient, "user:foo", "writer", "doc") + output, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples) if err != nil { t.Error(err) } diff --git a/cmd/query/list-relations.go b/cmd/query/list-relations.go index 4c71d70..5061e4e 100644 --- a/cmd/query/list-relations.go +++ b/cmd/query/list-relations.go @@ -32,6 +32,7 @@ func listRelations(clientConfig fga.ClientConfig, fgaClient client.SdkClient, user string, objects string, + contextualTuples []client.ClientTupleKey, ) (string, error) { var authorizationModel openfga.AuthorizationModel @@ -69,9 +70,10 @@ func listRelations(clientConfig fga.ClientConfig, } body := &client.ClientListRelationsRequest{ - User: user, - Object: objects, - Relations: relations, + User: user, + Object: objects, + Relations: relations, + ContextualTuples: &contextualTuples, } options := &client.ClientListRelationsOptions{} @@ -103,9 +105,14 @@ var listRelationsCmd = &cobra.Command{ return fmt.Errorf("failed to initialize FGA Client due to %w", err) } - output, err := listRelations(clientConfig, fgaClient, args[0], args[1]) + contextualTuples, err := cmdutils.ParseContextualTuples(cmd) if err != nil { - return err + return fmt.Errorf("error parsing contextual tuples for listRelations: %w", err) + } + + output, err := listRelations(clientConfig, fgaClient, args[0], args[1], contextualTuples) + if err != nil { + return fmt.Errorf("error listing relations: %w", err) } fmt.Print(output) diff --git a/cmd/query/list-relations_test.go b/cmd/query/list-relations_test.go index a6c5423..1a68d19 100644 --- a/cmd/query/list-relations_test.go +++ b/cmd/query/list-relations_test.go @@ -32,7 +32,11 @@ func TestListRelationsLatestAuthModelError(t *testing.T) { var clientConfig fga.ClientConfig - _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1") + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } + _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples) + if err == nil { t.Error("Expect error but there is none") } @@ -56,7 +60,11 @@ func TestListRelationsAuthModelSpecifiedError(t *testing.T) { AuthorizationModelID: "01GXSA8YR785C4FYS3C0RTG7B1", } - _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1") + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } + _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples) + if err == nil { t.Error("Expect error but there is none") } @@ -94,10 +102,14 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) { mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl) + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } body := client.ClientListRelationsRequest{ - User: "user:foo", - Relations: []string{"viewer"}, - Object: "doc:doc1", + User: "user:foo", + Relations: []string{"viewer"}, + Object: "doc:doc1", + ContextualTuples: &contextualTuples, } mockBody.EXPECT().Body(body).Return(mockListRelationsRequest) gomock.InOrder( @@ -107,7 +119,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) { var clientConfig fga.ClientConfig - _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1") + _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples) if err == nil { t.Error("Expect error but there is none") } @@ -147,10 +159,14 @@ func TestListRelationsLatestAuthModelList(t *testing.T) { mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl) + contextualTuples := []client.ClientTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } body := client.ClientListRelationsRequest{ - User: "user:foo", - Relations: []string{"viewer"}, - Object: "doc:doc1", + User: "user:foo", + Relations: []string{"viewer"}, + Object: "doc:doc1", + ContextualTuples: &contextualTuples, } mockBody.EXPECT().Body(body).Return(mockListRelationsRequest) gomock.InOrder( @@ -160,7 +176,7 @@ func TestListRelationsLatestAuthModelList(t *testing.T) { var clientConfig fga.ClientConfig - output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1") + output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples) if err != nil { t.Error(err) } diff --git a/cmd/query/query.go b/cmd/query/query.go index 8e2629f..984c70a 100644 --- a/cmd/query/query.go +++ b/cmd/query/query.go @@ -35,9 +35,11 @@ func init() { QueryCmd.AddCommand(listRelationsCmd) QueryCmd.PersistentFlags().String("store-id", "", "Store ID") - QueryCmd.Flags().String("store-id", "", "Store ID") - err := QueryCmd.MarkFlagRequired("store-id") - if err != nil { //nolint:wsl + QueryCmd.PersistentFlags().String("model-id", "", "Model ID") + QueryCmd.PersistentFlags().StringArray("contextual-tuple", []string{}, `Contextual Tuple, format: "user relation object"`) //nolint:lll + + err := QueryCmd.MarkPersistentFlagRequired("store-id") + if err != nil { fmt.Print(err) os.Exit(1) } diff --git a/cmd/tuples/tuple.go b/cmd/tuples/tuple.go index 3e2a5e1..7df3119 100644 --- a/cmd/tuples/tuple.go +++ b/cmd/tuples/tuple.go @@ -35,8 +35,7 @@ func init() { TupleCmd.AddCommand(changesCmd) TupleCmd.PersistentFlags().String("store-id", "", "Store ID") - TupleCmd.Flags().String("store-id", "", "Store ID") - err := TupleCmd.MarkFlagRequired("store-id") + err := TupleCmd.MarkPersistentFlagRequired("store-id") if err != nil { //nolint:wsl fmt.Print(err) os.Exit(1) diff --git a/go.mod b/go.mod index 814126d..0d10376 100644 --- a/go.mod +++ b/go.mod @@ -21,9 +21,9 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fcc2e72..f2693c1 100644 --- a/go.sum +++ b/go.sum @@ -283,8 +283,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -323,8 +323,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -334,8 +334,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/lib/clierrors/clierrors.go b/lib/clierrors/clierrors.go new file mode 100644 index 0000000..4af97a5 --- /dev/null +++ b/lib/clierrors/clierrors.go @@ -0,0 +1,12 @@ +package clierrors + +import ( + "errors" + "fmt" +) + +var ErrValidation = errors.New("validation error") + +func ValidationError(op string, details string) error { + return fmt.Errorf("%w - %s: %s", ErrValidation, op, details) +} diff --git a/lib/cmd-utils/get-contextual-tuples.go b/lib/cmd-utils/get-contextual-tuples.go new file mode 100644 index 0000000..059d25d --- /dev/null +++ b/lib/cmd-utils/get-contextual-tuples.go @@ -0,0 +1,38 @@ +package cmdutils + +import ( + "strings" + + "github.com/openfga/cli/lib/clierrors" + "github.com/openfga/go-sdk/client" + "github.com/spf13/cobra" +) + +func ParseContextualTuplesInner(contextualTuplesArray []string) ([]client.ClientTupleKey, error) { + contextualTuples := []client.ClientTupleKey{} + + if len(contextualTuplesArray) > 0 { + for index := 0; index < len(contextualTuplesArray); index++ { + tuple := strings.Split(contextualTuplesArray[index], " ") + if len(tuple) != 3 { //nolint:gomnd + return contextualTuples, + clierrors.ValidationError("ParseContextualTuplesInner", "Failed to parse contextual tuples, "+ //nolint:wrapcheck + "they must be of the format\"user relation object\" ") + } + + contextualTuples = append(contextualTuples, client.ClientTupleKey{ + User: tuple[0], + Relation: tuple[1], + Object: tuple[2], + }) + } + } + + return contextualTuples, nil +} + +func ParseContextualTuples(cmd *cobra.Command) ([]client.ClientTupleKey, error) { + contextualTuplesArray, _ := cmd.Flags().GetStringArray("contextual-tuple") + + return ParseContextualTuplesInner(contextualTuplesArray) +} diff --git a/lib/cmd-utils/get-contextual-tuples_test.go b/lib/cmd-utils/get-contextual-tuples_test.go new file mode 100644 index 0000000..061bfd7 --- /dev/null +++ b/lib/cmd-utils/get-contextual-tuples_test.go @@ -0,0 +1,102 @@ +package cmdutils_test + +import ( + "testing" + + cmdutils "github.com/openfga/cli/lib/cmd-utils" + "github.com/openfga/go-sdk/client" +) + +type tupleTestPassing struct { + raw []string + parsed []client.ClientTupleKey +} + +type tupleTestFailing struct { + raw []string +} + +func TestGetContextualTuplesWithNoError(t *testing.T) { + t.Parallel() + + tests := []tupleTestPassing{{ + raw: []string{"user:anne can_view document:2"}, + parsed: []client.ClientTupleKey{ + {User: "user:anne", Relation: "can_view", Object: "document:2"}, + }, + }, { + raw: []string{ + "group:product#member owner document:roadmap", + "user:beth can_delete folder:marketing", + "user:carl can_share repo:openfga/openfga", + }, + parsed: []client.ClientTupleKey{ + {User: "group:product#member", Relation: "owner", Object: "document:roadmap"}, + {User: "user:beth", Relation: "can_delete", Object: "folder:marketing"}, + {User: "user:carl", Relation: "can_share", Object: "repo:openfga/openfga"}, + }, + }, { + // Note that this is an invalid tuple, but the server will let us know that. + // We can validate against it in a future iteration + raw: []string{"anne can_view document-2"}, + parsed: []client.ClientTupleKey{ + {User: "anne", Relation: "can_view", Object: "document-2"}, + }, + }} + + for index := 0; index < len(tests); index++ { + test := tests[index] + t.Run("TestGetContextualTuplesWithNoError"+string(rune(index)), func(t *testing.T) { + t.Parallel() + tuples, err := cmdutils.ParseContextualTuplesInner(test.raw) + if err != nil { + t.Error(err) + } + + if len(tuples) != len(test.parsed) { + t.Errorf("Expected parsed tuples to have length %v actual %v", len(test.parsed), len(tuples)) + } + + for index := 0; index < len(tuples); index++ { + if tuples[index].User != test.parsed[index].User || + tuples[index].Relation != test.parsed[index].Relation || + tuples[index].Object != test.parsed[index].Object { + t.Errorf("Expected parsed tuples to match %v actual %v", test.parsed, tuples) + } + } + }) + } +} + +func TestGetContextualTuplesWithError(t *testing.T) { + t.Parallel() + + tests := []tupleTestFailing{{ + raw: []string{"user:anne can_view"}, + }, { + raw: []string{"can_view document:2"}, + }, { + raw: []string{"can_view"}, + }, { + raw: []string{"this is not a valid tuple"}, + }, { + raw: []string{ + "group:product#member owner document:roadmap", + "user:beth can_delete folder:marketing", + "user:carl can_share repo:openfga/openfga", + "user:dan can_share", + }, + }} + + for index := 0; index < len(tests); index++ { + test := tests[index] + t.Run("TestGetContextualTuplesWithNoError"+string(rune(index)), func(t *testing.T) { + t.Parallel() + _, err := cmdutils.ParseContextualTuplesInner(test.raw) + + if err == nil { + t.Error("Expect error but there is none") + } + }) + } +}