diff --git a/docs/azdo_help_reference.md b/docs/azdo_help_reference.md index f090a88e..476a0329 100644 --- a/docs/azdo_help_reference.md +++ b/docs/azdo_help_reference.md @@ -1037,6 +1037,20 @@ Aliases cr, c, new, n, add, a ``` +#### `azdo service-endpoint create github [ORGANIZATION/]PROJECT --name NAME [--url URL] [--token TOKEN] [flags]` + +Create a GitHub service endpoint + +``` + --configuration-id string Configuration for connecting to the endpoint (use an OAuth/installation configuration). Mutually exclusive with --token. +-q, --jq expression Filter JSON output using a jq expression + --json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it. + --name string Name of the service endpoint +-t, --template string Format JSON output using a Go template; see "azdo help formatting" + --token string Visit https://github.com/settings/tokens to create personal access tokens. Recommended scopes: repo, user, admin:repo_hook. If omitted, you will be prompted for a token when interactive. + --url string GitHub URL (defaults to https://github.com) +``` + ### `azdo service-endpoint delete [ORGANIZATION/]PROJECT/ID_OR_NAME [flags]` Delete a service endpoint from a project. diff --git a/docs/azdo_service-endpoint_create.md b/docs/azdo_service-endpoint_create.md index 5a70ee98..0c2bea0e 100644 --- a/docs/azdo_service-endpoint_create.md +++ b/docs/azdo_service-endpoint_create.md @@ -15,6 +15,7 @@ Check the available subcommands to create service connections of specific well-k ### Available commands * [azdo service-endpoint create azurerm](./azdo_service-endpoint_create_azurerm.md) +* [azdo service-endpoint create github](./azdo_service-endpoint_create_github.md) ### Options diff --git a/docs/azdo_service-endpoint_create_github.md b/docs/azdo_service-endpoint_create_github.md new file mode 100644 index 00000000..e5a67e53 --- /dev/null +++ b/docs/azdo_service-endpoint_create_github.md @@ -0,0 +1,58 @@ +## Command `azdo service-endpoint create github` + +``` +azdo service-endpoint create github [ORGANIZATION/]PROJECT --name NAME [--url URL] [--token TOKEN] [flags] +``` + +Create a GitHub service endpoint using a personal access token (PAT) or an installation/oauth configuration. + + +### Options + + +* `--configuration-id` `string` + + Configuration for connecting to the endpoint (use an OAuth/installation configuration). Mutually exclusive with --token. + +* `-q`, `--jq` `expression` + + Filter JSON output using a jq expression + +* `--json` `fields` + + Output JSON with the specified fields. Prefix a field with '-' to exclude it. + +* `--name` `string` + + Name of the service endpoint + +* `-t`, `--template` `string` + + Format JSON output using a Go template; see "azdo help formatting" + +* `--token` `string` + + Visit https://github.com/settings/tokens to create personal access tokens. Recommended scopes: repo, user, admin:repo_hook. If omitted, you will be prompted for a token when interactive. + +* `--url` `string` + + GitHub URL (defaults to https://github.com) + + +### JSON Fields + +`administratorsGroup`, `authorization`, `createdBy`, `data`, `description`, `groupScopeId`, `id`, `isReady`, `isShared`, `name`, `operationStatus`, `owner`, `readersGroup`, `serviceEndpointProjectReferences`, `type`, `url` + +### Examples + +```bash +# Create a GitHub service endpoint with a personal access token (PAT) +azdo service-endpoint create github my-org/my-project --name "gh-ep" --token + +# Create a GitHub service endpoint with an installation / OAuth configuration id +azdo service-endpoint create github my-org/my-project --name "gh-ep" --configuration-id +``` + +### See also + +* [azdo service-endpoint create](./azdo_service-endpoint_create.md) diff --git a/internal/cmd/serviceendpoint/create/azurerm/create_acc_test.go b/internal/cmd/serviceendpoint/create/azurerm/create_acc_test.go index d4599c07..556402d6 100644 --- a/internal/cmd/serviceendpoint/create/azurerm/create_acc_test.go +++ b/internal/cmd/serviceendpoint/create/azurerm/create_acc_test.go @@ -16,6 +16,13 @@ import ( "github.com/tmeckel/azdo-cli/internal/types" ) +type contextKey string + +const ( + ctxKeyCreateOpts contextKey = "azurerm/create-opts" + ctxKeyCertPath contextKey = "azurerm/cert-path" +) + const ( testCertificatePEM = `-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV diff --git a/internal/cmd/serviceendpoint/create/azurerm/test_keys.go b/internal/cmd/serviceendpoint/create/azurerm/test_keys.go deleted file mode 100644 index ed85f21b..00000000 --- a/internal/cmd/serviceendpoint/create/azurerm/test_keys.go +++ /dev/null @@ -1,8 +0,0 @@ -package azurerm - -type contextKey string - -const ( - ctxKeyCreateOpts contextKey = "azurerm/create-opts" - ctxKeyCertPath contextKey = "azurerm/cert-path" -) diff --git a/internal/cmd/serviceendpoint/create/create.go b/internal/cmd/serviceendpoint/create/create.go index 769904c9..2cc6e61a 100644 --- a/internal/cmd/serviceendpoint/create/create.go +++ b/internal/cmd/serviceendpoint/create/create.go @@ -18,6 +18,7 @@ import ( "golang.org/x/text/encoding/unicode" "github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/create/azurerm" + "github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/create/github" "github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/shared" "github.com/tmeckel/azdo-cli/internal/cmd/util" "github.com/tmeckel/azdo-cli/internal/types" @@ -98,6 +99,8 @@ func NewCmd(ctx util.CmdContext) *cobra.Command { cmd.AddCommand(azurerm.NewCmd(ctx)) + cmd.AddCommand(github.NewCmd(ctx)) + return cmd } diff --git a/internal/cmd/serviceendpoint/create/github/create.go b/internal/cmd/serviceendpoint/create/github/create.go new file mode 100644 index 00000000..6412cf61 --- /dev/null +++ b/internal/cmd/serviceendpoint/create/github/create.go @@ -0,0 +1,208 @@ +package github + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/google/uuid" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint" + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/shared" + "github.com/tmeckel/azdo-cli/internal/cmd/util" + "github.com/tmeckel/azdo-cli/internal/types" +) + +type createOptions struct { + project string + + name string + url string + token string + configurationID string + + exporter util.Exporter +} + +func NewCmd(ctx util.CmdContext) *cobra.Command { + opts := &createOptions{} + + cmd := &cobra.Command{ + Use: "github [ORGANIZATION/]PROJECT --name NAME [--url URL] [--token TOKEN]", + Short: "Create a GitHub service endpoint", + Long: heredoc.Doc(` + Create a GitHub service endpoint using a personal access token (PAT) or an installation/oauth configuration. + `), + Example: heredoc.Doc(` + # Create a GitHub service endpoint with a personal access token (PAT) + azdo service-endpoint create github my-org/my-project --name "gh-ep" --token + + # Create a GitHub service endpoint with an installation / OAuth configuration id + azdo service-endpoint create github my-org/my-project --name "gh-ep" --configuration-id + `), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.project = args[0] + return runCreate(ctx, opts) + }, + } + + cmd.Flags().StringVar(&opts.name, "name", "", "Name of the service endpoint") + cmd.Flags().StringVar(&opts.url, "url", "", "GitHub URL (defaults to https://github.com)") + // Help text taken from service-endpoint-types.json (inputDescriptors.AccessToken.description) + cmd.Flags().StringVar(&opts.token, "token", "", "Visit https://github.com/settings/tokens to create personal access tokens. Recommended scopes: repo, user, admin:repo_hook. If omitted, you will be prompted for a token when interactive.") + // Support installation/oauth configuration via ConfigurationId (InstallationToken scheme) + // Help text taken from service-endpoint-types.json (inputDescriptors.ConfigurationId.description) + cmd.Flags().StringVar(&opts.configurationID, "configuration-id", "", "Configuration for connecting to the endpoint (use an OAuth/installation configuration). Mutually exclusive with --token.") + + _ = cmd.MarkFlagRequired("name") + + util.AddJSONFlags(cmd, &opts.exporter, []string{ + "administratorsGroup", + "authorization", + "createdBy", + "data", + "description", + "groupScopeId", + "id", + "isReady", + "isShared", + "name", + "operationStatus", + "owner", + "readersGroup", + "serviceEndpointProjectReferences", + "type", + "url", + }) + + return cmd +} + +func runCreate(ctx util.CmdContext, opts *createOptions) error { + ios, err := ctx.IOStreams() + if err != nil { + return err + } + + p, err := ctx.Prompter() + if err != nil { + return err + } + + scope, err := util.ParseProjectScope(ctx, opts.project) + if err != nil { + return util.FlagErrorWrap(err) + } + + // default URL + if opts.url == "" { + opts.url = "https://github.com" + } + + // authentication selection: token (PAT) or configuration-id (InstallationToken) + if opts.token != "" && opts.configurationID != "" { + return fmt.Errorf("--token and --configuration-id are mutually exclusive") + } + if opts.token == "" && opts.configurationID == "" { + // default to prompting for token when interactive + if !ios.CanPrompt() { + return fmt.Errorf("no authentication provided: pass --token or --configuration-id (and enable prompting to provide token interactively)") + } + secret, err := p.Password("GitHub token:") + if err != nil { + return fmt.Errorf("prompt for token failed: %w", err) + } + opts.token = secret + } + + projectRef, err := shared.ResolveProjectReference(ctx, scope) + if err != nil { + return util.FlagErrorWrap(err) + } + + endpointType := "github" + owner := "library" + + var scheme string + var authParams map[string]string + if opts.configurationID != "" { + // InstallationToken scheme expects ConfigurationId parameter + scheme = "InstallationToken" + authParams = map[string]string{ + "ConfigurationId": opts.configurationID, + } + } else { + // default to PAT token + scheme = "Token" + authParams = map[string]string{ + "AccessToken": opts.token, + } + } + + endpoint := &serviceendpoint.ServiceEndpoint{ + Name: &opts.name, + Type: &endpointType, + Url: &opts.url, + Owner: &owner, + Authorization: &serviceendpoint.EndpointAuthorization{ + Scheme: &scheme, + Parameters: &authParams, + }, + ServiceEndpointProjectReferences: &[]serviceendpoint.ServiceEndpointProjectReference{ + { + ProjectReference: projectRef, + Name: &opts.name, + Description: types.ToPtr(""), + }, + }, + } + + ios.StartProgressIndicator() + defer ios.StopProgressIndicator() + + client, err := ctx.ClientFactory().ServiceEndpoint(ctx.Context(), scope.Organization) + if err != nil { + return fmt.Errorf("failed to create service endpoint client: %w", err) + } + + createdEndpoint, err := client.CreateServiceEndpoint(ctx.Context(), serviceendpoint.CreateServiceEndpointArgs{ + Endpoint: endpoint, + }) + if err != nil { + return fmt.Errorf("failed to create service endpoint: %w", err) + } + + zap.L().Debug("github service endpoint created", + zap.String("id", types.GetValue(createdEndpoint.Id, uuid.Nil).String()), + zap.String("name", types.GetValue(createdEndpoint.Name, "")), + ) + + ios.StopProgressIndicator() + + if opts.exporter != nil { + // redact authorization.parameters before emitting JSON to avoid leaking secrets + if createdEndpoint.Authorization != nil && createdEndpoint.Authorization.Parameters != nil { + for k := range *createdEndpoint.Authorization.Parameters { + (*createdEndpoint.Authorization.Parameters)[k] = "REDACTED" + } + } + return opts.exporter.Write(ios, createdEndpoint) + } + + tp, err := ctx.Printer("list") + if err != nil { + return err + } + tp.AddColumns("ID", "Name", "Type", "URL") + tp.EndRow() + tp.AddField(types.GetValue(createdEndpoint.Id, uuid.Nil).String()) + tp.AddField(types.GetValue(createdEndpoint.Name, "")) + tp.AddField(types.GetValue(createdEndpoint.Type, "")) + tp.AddField(types.GetValue(createdEndpoint.Url, "")) + tp.EndRow() + tp.Render() + + return nil +} diff --git a/internal/cmd/serviceendpoint/create/github/create_acc_test.go b/internal/cmd/serviceendpoint/create/github/create_acc_test.go new file mode 100644 index 00000000..e863a21b --- /dev/null +++ b/internal/cmd/serviceendpoint/create/github/create_acc_test.go @@ -0,0 +1,218 @@ +package github + +import ( + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint" + + "github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/test" + inttest "github.com/tmeckel/azdo-cli/internal/test" + "github.com/tmeckel/azdo-cli/internal/types" +) + +type contextKey string + +const ( + ctxKeyCreateOpts contextKey = "github/create-opts" +) + +func TestAccCreateGitHubServiceEndpoint(t *testing.T) { + sharedProj := test.NewSharedProject(fmt.Sprintf("azdo-cli-acc-%s", uuid.New().String())) + + t.Cleanup(func() { + _ = sharedProj.Cleanup() + }) + + t.Run("PersonalAccessToken", func(t *testing.T) { + t.Parallel() + + endpointName := fmt.Sprintf("azdo-cli-acc-gh-%s", uuid.New().String()) + + inttest.Test(t, inttest.TestCase{ + Steps: []inttest.Step{ + { + PreRun: func(ctx inttest.TestContext) error { + return sharedProj.Ensure(ctx) + }, + Run: func(ctx inttest.TestContext) error { + projectName, err := test.GetTestProjectName(ctx) + if err != nil { + return err + } + projectArg := fmt.Sprintf("%s/%s", ctx.Org(), projectName) + + opts := &createOptions{ + project: projectArg, + name: endpointName, + url: "https://github.com", + token: uuid.New().String(), + } + + ctx.SetValue(ctxKeyCreateOpts, opts) + return runCreate(ctx, opts) + }, + Verify: func(ctx inttest.TestContext) error { + client, err := ctx.ClientFactory().ServiceEndpoint(ctx.Context(), ctx.Org()) + if err != nil { + return err + } + + return inttest.Poll(func() error { + projectName, err := test.GetTestProjectName(ctx) + if err != nil { + return err + } + + endpoints, err := client.GetServiceEndpoints(ctx.Context(), serviceendpoint.GetServiceEndpointsArgs{ + Project: &projectName, + Type: types.ToPtr("github"), + IncludeDetails: types.ToPtr(true), + }) + if err != nil { + return err + } + + var found *serviceendpoint.ServiceEndpoint + for _, ep := range *endpoints { + if ep.Name != nil && *ep.Name == endpointName { + found = &ep + break + } + } + if found == nil { + return fmt.Errorf("service endpoint '%s' not found", endpointName) + } + + if found.Id != nil { + ctx.SetValue(test.CtxKeyEndpointID, found.Id.String()) + } + if refs := found.ServiceEndpointProjectReferences; refs != nil { + for _, r := range *refs { + if r.ProjectReference != nil && r.ProjectReference.Id != nil { + ctx.SetValue(test.CtxKeyProjectID, r.ProjectReference.Id.String()) + break + } + } + } + + if found.Type == nil || *found.Type != "github" { + return fmt.Errorf("expected endpoint type 'github', got '%s'", types.GetValue(found.Type, "")) + } + + // Authorization may be present; ensure scheme is set + if found.Authorization == nil || found.Authorization.Scheme == nil { + return fmt.Errorf("endpoint authorization scheme is nil") + } + + return nil + }, inttest.PollOptions{ + Tries: 10, + Timeout: 30 * time.Second, + }) + }, + PostRun: func(ctx inttest.TestContext) error { + return test.CleanupEndpointFromContext(ctx, test.CtxKeyEndpointID, test.CtxKeyProjectID) + }, + }, + }, + }) + }) + + t.Run("ConfigurationID", func(t *testing.T) { + t.Parallel() + + endpointName := fmt.Sprintf("azdo-cli-acc-gh-cfg-%s", uuid.New().String()) + + inttest.Test(t, inttest.TestCase{ + Steps: []inttest.Step{ + { + PreRun: func(ctx inttest.TestContext) error { + return sharedProj.Ensure(ctx) + }, + Run: func(ctx inttest.TestContext) error { + projectName, err := test.GetTestProjectName(ctx) + if err != nil { + return err + } + projectArg := fmt.Sprintf("%s/%s", ctx.Org(), projectName) + + opts := &createOptions{ + project: projectArg, + name: endpointName, + url: "https://github.com", + configurationID: uuid.New().String(), + } + + ctx.SetValue(ctxKeyCreateOpts, opts) + return runCreate(ctx, opts) + }, + Verify: func(ctx inttest.TestContext) error { + client, err := ctx.ClientFactory().ServiceEndpoint(ctx.Context(), ctx.Org()) + if err != nil { + return err + } + + return inttest.Poll(func() error { + projectName, err := test.GetTestProjectName(ctx) + if err != nil { + return err + } + + endpoints, err := client.GetServiceEndpoints(ctx.Context(), serviceendpoint.GetServiceEndpointsArgs{ + Project: &projectName, + Type: types.ToPtr("github"), + IncludeDetails: types.ToPtr(true), + }) + if err != nil { + return err + } + + var found *serviceendpoint.ServiceEndpoint + for _, ep := range *endpoints { + if ep.Name != nil && *ep.Name == endpointName { + found = &ep + break + } + } + if found == nil { + return fmt.Errorf("service endpoint '%s' not found", endpointName) + } + + if found.Id != nil { + ctx.SetValue(test.CtxKeyEndpointID, found.Id.String()) + } + if refs := found.ServiceEndpointProjectReferences; refs != nil { + for _, r := range *refs { + if r.ProjectReference != nil && r.ProjectReference.Id != nil { + ctx.SetValue(test.CtxKeyProjectID, r.ProjectReference.Id.String()) + break + } + } + } + + if found.Type == nil || *found.Type != "github" { + return fmt.Errorf("expected endpoint type 'github', got '%s'", types.GetValue(found.Type, "")) + } + + // Authorization may be present; ensure scheme is set + if found.Authorization == nil || found.Authorization.Scheme == nil { + return fmt.Errorf("endpoint authorization scheme is nil") + } + + return nil + }, inttest.PollOptions{ + Tries: 10, + Timeout: 30 * time.Second, + }) + }, + PostRun: func(ctx inttest.TestContext) error { + return test.CleanupEndpointFromContext(ctx, test.CtxKeyEndpointID, test.CtxKeyProjectID) + }, + }, + }, + }) + }) +} diff --git a/internal/cmd/serviceendpoint/create/github/create_test.go b/internal/cmd/serviceendpoint/create/github/create_test.go new file mode 100644 index 00000000..521edf13 --- /dev/null +++ b/internal/cmd/serviceendpoint/create/github/create_test.go @@ -0,0 +1,195 @@ +package github + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/core" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint" + "github.com/tmeckel/azdo-cli/internal/iostreams" + "github.com/tmeckel/azdo-cli/internal/mocks" + "github.com/tmeckel/azdo-cli/internal/types" +) + +func TestRunCreate_WithTokenFlag(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mCmdCtx := mocks.NewMockCmdContext(ctrl) + mCmdCtx.EXPECT().Context().Return(context.Background()).AnyTimes() + ios, _, _, _ := iostreams.Test() + mCmdCtx.EXPECT().IOStreams().Return(ios, nil).AnyTimes() + + mClientFactory := mocks.NewMockClientFactory(ctrl) + mCmdCtx.EXPECT().ClientFactory().Return(mClientFactory).AnyTimes() + + // Core client is used by ResolveProjectReference to fetch project metadata + mockCore := mocks.NewMockCoreClient(ctrl) + mClientFactory.EXPECT().Core(gomock.Any(), "org1").Return(mockCore, nil).AnyTimes() + mockCore.EXPECT().GetProject(gomock.Any(), gomock.Any()).Return(&core.TeamProject{Id: types.ToPtr(uuid.New())}, nil).AnyTimes() + + mockSEClient := mocks.NewMockServiceEndpointClient(ctrl) + // The connection factory mock exposes ServiceEndpoint via ClientFactory mock + mClientFactory.EXPECT().ServiceEndpoint(gomock.Any(), "org1").Return(mockSEClient, nil).AnyTimes() + + // Printer mock for table output + mPrinter := mocks.NewMockPrinter(ctrl) + mCmdCtx.EXPECT().Printer(gomock.Any()).Return(mPrinter, nil).AnyTimes() + mPrinter.EXPECT().AddColumns(gomock.Any()).AnyTimes() + mPrinter.EXPECT().AddField(gomock.Any()).AnyTimes() + mPrinter.EXPECT().EndRow().AnyTimes() + mPrinter.EXPECT().Render().AnyTimes() + + // Expect CreateServiceEndpoint to be called and return a created endpoint + created := &serviceendpoint.ServiceEndpoint{ + Id: types.ToPtr(uuid.New()), + Name: types.ToPtr("ep-name"), + Type: types.ToPtr("github"), + Url: types.ToPtr("https://github.com"), + } + mockSEClient.EXPECT().CreateServiceEndpoint(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args serviceendpoint.CreateServiceEndpointArgs) (*serviceendpoint.ServiceEndpoint, error) { + // Validate basic shape + if args.Endpoint == nil { + t.Fatalf("expected endpoint payload") + } + return created, nil + }, + ).Times(1) + + mCmdCtx.EXPECT().Prompter().Return(nil, nil).AnyTimes() + + opts := &createOptions{ + project: "org1/proj1", + name: "ep-name", + url: "", + token: "tok-flag", + } + + // run + err := runCreate(mCmdCtx, opts) + assert.NoError(t, err) +} + +func TestRunCreate_PromptForToken(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mCmdCtx := mocks.NewMockCmdContext(ctrl) + mCmdCtx.EXPECT().Context().Return(context.Background()).AnyTimes() + ios, _, _, _ := iostreams.Test() + // enable prompting + ios.SetStdinTTY(true) + ios.SetStdoutTTY(true) + mCmdCtx.EXPECT().IOStreams().Return(ios, nil).AnyTimes() + + mClientFactory := mocks.NewMockClientFactory(ctrl) + mCmdCtx.EXPECT().ClientFactory().Return(mClientFactory).AnyTimes() + + mockCore := mocks.NewMockCoreClient(ctrl) + mClientFactory.EXPECT().Core(gomock.Any(), "org1").Return(mockCore, nil).AnyTimes() + mockCore.EXPECT().GetProject(gomock.Any(), gomock.Any()).Return(&core.TeamProject{Id: types.ToPtr(uuid.New())}, nil).AnyTimes() + + mockSEClient := mocks.NewMockServiceEndpointClient(ctrl) + mClientFactory.EXPECT().ServiceEndpoint(gomock.Any(), "org1").Return(mockSEClient, nil).AnyTimes() + + // Printer mock for table output + mPrinter := mocks.NewMockPrinter(ctrl) + mCmdCtx.EXPECT().Printer(gomock.Any()).Return(mPrinter, nil).AnyTimes() + mPrinter.EXPECT().AddColumns(gomock.Any()).AnyTimes() + mPrinter.EXPECT().AddField(gomock.Any()).AnyTimes() + mPrinter.EXPECT().EndRow().AnyTimes() + mPrinter.EXPECT().Render().AnyTimes() + + created := &serviceendpoint.ServiceEndpoint{ + Id: types.ToPtr(uuid.New()), + Name: types.ToPtr("ep-name"), + Type: types.ToPtr("github"), + Url: types.ToPtr("https://github.com"), + } + mockSEClient.EXPECT().CreateServiceEndpoint(gomock.Any(), gomock.Any()).Return(created, nil).Times(1) + + // prompter mock: return a password + prom := mocks.NewMockPrompter(ctrl) + prom.EXPECT().Password(gomock.Any()).Return("sometoken", nil).Times(1) + mCmdCtx.EXPECT().Prompter().Return(prom, nil).AnyTimes() + + opts := &createOptions{ + project: "org1/proj1", + name: "ep-name", + url: "", + token: "", + } + + err := runCreate(mCmdCtx, opts) + assert.NoError(t, err) +} + +func TestRunCreate_WithConfigurationID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mCmdCtx := mocks.NewMockCmdContext(ctrl) + mCmdCtx.EXPECT().Context().Return(context.Background()).AnyTimes() + ios, _, _, _ := iostreams.Test() + mCmdCtx.EXPECT().IOStreams().Return(ios, nil).AnyTimes() + + mClientFactory := mocks.NewMockClientFactory(ctrl) + mCmdCtx.EXPECT().ClientFactory().Return(mClientFactory).AnyTimes() + + mockCore := mocks.NewMockCoreClient(ctrl) + mClientFactory.EXPECT().Core(gomock.Any(), "org1").Return(mockCore, nil).AnyTimes() + mockCore.EXPECT().GetProject(gomock.Any(), gomock.Any()).Return(&core.TeamProject{Id: types.ToPtr(uuid.New())}, nil).AnyTimes() + + mockSEClient := mocks.NewMockServiceEndpointClient(ctrl) + mClientFactory.EXPECT().ServiceEndpoint(gomock.Any(), "org1").Return(mockSEClient, nil).AnyTimes() + + // Printer mock for table output + mPrinter := mocks.NewMockPrinter(ctrl) + mCmdCtx.EXPECT().Printer(gomock.Any()).Return(mPrinter, nil).AnyTimes() + mPrinter.EXPECT().AddColumns(gomock.Any()).AnyTimes() + mPrinter.EXPECT().AddField(gomock.Any()).AnyTimes() + mPrinter.EXPECT().EndRow().AnyTimes() + mPrinter.EXPECT().Render().AnyTimes() + + // Expect CreateServiceEndpoint and validate Authorization scheme/params + mockSEClient.EXPECT().CreateServiceEndpoint(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args serviceendpoint.CreateServiceEndpointArgs) (*serviceendpoint.ServiceEndpoint, error) { + if args.Endpoint == nil || args.Endpoint.Authorization == nil { + t.Fatalf("expected endpoint with authorization") + } + if types.GetValue(args.Endpoint.Authorization.Scheme, "") != "InstallationToken" { + t.Fatalf("expected InstallationToken scheme, got %v", types.GetValue(args.Endpoint.Authorization.Scheme, "")) + } + if args.Endpoint.Authorization.Parameters == nil { + t.Fatalf("expected parameters map") + } + if val, ok := (*args.Endpoint.Authorization.Parameters)["ConfigurationId"]; !ok || val != "cfg-123" { + t.Fatalf("expected ConfigurationId=cfg-123, got %v", (*args.Endpoint.Authorization.Parameters)["ConfigurationId"]) + } + created := &serviceendpoint.ServiceEndpoint{ + Id: types.ToPtr(uuid.New()), + Name: types.ToPtr("ep-name"), + Type: types.ToPtr("github"), + Url: types.ToPtr("https://github.com"), + } + return created, nil + }, + ).Times(1) + + mCmdCtx.EXPECT().Prompter().Return(nil, nil).AnyTimes() + + opts := &createOptions{ + project: "org1/proj1", + name: "ep-name", + url: "", + configurationID: "cfg-123", + } + + err := runCreate(mCmdCtx, opts) + assert.NoError(t, err) +} diff --git a/internal/mocks/serviceendpoint_client_mock.go b/internal/mocks/serviceendpoint_client_mock.go new file mode 100644 index 00000000..7505c726 --- /dev/null +++ b/internal/mocks/serviceendpoint_client_mock.go @@ -0,0 +1,220 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint (interfaces: Client) +// +// Generated by this command: +// +// mockgen -package=mocks -destination internal/mocks/serviceendpoint_client_mock.go -mock_names Client=MockServiceEndpointClient github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint Client +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + serviceendpoint "github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint" + gomock "go.uber.org/mock/gomock" +) + +// MockServiceEndpointClient is a mock of Client interface. +type MockServiceEndpointClient struct { + ctrl *gomock.Controller + recorder *MockServiceEndpointClientMockRecorder + isgomock struct{} +} + +// MockServiceEndpointClientMockRecorder is the mock recorder for MockServiceEndpointClient. +type MockServiceEndpointClientMockRecorder struct { + mock *MockServiceEndpointClient +} + +// NewMockServiceEndpointClient creates a new mock instance. +func NewMockServiceEndpointClient(ctrl *gomock.Controller) *MockServiceEndpointClient { + mock := &MockServiceEndpointClient{ctrl: ctrl} + mock.recorder = &MockServiceEndpointClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockServiceEndpointClient) EXPECT() *MockServiceEndpointClientMockRecorder { + return m.recorder +} + +// CreateServiceEndpoint mocks base method. +func (m *MockServiceEndpointClient) CreateServiceEndpoint(arg0 context.Context, arg1 serviceendpoint.CreateServiceEndpointArgs) (*serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateServiceEndpoint", arg0, arg1) + ret0, _ := ret[0].(*serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateServiceEndpoint indicates an expected call of CreateServiceEndpoint. +func (mr *MockServiceEndpointClientMockRecorder) CreateServiceEndpoint(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServiceEndpoint", reflect.TypeOf((*MockServiceEndpointClient)(nil).CreateServiceEndpoint), arg0, arg1) +} + +// DeleteServiceEndpoint mocks base method. +func (m *MockServiceEndpointClient) DeleteServiceEndpoint(arg0 context.Context, arg1 serviceendpoint.DeleteServiceEndpointArgs) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteServiceEndpoint", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteServiceEndpoint indicates an expected call of DeleteServiceEndpoint. +func (mr *MockServiceEndpointClientMockRecorder) DeleteServiceEndpoint(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServiceEndpoint", reflect.TypeOf((*MockServiceEndpointClient)(nil).DeleteServiceEndpoint), arg0, arg1) +} + +// ExecuteServiceEndpointRequest mocks base method. +func (m *MockServiceEndpointClient) ExecuteServiceEndpointRequest(arg0 context.Context, arg1 serviceendpoint.ExecuteServiceEndpointRequestArgs) (*serviceendpoint.ServiceEndpointRequestResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteServiceEndpointRequest", arg0, arg1) + ret0, _ := ret[0].(*serviceendpoint.ServiceEndpointRequestResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteServiceEndpointRequest indicates an expected call of ExecuteServiceEndpointRequest. +func (mr *MockServiceEndpointClientMockRecorder) ExecuteServiceEndpointRequest(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteServiceEndpointRequest", reflect.TypeOf((*MockServiceEndpointClient)(nil).ExecuteServiceEndpointRequest), arg0, arg1) +} + +// GetServiceEndpointDetails mocks base method. +func (m *MockServiceEndpointClient) GetServiceEndpointDetails(arg0 context.Context, arg1 serviceendpoint.GetServiceEndpointDetailsArgs) (*serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceEndpointDetails", arg0, arg1) + ret0, _ := ret[0].(*serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceEndpointDetails indicates an expected call of GetServiceEndpointDetails. +func (mr *MockServiceEndpointClientMockRecorder) GetServiceEndpointDetails(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceEndpointDetails", reflect.TypeOf((*MockServiceEndpointClient)(nil).GetServiceEndpointDetails), arg0, arg1) +} + +// GetServiceEndpointExecutionRecords mocks base method. +func (m *MockServiceEndpointClient) GetServiceEndpointExecutionRecords(arg0 context.Context, arg1 serviceendpoint.GetServiceEndpointExecutionRecordsArgs) (*serviceendpoint.GetServiceEndpointExecutionRecordsResponseValue, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceEndpointExecutionRecords", arg0, arg1) + ret0, _ := ret[0].(*serviceendpoint.GetServiceEndpointExecutionRecordsResponseValue) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceEndpointExecutionRecords indicates an expected call of GetServiceEndpointExecutionRecords. +func (mr *MockServiceEndpointClientMockRecorder) GetServiceEndpointExecutionRecords(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceEndpointExecutionRecords", reflect.TypeOf((*MockServiceEndpointClient)(nil).GetServiceEndpointExecutionRecords), arg0, arg1) +} + +// GetServiceEndpointTypes mocks base method. +func (m *MockServiceEndpointClient) GetServiceEndpointTypes(arg0 context.Context, arg1 serviceendpoint.GetServiceEndpointTypesArgs) (*[]serviceendpoint.ServiceEndpointType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceEndpointTypes", arg0, arg1) + ret0, _ := ret[0].(*[]serviceendpoint.ServiceEndpointType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceEndpointTypes indicates an expected call of GetServiceEndpointTypes. +func (mr *MockServiceEndpointClientMockRecorder) GetServiceEndpointTypes(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceEndpointTypes", reflect.TypeOf((*MockServiceEndpointClient)(nil).GetServiceEndpointTypes), arg0, arg1) +} + +// GetServiceEndpoints mocks base method. +func (m *MockServiceEndpointClient) GetServiceEndpoints(arg0 context.Context, arg1 serviceendpoint.GetServiceEndpointsArgs) (*[]serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceEndpoints", arg0, arg1) + ret0, _ := ret[0].(*[]serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceEndpoints indicates an expected call of GetServiceEndpoints. +func (mr *MockServiceEndpointClientMockRecorder) GetServiceEndpoints(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceEndpoints", reflect.TypeOf((*MockServiceEndpointClient)(nil).GetServiceEndpoints), arg0, arg1) +} + +// GetServiceEndpointsByNames mocks base method. +func (m *MockServiceEndpointClient) GetServiceEndpointsByNames(arg0 context.Context, arg1 serviceendpoint.GetServiceEndpointsByNamesArgs) (*[]serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceEndpointsByNames", arg0, arg1) + ret0, _ := ret[0].(*[]serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceEndpointsByNames indicates an expected call of GetServiceEndpointsByNames. +func (mr *MockServiceEndpointClientMockRecorder) GetServiceEndpointsByNames(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceEndpointsByNames", reflect.TypeOf((*MockServiceEndpointClient)(nil).GetServiceEndpointsByNames), arg0, arg1) +} + +// GetServiceEndpointsWithRefreshedAuthentication mocks base method. +func (m *MockServiceEndpointClient) GetServiceEndpointsWithRefreshedAuthentication(arg0 context.Context, arg1 serviceendpoint.GetServiceEndpointsWithRefreshedAuthenticationArgs) (*[]serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceEndpointsWithRefreshedAuthentication", arg0, arg1) + ret0, _ := ret[0].(*[]serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceEndpointsWithRefreshedAuthentication indicates an expected call of GetServiceEndpointsWithRefreshedAuthentication. +func (mr *MockServiceEndpointClientMockRecorder) GetServiceEndpointsWithRefreshedAuthentication(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceEndpointsWithRefreshedAuthentication", reflect.TypeOf((*MockServiceEndpointClient)(nil).GetServiceEndpointsWithRefreshedAuthentication), arg0, arg1) +} + +// ShareServiceEndpoint mocks base method. +func (m *MockServiceEndpointClient) ShareServiceEndpoint(arg0 context.Context, arg1 serviceendpoint.ShareServiceEndpointArgs) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ShareServiceEndpoint", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ShareServiceEndpoint indicates an expected call of ShareServiceEndpoint. +func (mr *MockServiceEndpointClientMockRecorder) ShareServiceEndpoint(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShareServiceEndpoint", reflect.TypeOf((*MockServiceEndpointClient)(nil).ShareServiceEndpoint), arg0, arg1) +} + +// UpdateServiceEndpoint mocks base method. +func (m *MockServiceEndpointClient) UpdateServiceEndpoint(arg0 context.Context, arg1 serviceendpoint.UpdateServiceEndpointArgs) (*serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateServiceEndpoint", arg0, arg1) + ret0, _ := ret[0].(*serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateServiceEndpoint indicates an expected call of UpdateServiceEndpoint. +func (mr *MockServiceEndpointClientMockRecorder) UpdateServiceEndpoint(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateServiceEndpoint", reflect.TypeOf((*MockServiceEndpointClient)(nil).UpdateServiceEndpoint), arg0, arg1) +} + +// UpdateServiceEndpoints mocks base method. +func (m *MockServiceEndpointClient) UpdateServiceEndpoints(arg0 context.Context, arg1 serviceendpoint.UpdateServiceEndpointsArgs) (*[]serviceendpoint.ServiceEndpoint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateServiceEndpoints", arg0, arg1) + ret0, _ := ret[0].(*[]serviceendpoint.ServiceEndpoint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateServiceEndpoints indicates an expected call of UpdateServiceEndpoints. +func (mr *MockServiceEndpointClientMockRecorder) UpdateServiceEndpoints(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateServiceEndpoints", reflect.TypeOf((*MockServiceEndpointClient)(nil).UpdateServiceEndpoints), arg0, arg1) +} diff --git a/scripts/generate_mocks.sh b/scripts/generate_mocks.sh index 7b4aad0e..d9ad7a0a 100644 --- a/scripts/generate_mocks.sh +++ b/scripts/generate_mocks.sh @@ -77,6 +77,12 @@ mockgen \ -mock_names Client=MockPipelinePermissionsClient \ github.com/microsoft/azure-devops-go-api/azuredevops/v7/pipelinepermissions Client +echo "Generating Azure DevOps ServiceEndpoint client mock..." +mockgen \ + -package=mocks -destination internal/mocks/serviceendpoint_client_mock.go \ + -mock_names Client=MockServiceEndpointClient \ + github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint Client + echo "Generating Repository mock..." mockgen -source internal/azdo/repo.go \ -package=mocks -destination internal/mocks/repository_mock.go