diff --git a/docs/cli/commands/tanzu.md b/docs/cli/commands/tanzu.md index e9dd6eebc..cf0f2a641 100644 --- a/docs/cli/commands/tanzu.md +++ b/docs/cli/commands/tanzu.md @@ -10,7 +10,7 @@ The Tanzu CLI ### SEE ALSO -* [tanzu api-token](tanzu_api-token.md) - API Token Generation for the Tanzu Platform +* [tanzu api-token](tanzu_api-token.md) - API Token Generation for Tanzu Platform * [tanzu completion](tanzu_completion.md) - Output shell completion code * [tanzu config](tanzu_config.md) - Configuration for the CLI * [tanzu context](tanzu_context.md) - Configure and manage contexts for the Tanzu CLI diff --git a/docs/cli/commands/tanzu_api-token.md b/docs/cli/commands/tanzu_api-token.md index 67e97b6fc..1aafe0460 100644 --- a/docs/cli/commands/tanzu_api-token.md +++ b/docs/cli/commands/tanzu_api-token.md @@ -1,6 +1,6 @@ ## tanzu api-token -API Token Generation for the Tanzu Platform +API Token Generation for Tanzu Platform ### Options diff --git a/docs/cli/commands/tanzu_api-token_create.md b/docs/cli/commands/tanzu_api-token_create.md index bef8645b8..106d8c175 100644 --- a/docs/cli/commands/tanzu_api-token_create.md +++ b/docs/cli/commands/tanzu_api-token_create.md @@ -14,5 +14,5 @@ tanzu api-token create [flags] ### SEE ALSO -* [tanzu api-token](tanzu_api-token.md) - API Token Generation for the Tanzu Platform +* [tanzu api-token](tanzu_api-token.md) - API Token Generation for Tanzu Platform diff --git a/go.mod b/go.mod index c07c4132e..33fc21a61 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/gobwas/glob v0.2.3 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/gnostic v0.6.9 + github.com/google/go-cmp v0.5.9 github.com/google/go-containerregistry v0.15.2 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 @@ -139,7 +140,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/certificate-transparency-go v1.1.6 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-github/v50 v50.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -196,6 +196,7 @@ require ( github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/thales-e-security/pool v0.0.2 // indirect diff --git a/go.sum b/go.sum index 47782b028..f434f0fb5 100644 --- a/go.sum +++ b/go.sum @@ -684,6 +684,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/pkg/auth/uaa/tanzu.go b/pkg/auth/uaa/tanzu.go index 1b0c9d207..76f367eaa 100644 --- a/pkg/auth/uaa/tanzu.go +++ b/pkg/auth/uaa/tanzu.go @@ -31,7 +31,7 @@ func getIssuerEndpoints(issuerURL string) common.IssuerEndPoints { } } -func TanzuLogin(issuerURL string, opts ...common.LoginOption) (*common.Token, error) { +var TanzuLogin = func(issuerURL string, opts ...common.LoginOption) (*common.Token, error) { issuerEndpoints := getIssuerEndpoints(issuerURL) h := common.NewTanzuLoginHandler(issuerURL, issuerEndpoints.AuthURL, issuerEndpoints.TokenURL, TanzuCLIClientID, tanzuCLIClientSecret, defaultListenAddress, defaultCallbackPath, config.UAAIdpType, nil, nil, term.IsTerminal) diff --git a/pkg/command/apitoken.go b/pkg/command/apitoken.go index 6010ce32e..1f6cbe31a 100644 --- a/pkg/command/apitoken.go +++ b/pkg/command/apitoken.go @@ -5,7 +5,6 @@ package command import ( "fmt" - "time" "github.com/fatih/color" "github.com/pkg/errors" @@ -24,7 +23,7 @@ import ( func newAPITokenCmd() *cobra.Command { apiTokenCmd := &cobra.Command{ Use: "api-token", - Short: "API Token Generation for the Tanzu Platform", + Short: "API Token Generation for Tanzu Platform", Aliases: []string{"apitoken"}, Annotations: map[string]string{ "group": string(plugin.SystemCmdGroup), @@ -53,14 +52,14 @@ func newAPITokenCreateCmd() *cobra.Command { func createAPIToken(cmd *cobra.Command, _ []string) (err error) { c, err := config.GetActiveContext(types.ContextTypeTanzu) if err != nil { - return errors.New("No active context of type `tanzu`. Please login to Tanzu Platform first to generate the API token") + return errors.New("No active context of type `tanzu`. Please login to Tanzu Platform first to generate an API token") } if c == nil || c.GlobalOpts == nil || c.GlobalOpts.Auth.Issuer == "" { - return errors.New("Invalid active context of type `tanzu`. Please login to Tanzu Platform first to generate the API token") + return errors.New("Invalid active context of type `tanzu`. Please login to Tanzu Platform first to generate an API token") } // Make sure it is of type tanzu with tanzuIdpType as `uaa` else return error if idpType, exist := c.AdditionalMetadata[config.TanzuIdpTypeKey]; !exist || idpType != "uaa" { - return errors.New("invalid IDP type for the active context. Only UAA IDP type is supported for generating API token") + return errors.New("invalid IDP type for the active context. Only UAA IDP type is supported for generating an API token") } var token *commonauth.Token @@ -80,9 +79,8 @@ func createAPIToken(cmd *cobra.Command, _ []string) (err error) { fmt.Fprint(cmd.OutOrStdout(), bold.Sprint("==\n\n")) fmt.Fprintf(cmd.OutOrStdout(), "%s Your generated API token is: %s\n\n", bold.Sprint("API Token Generation Successful!"), cyanBold.Sprint(token.RefreshToken)) - fmt.Fprintf(cmd.OutOrStdout(), "%s The token will expire on %s.\n", bold.Sprint("Token Expiration:"), bold.Sprint(time.Now().Local().Add(time.Second*time.Duration(token.ExpiresIn)))) - fmt.Fprint(cmd.OutOrStdout(), "Please copy and save your token securely. Note that you will need to regenerate a new token before expiration time and login again to continue using the CLI.\n\n") - fmt.Fprintf(cmd.OutOrStdout(), "For non-interactive login use the API token as follow: %s\n", cyanBold.Sprint("TANZU_API_TOKEN= tanzu login --endpoint ")) + fmt.Fprintf(cmd.OutOrStdout(), "For non-interactive login use the API token as follow: %s\n\n", cyanBold.Sprint("TANZU_API_TOKEN= tanzu login --endpoint ")) + fmt.Fprint(cmd.OutOrStdout(), "Please copy and save your token securely. Note that you will need to regenerate a new token before expiration time and login again to continue using the CLI.\n") return nil } diff --git a/pkg/command/apitoken_test.go b/pkg/command/apitoken_test.go new file mode 100644 index 000000000..6c82555b4 --- /dev/null +++ b/pkg/command/apitoken_test.go @@ -0,0 +1,195 @@ +// Copyright 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/otiai10/copy" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/vmware-tanzu/tanzu-plugin-runtime/config" + configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" + + "github.com/vmware-tanzu/tanzu-cli/pkg/auth/common" + "github.com/vmware-tanzu/tanzu-cli/pkg/auth/uaa" +) + +// mockTanzuLogin is a mock implementation of the TanzuLogin function +type mockTanzuLogin struct { + mock.Mock +} + +func (m *mockTanzuLogin) TanzuLogin(issuerURL string, opts ...common.LoginOption) (*common.Token, error) { + args := m.Called(issuerURL, opts) + return args.Get(0).(*common.Token), args.Error(1) +} + +func TestCreateAPIToken(t *testing.T) { + var configFile, configFileNG *os.File + var err error + + setupEnv := func() { + configFile, err = os.CreateTemp("", "config") + assert.NoError(t, err) + + err = copy.Copy(filepath.Join("..", "fakes", "config", "tanzu_config.yaml"), configFile.Name()) + assert.NoError(t, err) + + os.Setenv("TANZU_CONFIG", configFile.Name()) + + configFileNG, err = os.CreateTemp("", "config_ng") + assert.NoError(t, err) + + os.Setenv("TANZU_CONFIG_NEXT_GEN", configFileNG.Name()) + err = copy.Copy(filepath.Join("..", "fakes", "config", "tanzu_config_ng.yaml"), configFileNG.Name()) + assert.NoError(t, err) + } + + teardownEnv := func() { + os.Unsetenv("TANZU_CONFIG") + os.Unsetenv("TANZU_CONFIG_NEXT_GEN") + os.RemoveAll(configFile.Name()) + os.RemoveAll(configFileNG.Name()) + } + + tests := []struct { + name string + context *configtypes.Context + tanzuLoginErr error + wantErr bool + errMsg string + output string + }{ + { + name: "success", + context: &configtypes.Context{ + Name: "fakecontext", + ContextType: configtypes.ContextTypeTanzu, + GlobalOpts: &configtypes.GlobalServer{ + Auth: configtypes.GlobalServerAuth{ + Issuer: "https://example.com", + }, + }, + AdditionalMetadata: map[string]interface{}{ + config.TanzuIdpTypeKey: config.UAAIdpType, + }, + }, + tanzuLoginErr: nil, + wantErr: false, + output: `== + +API Token Generation Successful! Your generated API token is: refresh-token + +For non-interactive login use the API token as follow: TANZU_API_TOKEN= tanzu login --endpoint + +Please copy and save your token securely. Note that you will need to regenerate a new token before expiration time and login again to continue using the CLI. +`, + }, + { + name: "no active context", + context: nil, + tanzuLoginErr: nil, + wantErr: true, + errMsg: "No active context of type `tanzu`. Please login to Tanzu Platform first to generate an API token", + }, + { + name: "invalid active context", + context: &configtypes.Context{ + Name: "fakecontext", + ContextType: configtypes.ContextTypeTanzu, + AdditionalMetadata: map[string]interface{}{ + config.TanzuIdpTypeKey: config.UAAIdpType, + }, + }, + tanzuLoginErr: nil, + wantErr: true, + errMsg: "Invalid active context of type `tanzu`. Please login to Tanzu Platform first to generate an API token", + }, + { + name: "invalid IDP type", + context: &configtypes.Context{ + Name: "fakecontext", + ContextType: configtypes.ContextTypeTanzu, + GlobalOpts: &configtypes.GlobalServer{ + Auth: configtypes.GlobalServerAuth{ + Issuer: "https://example.com", + }, + }, + AdditionalMetadata: map[string]interface{}{ + config.TanzuIdpTypeKey: "other", + }, + }, + tanzuLoginErr: nil, + wantErr: true, + errMsg: "invalid IDP type for the active context. Only UAA IDP type is supported for generating an API token", + }, + { + name: "TanzuLogin error", + context: &configtypes.Context{ + Name: "fakecontext", + ContextType: configtypes.ContextTypeTanzu, + GlobalOpts: &configtypes.GlobalServer{ + Auth: configtypes.GlobalServerAuth{ + Issuer: "https://example.com", + }, + }, + AdditionalMetadata: map[string]interface{}{ + config.TanzuIdpTypeKey: config.UAAIdpType, + }, + }, + tanzuLoginErr: errors.New("TanzuLogin error"), + wantErr: true, + errMsg: "unable to login, TanzuLogin error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupEnv() + defer teardownEnv() + + cmd := &cobra.Command{} + outputBuf := &bytes.Buffer{} + cmd.SetOutput(outputBuf) + + originalTanzuLogin := uaa.TanzuLogin + defer func() { + uaa.TanzuLogin = originalTanzuLogin + }() + + mockTanzuLogin := &mockTanzuLogin{} + uaa.TanzuLogin = mockTanzuLogin.TanzuLogin + if tt.context != nil && tt.context.GlobalOpts != nil && tt.context.AdditionalMetadata[config.TanzuIdpTypeKey] == config.UAAIdpType { + mockTanzuLogin.On("TanzuLogin", tt.context.GlobalOpts.Auth.Issuer, mock.Anything). + Return(&common.Token{RefreshToken: "refresh-token", ExpiresIn: 3600}, tt.tanzuLoginErr) + } + + if tt.context != nil { + err = config.SetContext(tt.context, true) + assert.NoError(t, err) + } + + err := createAPIToken(cmd, nil) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if diff := cmp.Diff(tt.output, outputBuf.String()); diff != "" { + t.Errorf("Unexpected output (-expected, +actual): %s", diff) + } + } + + mockTanzuLogin.AssertExpectations(t) + }) + } +} diff --git a/pkg/utils/time/time.go b/pkg/utils/time/time.go new file mode 100644 index 000000000..4416d06d9 --- /dev/null +++ b/pkg/utils/time/time.go @@ -0,0 +1,9 @@ +// Copyright 2024 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package time contains time specific functions and variable to make it to overwrite for unit tests +package time + +import "time" + +var Now = time.Now