diff --git a/cmd/influx/org.go b/cmd/influx/org.go index 7d1e1ae..c22bce1 100644 --- a/cmd/influx/org.go +++ b/cmd/influx/org.go @@ -16,6 +16,7 @@ func newOrgCmd() *cli.Command { newOrgCreateCmd(), newOrgDeleteCmd(), newOrgListCmd(), + newOrgMembersCmd(), newOrgUpdateCmd(), }, } diff --git a/cmd/influx/org_members.go b/cmd/influx/org_members.go new file mode 100644 index 0000000..f58e223 --- /dev/null +++ b/cmd/influx/org_members.go @@ -0,0 +1,134 @@ +package main + +import ( + "github.com/influxdata/influx-cli/v2/internal/cmd/org" + "github.com/influxdata/influx-cli/v2/pkg/cli/middleware" + "github.com/urfave/cli/v2" +) + +func newOrgMembersCmd() *cli.Command { + return &cli.Command{ + Name: "members", + Usage: "Organization membership commands", + Subcommands: []*cli.Command{ + newOrgMembersAddCmd(), + newOrgMembersListCmd(), + newOrgMembersRemoveCmd(), + }, + } +} + +func newOrgMembersAddCmd() *cli.Command { + var params org.AddMemberParams + return &cli.Command{ + Name: "add", + Usage: "Add organization member", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlagsNoPrint(), + &cli.GenericFlag{ + Name: "member", + Usage: "The member ID", + Aliases: []string{"m"}, + Required: true, + Value: ¶ms.MemberId, + }, + &cli.StringFlag{ + Name: "name", + Usage: "The organization name", + Aliases: []string{"n"}, + EnvVars: []string{"INFLUX_ORG"}, + Destination: ¶ms.OrgName, + }, + &cli.GenericFlag{ + Name: "id", + Usage: "The organization ID", + Aliases: []string{"i"}, + EnvVars: []string{"INFLUX_ORG_ID"}, + Value: ¶ms.OrgID, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + } + return client.AddMember(ctx.Context, ¶ms) + }, + } +} + +func newOrgMembersListCmd() *cli.Command { + var params org.ListMemberParams + return &cli.Command{ + Name: "list", + Aliases: []string{"find", "ls"}, + Usage: "List organization members", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlags(), + &cli.StringFlag{ + Name: "name", + Usage: "The organization name", + Aliases: []string{"n"}, + EnvVars: []string{"INFLUX_ORG"}, + Destination: ¶ms.OrgName, + }, + &cli.GenericFlag{ + Name: "id", + Usage: "The organization ID", + Aliases: []string{"i"}, + EnvVars: []string{"INFLUX_ORG_ID"}, + Value: ¶ms.OrgID, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + UsersApi: getAPI(ctx).UsersApi, + } + return client.ListMembers(ctx.Context, ¶ms) + }, + } +} + +func newOrgMembersRemoveCmd() *cli.Command { + var params org.RemoveMemberParams + return &cli.Command{ + Name: "remove", + Usage: "Remove organization member", + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Flags: append( + commonFlagsNoPrint(), + &cli.GenericFlag{ + Name: "member", + Usage: "The member ID", + Aliases: []string{"m"}, + Required: true, + Value: ¶ms.MemberId, + }, + &cli.StringFlag{ + Name: "name", + Usage: "The organization name", + Aliases: []string{"n"}, + EnvVars: []string{"INFLUX_ORG"}, + Destination: ¶ms.OrgName, + }, + &cli.GenericFlag{ + Name: "id", + Usage: "The organization ID", + Aliases: []string{"i"}, + EnvVars: []string{"INFLUX_ORG_ID"}, + Value: ¶ms.OrgID, + }, + ), + Action: func(ctx *cli.Context) error { + client := org.Client{ + CLI: getCLI(ctx), + OrganizationsApi: getAPI(ctx).OrganizationsApi, + } + return client.RemoveMember(ctx.Context, ¶ms) + }, + } +} diff --git a/internal/api/api_users.gen.go b/internal/api/api_users.gen.go new file mode 100644 index 0000000..c57f318 --- /dev/null +++ b/internal/api/api_users.gen.go @@ -0,0 +1,519 @@ +/* + * Subset of Influx API covered by Influx CLI + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 2.0.0 + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package api + +import ( + _gzip "compress/gzip" + _context "context" + _io "io" + _ioutil "io/ioutil" + _nethttp "net/http" + _neturl "net/url" + "strings" +) + +// Linger please +var ( + _ _context.Context +) + +type UsersApi interface { + + /* + * DeleteUsersID Delete a user + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param userID The ID of the user to delete. + * @return ApiDeleteUsersIDRequest + */ + DeleteUsersID(ctx _context.Context, userID string) ApiDeleteUsersIDRequest + + /* + * DeleteUsersIDExecute executes the request + */ + DeleteUsersIDExecute(r ApiDeleteUsersIDRequest) error + + /* + * GetUsersID Retrieve a user + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param userID The user ID. + * @return ApiGetUsersIDRequest + */ + GetUsersID(ctx _context.Context, userID string) ApiGetUsersIDRequest + + /* + * GetUsersIDExecute executes the request + * @return UserResponse + */ + GetUsersIDExecute(r ApiGetUsersIDRequest) (UserResponse, error) + + /* + * PatchUsersID Update a user + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param userID The ID of the user to update. + * @return ApiPatchUsersIDRequest + */ + PatchUsersID(ctx _context.Context, userID string) ApiPatchUsersIDRequest + + /* + * PatchUsersIDExecute executes the request + * @return UserResponse + */ + PatchUsersIDExecute(r ApiPatchUsersIDRequest) (UserResponse, error) +} + +// usersApiGzipReadCloser supports streaming gzip response-bodies directly from the server. +type usersApiGzipReadCloser struct { + underlying _io.ReadCloser + gzip _io.ReadCloser +} + +func (gzrc *usersApiGzipReadCloser) Read(p []byte) (int, error) { + return gzrc.gzip.Read(p) +} +func (gzrc *usersApiGzipReadCloser) Close() error { + if err := gzrc.gzip.Close(); err != nil { + return err + } + return gzrc.underlying.Close() +} + +// UsersApiService UsersApi service +type UsersApiService service + +type ApiDeleteUsersIDRequest struct { + ctx _context.Context + ApiService UsersApi + userID string + zapTraceSpan *string +} + +func (r ApiDeleteUsersIDRequest) UserID(userID string) ApiDeleteUsersIDRequest { + r.userID = userID + return r +} +func (r ApiDeleteUsersIDRequest) GetUserID() string { + return r.userID +} + +func (r ApiDeleteUsersIDRequest) ZapTraceSpan(zapTraceSpan string) ApiDeleteUsersIDRequest { + r.zapTraceSpan = &zapTraceSpan + return r +} +func (r ApiDeleteUsersIDRequest) GetZapTraceSpan() *string { + return r.zapTraceSpan +} + +func (r ApiDeleteUsersIDRequest) Execute() error { + return r.ApiService.DeleteUsersIDExecute(r) +} + +/* + * DeleteUsersID Delete a user + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param userID The ID of the user to delete. + * @return ApiDeleteUsersIDRequest + */ +func (a *UsersApiService) DeleteUsersID(ctx _context.Context, userID string) ApiDeleteUsersIDRequest { + return ApiDeleteUsersIDRequest{ + ApiService: a, + ctx: ctx, + userID: userID, + } +} + +/* + * Execute executes the request + */ +func (a *UsersApiService) DeleteUsersIDExecute(r ApiDeleteUsersIDRequest) error { + var ( + localVarHTTPMethod = _nethttp.MethodDelete + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "UsersApiService.DeleteUsersID") + if err != nil { + return GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/users/{userID}" + localVarPath = strings.Replace(localVarPath, "{"+"userID"+"}", _neturl.PathEscape(parameterToString(r.userID, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.zapTraceSpan != nil { + localVarHeaderParams["Zap-Trace-Span"] = parameterToString(*r.zapTraceSpan, "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return err + } + + var body _io.ReadCloser = localVarHTTPResponse.Body + if localVarHTTPResponse.Header.Get("Content-Encoding") == "gzip" { + gzr, err := _gzip.NewReader(body) + if err != nil { + body.Close() + return err + } + body = &usersApiGzipReadCloser{underlying: body, gzip: gzr} + } + + if localVarHTTPResponse.StatusCode >= 300 { + localVarBody, err := _ioutil.ReadAll(body) + body.Close() + if err != nil { + return err + } + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return newErr + } + newErr.model = &v + return newErr + } + + return nil +} + +type ApiGetUsersIDRequest struct { + ctx _context.Context + ApiService UsersApi + userID string + zapTraceSpan *string +} + +func (r ApiGetUsersIDRequest) UserID(userID string) ApiGetUsersIDRequest { + r.userID = userID + return r +} +func (r ApiGetUsersIDRequest) GetUserID() string { + return r.userID +} + +func (r ApiGetUsersIDRequest) ZapTraceSpan(zapTraceSpan string) ApiGetUsersIDRequest { + r.zapTraceSpan = &zapTraceSpan + return r +} +func (r ApiGetUsersIDRequest) GetZapTraceSpan() *string { + return r.zapTraceSpan +} + +func (r ApiGetUsersIDRequest) Execute() (UserResponse, error) { + return r.ApiService.GetUsersIDExecute(r) +} + +/* + * GetUsersID Retrieve a user + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param userID The user ID. + * @return ApiGetUsersIDRequest + */ +func (a *UsersApiService) GetUsersID(ctx _context.Context, userID string) ApiGetUsersIDRequest { + return ApiGetUsersIDRequest{ + ApiService: a, + ctx: ctx, + userID: userID, + } +} + +/* + * Execute executes the request + * @return UserResponse + */ +func (a *UsersApiService) GetUsersIDExecute(r ApiGetUsersIDRequest) (UserResponse, error) { + var ( + localVarHTTPMethod = _nethttp.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue UserResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "UsersApiService.GetUsersID") + if err != nil { + return localVarReturnValue, GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/users/{userID}" + localVarPath = strings.Replace(localVarPath, "{"+"userID"+"}", _neturl.PathEscape(parameterToString(r.userID, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.zapTraceSpan != nil { + localVarHeaderParams["Zap-Trace-Span"] = parameterToString(*r.zapTraceSpan, "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, err + } + + var body _io.ReadCloser = localVarHTTPResponse.Body + if localVarHTTPResponse.Header.Get("Content-Encoding") == "gzip" { + gzr, err := _gzip.NewReader(body) + if err != nil { + body.Close() + return localVarReturnValue, err + } + body = &usersApiGzipReadCloser{underlying: body, gzip: gzr} + } + + if localVarHTTPResponse.StatusCode >= 300 { + localVarBody, err := _ioutil.ReadAll(body) + body.Close() + if err != nil { + return localVarReturnValue, err + } + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, newErr + } + newErr.model = &v + return localVarReturnValue, newErr + } + + localVarBody, err := _ioutil.ReadAll(body) + body.Close() + if err != nil { + return localVarReturnValue, err + } + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, newErr + } + + return localVarReturnValue, nil +} + +type ApiPatchUsersIDRequest struct { + ctx _context.Context + ApiService UsersApi + userID string + user *User + zapTraceSpan *string +} + +func (r ApiPatchUsersIDRequest) UserID(userID string) ApiPatchUsersIDRequest { + r.userID = userID + return r +} +func (r ApiPatchUsersIDRequest) GetUserID() string { + return r.userID +} + +func (r ApiPatchUsersIDRequest) User(user User) ApiPatchUsersIDRequest { + r.user = &user + return r +} +func (r ApiPatchUsersIDRequest) GetUser() *User { + return r.user +} + +func (r ApiPatchUsersIDRequest) ZapTraceSpan(zapTraceSpan string) ApiPatchUsersIDRequest { + r.zapTraceSpan = &zapTraceSpan + return r +} +func (r ApiPatchUsersIDRequest) GetZapTraceSpan() *string { + return r.zapTraceSpan +} + +func (r ApiPatchUsersIDRequest) Execute() (UserResponse, error) { + return r.ApiService.PatchUsersIDExecute(r) +} + +/* + * PatchUsersID Update a user + * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param userID The ID of the user to update. + * @return ApiPatchUsersIDRequest + */ +func (a *UsersApiService) PatchUsersID(ctx _context.Context, userID string) ApiPatchUsersIDRequest { + return ApiPatchUsersIDRequest{ + ApiService: a, + ctx: ctx, + userID: userID, + } +} + +/* + * Execute executes the request + * @return UserResponse + */ +func (a *UsersApiService) PatchUsersIDExecute(r ApiPatchUsersIDRequest) (UserResponse, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPatch + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue UserResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "UsersApiService.PatchUsersID") + if err != nil { + return localVarReturnValue, GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/users/{userID}" + localVarPath = strings.Replace(localVarPath, "{"+"userID"+"}", _neturl.PathEscape(parameterToString(r.userID, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + if r.user == nil { + return localVarReturnValue, reportError("user is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.zapTraceSpan != nil { + localVarHeaderParams["Zap-Trace-Span"] = parameterToString(*r.zapTraceSpan, "") + } + // body params + localVarPostBody = r.user + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, err + } + + var body _io.ReadCloser = localVarHTTPResponse.Body + if localVarHTTPResponse.Header.Get("Content-Encoding") == "gzip" { + gzr, err := _gzip.NewReader(body) + if err != nil { + body.Close() + return localVarReturnValue, err + } + body = &usersApiGzipReadCloser{underlying: body, gzip: gzr} + } + + if localVarHTTPResponse.StatusCode >= 300 { + localVarBody, err := _ioutil.ReadAll(body) + body.Close() + if err != nil { + return localVarReturnValue, err + } + newErr := GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, newErr + } + newErr.model = &v + return localVarReturnValue, newErr + } + + localVarBody, err := _ioutil.ReadAll(body) + body.Close() + if err != nil { + return localVarReturnValue, err + } + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, newErr + } + + return localVarReturnValue, nil +} diff --git a/internal/api/client.gen.go b/internal/api/client.gen.go index e3e1d0c..a37f140 100644 --- a/internal/api/client.gen.go +++ b/internal/api/client.gen.go @@ -59,6 +59,8 @@ type APIClient struct { SetupApi SetupApi + UsersApi UsersApi + WriteApi WriteApi } @@ -84,6 +86,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.OrganizationsApi = (*OrganizationsApiService)(&c.common) c.QueryApi = (*QueryApiService)(&c.common) c.SetupApi = (*SetupApiService)(&c.common) + c.UsersApi = (*UsersApiService)(&c.common) c.WriteApi = (*WriteApiService)(&c.common) return c diff --git a/internal/api/contract/cli.yml b/internal/api/contract/cli.yml index ca45bc5..814e1ee 100644 --- a/internal/api/contract/cli.yml +++ b/internal/api/contract/cli.yml @@ -31,6 +31,8 @@ paths: $ref: "./openapi/src/cloud/paths/measurements_measurementID.yml" /query: $ref: "./overrides/paths/query.yml" + /users/{userID}: + $ref: "./openapi/src/common/paths/users_userID.yml" components: parameters: TraceSpan: @@ -56,6 +58,8 @@ components: $ref: "./openapi/src/common/schemas/OnboardingRequest.yml" OnboardingResponse: $ref: "./openapi/src/common/schemas/OnboardingResponse.yml" + User: + $ref: "./openapi/src/common/schemas/User.yml" UserResponse: $ref: "./openapi/src/common/schemas/UserResponse.yml" Links: diff --git a/internal/api/model_user.gen.go b/internal/api/model_user.gen.go new file mode 100644 index 0000000..f72dfe6 --- /dev/null +++ b/internal/api/model_user.gen.go @@ -0,0 +1,219 @@ +/* + * Subset of Influx API covered by Influx CLI + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * API version: 2.0.0 + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package api + +import ( + "encoding/json" +) + +// User struct for User +type User struct { + Id *string `json:"id,omitempty"` + OauthID *string `json:"oauthID,omitempty"` + Name string `json:"name"` + // If inactive the user is inactive. + Status *string `json:"status,omitempty"` +} + +// NewUser instantiates a new User object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUser(name string) *User { + this := User{} + this.Name = name + var status string = "active" + this.Status = &status + return &this +} + +// NewUserWithDefaults instantiates a new User object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewUserWithDefaults() *User { + this := User{} + var status string = "active" + this.Status = &status + return &this +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *User) GetId() string { + if o == nil || o.Id == nil { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *User) GetIdOk() (*string, bool) { + if o == nil || o.Id == nil { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *User) HasId() bool { + if o != nil && o.Id != nil { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *User) SetId(v string) { + o.Id = &v +} + +// GetOauthID returns the OauthID field value if set, zero value otherwise. +func (o *User) GetOauthID() string { + if o == nil || o.OauthID == nil { + var ret string + return ret + } + return *o.OauthID +} + +// GetOauthIDOk returns a tuple with the OauthID field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *User) GetOauthIDOk() (*string, bool) { + if o == nil || o.OauthID == nil { + return nil, false + } + return o.OauthID, true +} + +// HasOauthID returns a boolean if a field has been set. +func (o *User) HasOauthID() bool { + if o != nil && o.OauthID != nil { + return true + } + + return false +} + +// SetOauthID gets a reference to the given string and assigns it to the OauthID field. +func (o *User) SetOauthID(v string) { + o.OauthID = &v +} + +// GetName returns the Name field value +func (o *User) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *User) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *User) SetName(v string) { + o.Name = v +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *User) GetStatus() string { + if o == nil || o.Status == nil { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *User) GetStatusOk() (*string, bool) { + if o == nil || o.Status == nil { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *User) HasStatus() bool { + if o != nil && o.Status != nil { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *User) SetStatus(v string) { + o.Status = &v +} + +func (o User) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Id != nil { + toSerialize["id"] = o.Id + } + if o.OauthID != nil { + toSerialize["oauthID"] = o.OauthID + } + if true { + toSerialize["name"] = o.Name + } + if o.Status != nil { + toSerialize["status"] = o.Status + } + return json.Marshal(toSerialize) +} + +type NullableUser struct { + value *User + isSet bool +} + +func (v NullableUser) Get() *User { + return v.value +} + +func (v *NullableUser) Set(val *User) { + v.value = val + v.isSet = true +} + +func (v NullableUser) IsSet() bool { + return v.isSet +} + +func (v *NullableUser) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableUser(val *User) *NullableUser { + return &NullableUser{value: val, isSet: true} +} + +func (v NullableUser) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableUser) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/cmd/org/client.go b/internal/cmd/org/client.go index 195acdf..78c2aa8 100644 --- a/internal/cmd/org/client.go +++ b/internal/cmd/org/client.go @@ -8,6 +8,7 @@ import ( type Client struct { cmd.CLI api.OrganizationsApi + api.UsersApi } type printOrgOpts struct { diff --git a/internal/cmd/org/org_members.go b/internal/cmd/org/org_members.go new file mode 100644 index 0000000..6eace8a --- /dev/null +++ b/internal/cmd/org/org_members.go @@ -0,0 +1,164 @@ +package org + +import ( + "context" + "errors" + "fmt" + + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/pkg/influxid" +) + +var ErrMustSpecifyOrg = errors.New("must specify org ID or org name") + +type AddMemberParams struct { + MemberId influxid.ID + OrgName string + OrgID influxid.ID +} + +func (c Client) AddMember(ctx context.Context, params *AddMemberParams) (err error) { + if !params.OrgID.Valid() && params.OrgName == "" && c.ActiveConfig.Org == "" { + return ErrMustSpecifyOrg + } + + orgID := params.OrgID.String() + if !params.OrgID.Valid() { + if orgID, err = c.getOrgId(ctx, params.OrgName); err != nil { + return err + } + } + + member, err := c.PostOrgsIDMembers(ctx, orgID). + AddResourceMemberRequestBody(*api.NewAddResourceMemberRequestBody(params.MemberId.String())). + Execute() + if err != nil { + return fmt.Errorf("failed to add user %q to org %q: %w", params.MemberId.String(), orgID, err) + } + + _, err = c.StdIO.Write([]byte(fmt.Sprintf("user %q has been added as a member of org %q\n", *member.Id, orgID))) + return err +} + +type ListMemberParams struct { + OrgName string + OrgID influxid.ID +} + +const maxConcurrentGetUserRequests = 10 + +func (c Client) ListMembers(ctx context.Context, params *ListMemberParams) (err error) { + if !params.OrgID.Valid() && params.OrgName == "" && c.ActiveConfig.Org == "" { + return ErrMustSpecifyOrg + } + + orgID := params.OrgID.String() + if !params.OrgID.Valid() { + if orgID, err = c.getOrgId(ctx, params.OrgName); err != nil { + return err + } + } + + members, err := c.GetOrgsIDMembers(ctx, orgID).Execute() + if err != nil { + return fmt.Errorf("failed to find members of org %q: %w", orgID, err) + } + + type indexedUser struct { + user api.UserResponse + index int + } + userChan := make(chan indexedUser, maxConcurrentGetUserRequests) + semChan := make(chan struct{}, maxConcurrentGetUserRequests) + errChan := make(chan error) + + var resourceMembers []api.ResourceMember + if members.Users != nil { + resourceMembers = *members.Users + } + // Fetch user details about all members of the org. + for i, member := range resourceMembers { + go func(i int, memberId string) { + semChan <- struct{}{} + defer func() { <-semChan }() + + user, err := c.GetUsersID(ctx, memberId).Execute() + if err != nil { + errChan <- fmt.Errorf("failed to retrieve details for user %q: %w", memberId, err) + return + } + userChan <- indexedUser{user: user, index: i} + }(i, member.GetId()) + } + + users := make([]api.UserResponse, len(resourceMembers)) + for range resourceMembers { + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errChan: + return err + case user := <-userChan: + users[user.index] = user.user + } + } + + if c.PrintAsJSON { + return c.PrintJSON(users) + } + + rows := make([]map[string]interface{}, len(resourceMembers)) + for i, user := range users { + rows[i] = map[string]interface{}{ + "ID": user.GetId(), + "Name": user.GetName(), + "User Type": "member", + "Status": user.GetStatus(), + } + } + + return c.PrintTable([]string{"ID", "Name", "User Type", "Status"}, rows...) +} + +type RemoveMemberParams struct { + MemberId influxid.ID + OrgName string + OrgID influxid.ID +} + +func (c Client) RemoveMember(ctx context.Context, params *RemoveMemberParams) (err error) { + if !params.OrgID.Valid() && params.OrgName == "" && c.ActiveConfig.Org == "" { + return ErrMustSpecifyOrg + } + + orgID := params.OrgID.String() + if !params.OrgID.Valid() { + if orgID, err = c.getOrgId(ctx, params.OrgName); err != nil { + return err + } + } + + if err = c.DeleteOrgsIDMembersID(ctx, orgID, params.MemberId.String()).Execute(); err != nil { + return fmt.Errorf("failed to remove member %q from org %q", params.MemberId, orgID) + } + + _, err = c.StdIO.Write([]byte(fmt.Sprintf("user %q has been removed from org %q\n", params.MemberId, orgID))) + return err +} + +func (c Client) getOrgId(ctx context.Context, orgName string) (string, error) { + req := c.GetOrgs(ctx) + if orgName != "" { + req = req.Org(orgName) + } else { + req = req.Org(c.ActiveConfig.Org) + } + orgs, err := req.Execute() + if err != nil { + return "", fmt.Errorf("failed to find org %q: %w", orgName, err) + } + if orgs.Orgs == nil || len(*orgs.Orgs) == 0 { + return "", fmt.Errorf("no org found with name %q", orgName) + } + return *(*orgs.Orgs)[0].Id, nil +} diff --git a/internal/cmd/org/org_members_test.go b/internal/cmd/org/org_members_test.go new file mode 100644 index 0000000..7630a86 --- /dev/null +++ b/internal/cmd/org/org_members_test.go @@ -0,0 +1,452 @@ +package org_test + +import ( + "bytes" + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/golang/mock/gomock" + "github.com/influxdata/influx-cli/v2/internal/api" + "github.com/influxdata/influx-cli/v2/internal/cmd" + "github.com/influxdata/influx-cli/v2/internal/cmd/org" + "github.com/influxdata/influx-cli/v2/internal/config" + "github.com/influxdata/influx-cli/v2/internal/mock" + "github.com/influxdata/influx-cli/v2/internal/testutils" + "github.com/influxdata/influx-cli/v2/pkg/influxid" + "github.com/stretchr/testify/assert" + tmock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var id1, _ = influxid.IDFromString("1111111111111111") +var id2, _ = influxid.IDFromString("2222222222222222") + +func TestClient_AddMember(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params org.AddMemberParams + defaultOrgName string + registerExpectations func(*testing.T, *mock.MockOrganizationsApi) + expectedOut string + expectedErr string + }{ + { + name: "org by ID", + params: org.AddMemberParams{ + OrgID: id1, + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().PostOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())). + Return(api.ApiPostOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String())) + orgApi.EXPECT().PostOrgsIDMembersExecute(tmock.MatchedBy(func(in api.ApiPostOrgsIDMembersRequest) bool { + body := in.GetAddResourceMemberRequestBody() + return assert.Equal(t, id1.String(), in.GetOrgID()) && + assert.NotNil(t, body) && + assert.Equal(t, id2.String(), body.GetId()) + })).Return(api.ResourceMember{Id: api.PtrString(id2.String())}, nil) + }, + expectedOut: "user \"2222222222222222\" has been added as a member of org \"1111111111111111\"", + }, + { + name: "org by name", + params: org.AddMemberParams{ + OrgName: "org", + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "org", *in.GetOrg()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString(id1.String())}}, + }, nil) + + orgApi.EXPECT().PostOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())). + Return(api.ApiPostOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String())) + orgApi.EXPECT().PostOrgsIDMembersExecute(tmock.MatchedBy(func(in api.ApiPostOrgsIDMembersRequest) bool { + body := in.GetAddResourceMemberRequestBody() + return assert.Equal(t, id1.String(), in.GetOrgID()) && + assert.NotNil(t, body) && + assert.Equal(t, id2.String(), body.GetId()) + })).Return(api.ResourceMember{Id: api.PtrString(id2.String())}, nil) + }, + expectedOut: "user \"2222222222222222\" has been added as a member of org \"1111111111111111\"", + }, + { + name: "by config org", + params: org.AddMemberParams{ + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString(id1.String())}}, + }, nil) + + orgApi.EXPECT().PostOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())). + Return(api.ApiPostOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String())) + orgApi.EXPECT().PostOrgsIDMembersExecute(tmock.MatchedBy(func(in api.ApiPostOrgsIDMembersRequest) bool { + body := in.GetAddResourceMemberRequestBody() + return assert.Equal(t, id1.String(), in.GetOrgID()) && + assert.NotNil(t, body) && + assert.Equal(t, id2.String(), body.GetId()) + })).Return(api.ResourceMember{Id: api.PtrString(id2.String())}, nil) + }, + expectedOut: "user \"2222222222222222\" has been added as a member of org \"1111111111111111\"", + }, + { + name: "no such org", + params: org.AddMemberParams{ + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Organizations{}, errors.New("not found")) + }, + expectedErr: "not found", + }, + { + name: "missing org", + expectedErr: org.ErrMustSpecifyOrg.Error(), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + api := mock.NewMockOrganizationsApi(ctrl) + if tc.registerExpectations != nil { + tc.registerExpectations(t, api) + } + + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{ + CLI: cmd.CLI{StdIO: stdio, ActiveConfig: config.Config{Org: tc.defaultOrgName}}, + OrganizationsApi: api, + } + err := cli.AddMember(context.Background(), &tc.params) + if tc.expectedErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectedOut, strings.TrimSpace(stdout.String())) + }) + } +} + +func TestClient_ListMembers(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params org.ListMemberParams + defaultOrgName string + registerOrgExpectations func(*testing.T, *mock.MockOrganizationsApi) + registerUserExpectations func(*testing.T, *mock.MockUsersApi) + expectedOut []string + expectedErr string + }{ + { + name: "no members", + defaultOrgName: "my-org", + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString(id1.String())}}, + }, nil) + + req := api.ApiGetOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String()) + orgApi.EXPECT().GetOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())).Return(req) + orgApi.EXPECT().GetOrgsIDMembersExecute(gomock.Eq(req)).Return(api.ResourceMembers{}, nil) + }, + }, + { + name: "one member", + params: org.ListMemberParams{ + OrgName: "org", + }, + defaultOrgName: "my-org", + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "org", *in.GetOrg()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString(id1.String())}}, + }, nil) + + req := api.ApiGetOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String()) + orgApi.EXPECT().GetOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())).Return(req) + orgApi.EXPECT().GetOrgsIDMembersExecute(gomock.Eq(req)). + Return(api.ResourceMembers{Users: &[]api.ResourceMember{{Id: api.PtrString(id2.String())}}}, nil) + }, + registerUserExpectations: func(t *testing.T, userApi *mock.MockUsersApi) { + req := api.ApiGetUsersIDRequest{ApiService: userApi}.UserID(id2.String()) + userApi.EXPECT().GetUsersID(gomock.Any(), gomock.Eq(id2.String())).Return(req) + userApi.EXPECT().GetUsersIDExecute(gomock.Eq(req)).Return(api.UserResponse{ + Id: api.PtrString(id2.String()), + Name: "user1", + Status: api.PtrString("active"), + }, nil) + }, + expectedOut: []string{`2222222222222222\s+user1\s+member\s+active`}, + }, + { + name: "many members", + params: org.ListMemberParams{ + OrgID: id1, + }, + // NOTE: We previously saw a deadlock when # members was > 10, so test that here. + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + req := api.ApiGetOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String()) + orgApi.EXPECT().GetOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())).Return(req) + members := make([]api.ResourceMember, 11) + for i := 0; i < 11; i++ { + members[i] = api.ResourceMember{Id: api.PtrString(fmt.Sprintf("%016d", i))} + } + orgApi.EXPECT().GetOrgsIDMembersExecute(gomock.Eq(req)).Return(api.ResourceMembers{Users: &members}, nil) + }, + registerUserExpectations: func(t *testing.T, userApi *mock.MockUsersApi) { + for i := 0; i < 11; i++ { + id := fmt.Sprintf("%016d", i) + status := "active" + if i%2 == 0 { + status = "inactive" + } + req := api.ApiGetUsersIDRequest{ApiService: userApi}.UserID(id) + userApi.EXPECT().GetUsersID(gomock.Any(), gomock.Eq(id)).Return(req) + userApi.EXPECT().GetUsersIDExecute(gomock.Eq(req)).Return(api.UserResponse{ + Id: &id, + Name: fmt.Sprintf("user%d", i), + Status: &status, + }, nil) + } + }, + expectedOut: []string{ + `0000000000000000\s+user0\s+member\s+inactive`, + `0000000000000001\s+user1\s+member\s+active`, + `0000000000000002\s+user2\s+member\s+inactive`, + `0000000000000003\s+user3\s+member\s+active`, + `0000000000000004\s+user4\s+member\s+inactive`, + `0000000000000005\s+user5\s+member\s+active`, + `0000000000000006\s+user6\s+member\s+inactive`, + `0000000000000007\s+user7\s+member\s+active`, + `0000000000000008\s+user8\s+member\s+inactive`, + `0000000000000009\s+user9\s+member\s+active`, + `0000000000000010\s+user10\s+member\s+inactive`, + }, + }, + { + name: "no such org", + defaultOrgName: "my-org", + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Organizations{}, errors.New("not found")) + }, + expectedErr: "not found", + }, + { + name: "no such user", + params: org.ListMemberParams{ + OrgID: id1, + }, + registerOrgExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + req := api.ApiGetOrgsIDMembersRequest{ApiService: orgApi}.OrgID(id1.String()) + orgApi.EXPECT().GetOrgsIDMembers(gomock.Any(), gomock.Eq(id1.String())).Return(req) + orgApi.EXPECT().GetOrgsIDMembersExecute(gomock.Eq(req)). + Return(api.ResourceMembers{Users: &[]api.ResourceMember{{Id: api.PtrString(id2.String())}}}, nil) + }, + registerUserExpectations: func(t *testing.T, userApi *mock.MockUsersApi) { + req := api.ApiGetUsersIDRequest{ApiService: userApi}.UserID(id2.String()) + userApi.EXPECT().GetUsersID(gomock.Any(), gomock.Eq(id2.String())).Return(req) + userApi.EXPECT().GetUsersIDExecute(gomock.Eq(req)).Return(api.UserResponse{}, errors.New("not found")) + }, + expectedErr: "user \"2222222222222222\": not found", + }, + { + name: "missing org", + expectedErr: org.ErrMustSpecifyOrg.Error(), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + orgApi := mock.NewMockOrganizationsApi(ctrl) + userApi := mock.NewMockUsersApi(ctrl) + if tc.registerOrgExpectations != nil { + tc.registerOrgExpectations(t, orgApi) + } + if tc.registerUserExpectations != nil { + tc.registerUserExpectations(t, userApi) + } + + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{ + CLI: cmd.CLI{StdIO: stdio, ActiveConfig: config.Config{Org: tc.defaultOrgName}}, + OrganizationsApi: orgApi, + UsersApi: userApi, + } + err := cli.ListMembers(context.Background(), &tc.params) + if tc.expectedErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + return + } + require.NoError(t, err) + testutils.MatchLines(t, append([]string{`ID\s+Name\s+User Type\s+Status`}, tc.expectedOut...), + strings.Split(stdout.String(), "\n")) + }) + } +} + +func TestClient_RemoveMembers(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params org.RemoveMemberParams + defaultOrgName string + registerExpectations func(*testing.T, *mock.MockOrganizationsApi) + expectedOut string + expectedErr string + }{ + { + name: "org by ID", + params: org.RemoveMemberParams{ + OrgID: id1, + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + req := api.ApiDeleteOrgsIDMembersIDRequest{ApiService: orgApi}.OrgID(id1.String()).UserID(id2.String()) + orgApi.EXPECT(). + DeleteOrgsIDMembersID(gomock.Any(), gomock.Eq(id1.String()), gomock.Eq(id2.String())).Return(req) + orgApi.EXPECT().DeleteOrgsIDMembersIDExecute(gomock.Eq(req)).Return(nil) + }, + expectedOut: "user \"2222222222222222\" has been removed from org \"1111111111111111\"", + }, + { + name: "org by name", + params: org.RemoveMemberParams{ + OrgName: "org", + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "org", *in.GetOrg()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString(id1.String())}}, + }, nil) + + req := api.ApiDeleteOrgsIDMembersIDRequest{ApiService: orgApi}.OrgID(id1.String()).UserID(id2.String()) + orgApi.EXPECT(). + DeleteOrgsIDMembersID(gomock.Any(), gomock.Eq(id1.String()), gomock.Eq(id2.String())).Return(req) + orgApi.EXPECT().DeleteOrgsIDMembersIDExecute(gomock.Eq(req)).Return(nil) + }, + expectedOut: "user \"2222222222222222\" has been removed from org \"1111111111111111\"", + }, + { + name: "by config org", + params: org.RemoveMemberParams{ + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Organizations{ + Orgs: &[]api.Organization{{Id: api.PtrString(id1.String())}}, + }, nil) + + req := api.ApiDeleteOrgsIDMembersIDRequest{ApiService: orgApi}.OrgID(id1.String()).UserID(id2.String()) + orgApi.EXPECT(). + DeleteOrgsIDMembersID(gomock.Any(), gomock.Eq(id1.String()), gomock.Eq(id2.String())).Return(req) + orgApi.EXPECT().DeleteOrgsIDMembersIDExecute(gomock.Eq(req)).Return(nil) + }, + expectedOut: "user \"2222222222222222\" has been removed from org \"1111111111111111\"", + }, + { + name: "no such org", + params: org.RemoveMemberParams{ + MemberId: id2, + }, + defaultOrgName: "my-org", + registerExpectations: func(t *testing.T, orgApi *mock.MockOrganizationsApi) { + orgApi.EXPECT().GetOrgs(gomock.Any()).Return(api.ApiGetOrgsRequest{ApiService: orgApi}) + orgApi.EXPECT().GetOrgsExecute(tmock.MatchedBy(func(in api.ApiGetOrgsRequest) bool { + return assert.Equal(t, "my-org", *in.GetOrg()) + })).Return(api.Organizations{}, errors.New("not found")) + }, + expectedErr: "not found", + }, + { + name: "missing org", + expectedErr: org.ErrMustSpecifyOrg.Error(), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + api := mock.NewMockOrganizationsApi(ctrl) + if tc.registerExpectations != nil { + tc.registerExpectations(t, api) + } + + stdout := bytes.Buffer{} + stdio := mock.NewMockStdIO(ctrl) + stdio.EXPECT().Write(gomock.Any()).DoAndReturn(stdout.Write).AnyTimes() + + cli := org.Client{ + CLI: cmd.CLI{StdIO: stdio, ActiveConfig: config.Config{Org: tc.defaultOrgName}}, + OrganizationsApi: api, + } + err := cli.RemoveMember(context.Background(), &tc.params) + if tc.expectedErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectedOut, strings.TrimSpace(stdout.String())) + }) + } +} diff --git a/internal/mock/api_users.gen.go b/internal/mock/api_users.gen.go new file mode 100644 index 0000000..c90e01e --- /dev/null +++ b/internal/mock/api_users.gen.go @@ -0,0 +1,122 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/influxdata/influx-cli/v2/internal/api (interfaces: UsersApi) + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + api "github.com/influxdata/influx-cli/v2/internal/api" +) + +// MockUsersApi is a mock of UsersApi interface. +type MockUsersApi struct { + ctrl *gomock.Controller + recorder *MockUsersApiMockRecorder +} + +// MockUsersApiMockRecorder is the mock recorder for MockUsersApi. +type MockUsersApiMockRecorder struct { + mock *MockUsersApi +} + +// NewMockUsersApi creates a new mock instance. +func NewMockUsersApi(ctrl *gomock.Controller) *MockUsersApi { + mock := &MockUsersApi{ctrl: ctrl} + mock.recorder = &MockUsersApiMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsersApi) EXPECT() *MockUsersApiMockRecorder { + return m.recorder +} + +// DeleteUsersID mocks base method. +func (m *MockUsersApi) DeleteUsersID(arg0 context.Context, arg1 string) api.ApiDeleteUsersIDRequest { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUsersID", arg0, arg1) + ret0, _ := ret[0].(api.ApiDeleteUsersIDRequest) + return ret0 +} + +// DeleteUsersID indicates an expected call of DeleteUsersID. +func (mr *MockUsersApiMockRecorder) DeleteUsersID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUsersID", reflect.TypeOf((*MockUsersApi)(nil).DeleteUsersID), arg0, arg1) +} + +// DeleteUsersIDExecute mocks base method. +func (m *MockUsersApi) DeleteUsersIDExecute(arg0 api.ApiDeleteUsersIDRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUsersIDExecute", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUsersIDExecute indicates an expected call of DeleteUsersIDExecute. +func (mr *MockUsersApiMockRecorder) DeleteUsersIDExecute(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUsersIDExecute", reflect.TypeOf((*MockUsersApi)(nil).DeleteUsersIDExecute), arg0) +} + +// GetUsersID mocks base method. +func (m *MockUsersApi) GetUsersID(arg0 context.Context, arg1 string) api.ApiGetUsersIDRequest { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersID", arg0, arg1) + ret0, _ := ret[0].(api.ApiGetUsersIDRequest) + return ret0 +} + +// GetUsersID indicates an expected call of GetUsersID. +func (mr *MockUsersApiMockRecorder) GetUsersID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersID", reflect.TypeOf((*MockUsersApi)(nil).GetUsersID), arg0, arg1) +} + +// GetUsersIDExecute mocks base method. +func (m *MockUsersApi) GetUsersIDExecute(arg0 api.ApiGetUsersIDRequest) (api.UserResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsersIDExecute", arg0) + ret0, _ := ret[0].(api.UserResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUsersIDExecute indicates an expected call of GetUsersIDExecute. +func (mr *MockUsersApiMockRecorder) GetUsersIDExecute(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersIDExecute", reflect.TypeOf((*MockUsersApi)(nil).GetUsersIDExecute), arg0) +} + +// PatchUsersID mocks base method. +func (m *MockUsersApi) PatchUsersID(arg0 context.Context, arg1 string) api.ApiPatchUsersIDRequest { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PatchUsersID", arg0, arg1) + ret0, _ := ret[0].(api.ApiPatchUsersIDRequest) + return ret0 +} + +// PatchUsersID indicates an expected call of PatchUsersID. +func (mr *MockUsersApiMockRecorder) PatchUsersID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchUsersID", reflect.TypeOf((*MockUsersApi)(nil).PatchUsersID), arg0, arg1) +} + +// PatchUsersIDExecute mocks base method. +func (m *MockUsersApi) PatchUsersIDExecute(arg0 api.ApiPatchUsersIDRequest) (api.UserResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PatchUsersIDExecute", arg0) + ret0, _ := ret[0].(api.UserResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PatchUsersIDExecute indicates an expected call of PatchUsersIDExecute. +func (mr *MockUsersApiMockRecorder) PatchUsersIDExecute(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchUsersIDExecute", reflect.TypeOf((*MockUsersApi)(nil).PatchUsersIDExecute), arg0) +} diff --git a/internal/mock/gen.go b/internal/mock/gen.go index 59e52df..c6bb02d 100644 --- a/internal/mock/gen.go +++ b/internal/mock/gen.go @@ -8,6 +8,7 @@ package mock //go:generate go run github.com/golang/mock/mockgen -package mock -destination api_setup.gen.go github.com/influxdata/influx-cli/v2/internal/api SetupApi //go:generate go run github.com/golang/mock/mockgen -package mock -destination api_write.gen.go github.com/influxdata/influx-cli/v2/internal/api WriteApi //go:generate go run github.com/golang/mock/mockgen -package mock -destination api_query.gen.go github.com/influxdata/influx-cli/v2/internal/api QueryApi +//go:generate go run github.com/golang/mock/mockgen -package mock -destination api_users.gen.go github.com/influxdata/influx-cli/v2/internal/api UsersApi // Other mocks //go:generate go run github.com/golang/mock/mockgen -package mock -destination config.gen.go -mock_names Service=MockConfigService github.com/influxdata/influx-cli/v2/internal/config Service