diff --git a/cmd/query/list-users.go b/cmd/query/list-users.go index 0caf3775..204446cb 100644 --- a/cmd/query/list-users.go +++ b/cmd/query/list-users.go @@ -26,6 +26,7 @@ import ( "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" + "github.com/openfga/cli/internal/clierrors" "github.com/openfga/cli/internal/cmdutils" "github.com/openfga/cli/internal/output" ) @@ -50,13 +51,18 @@ func parseUserFilters(rawUserFilter string) []openfga.UserTypeFilter { return userFilters } -func parseObject(rawObject string) openfga.FgaObject { - splitObject := strings.Split(rawObject, ":") +func parseObject(rawObject string) (openfga.FgaObject, error) { + objectType, objectID, found := strings.Cut(rawObject, ":") + if !found || objectType == "" || objectID == "" { + return openfga.FgaObject{}, clierrors.ValidationError( //nolint:wrapcheck + "parseObject", + "Failed to parse object, it must be of the format \":\", for example \"document:roadmap\"") + } return openfga.FgaObject{ - Type: splitObject[0], - Id: splitObject[1], - } + Type: objectType, + Id: objectID, + }, nil } func listUsers( @@ -69,8 +75,13 @@ func listUsers( queryContext *map[string]any, consistency *openfga.ConsistencyPreference, ) (*client.ClientListUsersResponse, error) { + object, err := parseObject(rawObject) + if err != nil { + return nil, err + } + body := &client.ClientListUsersRequest{ - Object: parseObject(rawObject), + Object: object, Relation: relation, UserFilters: parseUserFilters(rawUserFilter), ContextualTuples: contextualTuples, diff --git a/cmd/query/list-users_test.go b/cmd/query/list-users_test.go index 1e41e85d..3be708c7 100644 --- a/cmd/query/list-users_test.go +++ b/cmd/query/list-users_test.go @@ -219,3 +219,76 @@ func TestListUsersWithConsistency(t *testing.T) { t.Errorf("Expect %v but actual %v", expectedResponse, *output) } } + +func TestListUsersObjectWithoutTypeReturnsError(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + _, err := listUsers( + t.Context(), + mockFgaClient, + "roadmap", + "admin", + "user", + []client.ClientContextualTupleKey{}, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) + if err == nil { + t.Fatal("expected an error for an object with no type separator, got nil") + } +} + +func TestListUsersObjectIDKeepsColons(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + mockExecute := mock_client.NewMockSdkClientListUsersRequestInterface(mockCtrl) + + expectedResponse := client.ClientListUsersResponse{Users: []openfga.User{}} + + mockExecute.EXPECT().Execute().Return(&expectedResponse, nil) + + mockRequest := mock_client.NewMockSdkClientListUsersRequestInterface(mockCtrl) + options := client.ClientListUsersOptions{} + mockRequest.EXPECT().Options(options).Return(mockExecute) + + mockBody := mock_client.NewMockSdkClientListUsersRequestInterface(mockCtrl) + + contextualTuples := []client.ClientContextualTupleKey{} + + body := client.ClientListUsersRequest{ + Object: openfga.FgaObject{ + Type: "doc", + Id: "urn:ietf:rfc:2648", + }, + Relation: "admin", + UserFilters: []openfga.UserTypeFilter{{Type: "user"}}, + ContextualTuples: contextualTuples, + Context: queryContext, + } + mockBody.EXPECT().Body(body).Return(mockRequest) + + mockFgaClient.EXPECT().ListUsers(t.Context()).Return(mockBody) + + if _, err := listUsers( + t.Context(), + mockFgaClient, + "doc:urn:ietf:rfc:2648", + "admin", + "user", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ); err != nil { + t.Fatal(err) + } +}