diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index 2da19635..95d35535 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -1,26 +1,18 @@ package delete import ( - "encoding/hex" - "errors" "fmt" "io" - "math/big" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -29,9 +21,6 @@ type Inputs struct { WorkflowName string `validate:"workflow_name"` WorkflowOwner string `validate:"workflow_owner"` SkipConfirmation bool - - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -72,13 +61,9 @@ type handler struct { credentials *credentials.Credentials environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { @@ -92,37 +77,44 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { - oc, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "delete") + resolvedWorkflowOwner, err := h.resolveWorkflowOwner() if err != nil { - return Inputs{}, err + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), - WorkflowRegistryContractChainName: oc.ChainName(), - WorkflowRegistryContractAddress: oc.Address(), + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, + SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), }, nil } +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner() (string, error) { + if h.runtimeContext.ResolvedRegistry.Type() != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil + } + + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") + } + + if len(owner) >= 2 && owner[:2] != "0x" { + owner = "0x" + owner + } + + return owner, nil +} + func (h *handler) ValidateInputs() error { validate, err := validation.NewValidator() if err != nil { @@ -138,42 +130,39 @@ func (h *handler) ValidateInputs() error { } func (h *handler) Execute() error { - workflowName := h.inputs.WorkflowName - workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + adapter, err := newRegistryDeleteStrategy(h.runtimeContext.ResolvedRegistry, h) + if err != nil { + return err + } h.displayWorkflowDetails() - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - allWorkflows, err := h.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) + workflows, err := adapter.FetchWorkflows() if err != nil { - return fmt.Errorf("failed to get workflow list: %w", err) + return err } - if len(allWorkflows) == 0 { - ui.Warning(fmt.Sprintf("No workflows found for name: %s", workflowName)) + + if len(workflows) == 0 { + ui.Warning(fmt.Sprintf("No workflows found for name: %s", h.inputs.WorkflowName)) return nil } // Note: The way deploy is set up, there will only ever be one workflow in the command for now - h.runtimeContext.Workflow.ID = hex.EncodeToString(allWorkflows[0].WorkflowId[:]) + h.runtimeContext.Workflow.ID = workflows[0].ID - ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(allWorkflows), workflowName)) - for i, wf := range allWorkflows { - status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] + ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(workflows), h.inputs.WorkflowName)) + for i, wf := range workflows { ui.Print(fmt.Sprintf(" %d. Workflow", i+1)) - ui.Dim(fmt.Sprintf(" ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner.Hex())) + ui.Dim(fmt.Sprintf(" ID: %s", wf.ID)) + ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner)) ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) - ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryUrl)) - ui.Dim(fmt.Sprintf(" Workflow Status: %s", status)) + ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryURL)) + ui.Dim(fmt.Sprintf(" Workflow Status: %s", wf.Status)) ui.Line() } - shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) + shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, h.inputs.WorkflowName) if err != nil { return err } @@ -182,84 +171,13 @@ func (h *handler) Execute() error { return nil } - ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(allWorkflows))) - var errs []error - for _, wf := range allWorkflows { - txOut, err := h.wrc.DeleteWorkflow(wf.WorkflowId) - if err != nil { - h.log.Error(). - Err(err). - Str("workflowId", hex.EncodeToString(wf.WorkflowId[:])). - Msg("Failed to delete workflow") - errs = append(errs, err) - continue - } - oc, _ := h.runtimeContext.ResolvedRegistry.(*settings.OnChainRegistry) - - switch txOut.Type { - case client.Regular: - ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) - ui.Success(fmt.Sprintf("Deleted workflow ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow deletion transaction prepared!") - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - DeleteWorkflow: &types.DeleteWorkflow{ - Payload: types.UserWorkflowDeleteInput{ - WorkflowID: h.runtimeContext.Workflow.ID, - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(workflows))) - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) - } - - // Workflow artifacts deletion will be handled by a background cleanup process. - } - if len(errs) > 0 { - return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) + err = adapter.DeleteWorkflows(workflows) + if err != nil { + return err } + ui.Success("Workflows deleted successfully") return nil } @@ -293,6 +211,6 @@ func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Deleting Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/delete/delete_test.go b/cmd/workflow/delete/delete_test.go index 55ea63f7..dbdc07ad 100644 --- a/cmd/workflow/delete/delete_test.go +++ b/cmd/workflow/delete/delete_test.go @@ -88,13 +88,6 @@ func TestWorkflowDeleteCommand(t *testing.T) { } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA - if tt.inputs.WorkflowRegistryContractAddress == "" { - tt.inputs.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if tt.inputs.WorkflowRegistryContractChainName == "" { - tt.inputs.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } - handler := newHandler(ctx, testutil.EmptyMockStdinReader()) handler.inputs = tt.inputs diff --git a/cmd/workflow/delete/registry_delete_strategy.go b/cmd/workflow/delete/registry_delete_strategy.go new file mode 100644 index 00000000..3e347565 --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy.go @@ -0,0 +1,29 @@ +package delete + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +type WorkflowToDelete struct { + ID string + Owner string + DonFamily string + Tag string + BinaryURL string + Status string + RawID any // Holds the registry-specific ID type ([32]byte for on-chain, string for private) +} + +// registryDeleteStrategy encapsulates target-specific delete logic. +type registryDeleteStrategy interface { + FetchWorkflows() ([]WorkflowToDelete, error) + DeleteWorkflows(workflows []WorkflowToDelete) error +} + +// newRegistryDeleteStrategy returns the appropriate strategy for the given target. +func newRegistryDeleteStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryDeleteStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryDeleteStrategy(h), nil + } + return newOnchainRegistryDeleteStrategy(h) +} diff --git a/cmd/workflow/delete/registry_delete_strategy_onchain.go b/cmd/workflow/delete/registry_delete_strategy_onchain.go new file mode 100644 index 00000000..1dd7335b --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy_onchain.go @@ -0,0 +1,165 @@ +package delete + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryDeleteStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryDeleteStrategy(h *handler) (*onchainRegistryDeleteStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "delete") + if err != nil { + return nil, err + } + + a := &onchainRegistryDeleteStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, error) { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return nil, a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + + allWorkflows, err := a.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) + if err != nil { + return nil, fmt.Errorf("failed to get workflow list: %w", err) + } + + var workflows []WorkflowToDelete + for _, wf := range allWorkflows { + status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] + workflows = append(workflows, WorkflowToDelete{ + ID: hex.EncodeToString(wf.WorkflowId[:]), + Owner: wf.Owner.Hex(), + DonFamily: wf.DonFamily, + Tag: wf.Tag, + BinaryURL: wf.BinaryUrl, + Status: status, + RawID: wf.WorkflowId, + }) + } + + return workflows, nil +} + +func (a *onchainRegistryDeleteStrategy) DeleteWorkflows(workflows []WorkflowToDelete) error { + h := a.h + var errs []error + for _, wf := range workflows { + workflowID := wf.RawID.([32]byte) + txOut, err := a.wrc.DeleteWorkflow(workflowID) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", wf.ID). + Msg("Failed to delete workflow") + errs = append(errs, err) + continue + } + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success("Transaction confirmed") + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", wf.ID)) + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow deletion transaction prepared!") + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + DeleteWorkflow: &types.DeleteWorkflow{ + Payload: types.UserWorkflowDeleteInput{ + WorkflowID: wf.ID, + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", h.inputs.WorkflowName, time.Now().Format("20060102_150405")) + } + + err = cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + if err != nil { + return err + } + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + + // Workflow artifacts deletion will be handled by a background cleanup process. + } + if len(errs) > 0 { + return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) + } + return nil +} diff --git a/cmd/workflow/delete/registry_delete_strategy_private.go b/cmd/workflow/delete/registry_delete_strategy_private.go new file mode 100644 index 00000000..2f81f125 --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy_private.go @@ -0,0 +1,71 @@ +package delete + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryDeleteStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryDeleteStrategy(h *handler) *privateRegistryDeleteStrategy { + return &privateRegistryDeleteStrategy{h: h} +} + +func (a *privateRegistryDeleteStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, error) { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to delete... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return nil, fmt.Errorf("failed to get workflow: %w", err) + } + + return []WorkflowToDelete{ + { + ID: workflow.WorkflowID, + Owner: workflow.Owner, + DonFamily: workflow.DonFamily, + Tag: workflow.Tag, + BinaryURL: workflow.BinaryURL, + Status: string(workflow.Status), + RawID: workflow.WorkflowID, + }, + }, nil +} + +func (a *privateRegistryDeleteStrategy) DeleteWorkflows(workflows []WorkflowToDelete) error { + h := a.h + + for _, wf := range workflows { + workflowID := wf.RawID.(string) + deletedID, err := a.prc.DeleteWorkflowInRegistry(workflowID) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", workflowID). + Msg("Failed to delete workflow") + return fmt.Errorf("failed to delete workflow in private registry: %w", err) + } + + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", deletedID)) + } + + return nil +} diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 014081a2..0b6c6f06 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -626,3 +626,162 @@ func RunWorkflowActivatePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { require.Contains(t, out, "Status: WORKFLOW_STATUS_ACTIVE", "expected active status in details.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) } + +// workflowDeletePrivateRegistry deletes a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowDeletePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var deleteWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "DeleteOffchainWorkflow") { + deleteWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "deleteOffchainWorkflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "delete", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := createTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow delete failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, deleteWorkflowCalled.Load(), "expected DeleteOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowDeletePrivateRegistryHappyPath runs the workflow delete happy path for private registry. +func RunWorkflowDeletePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowDeletePrivateRegistry(t, tc) + require.Contains(t, out, "Workflows deleted successfully", "expected private registry delete success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Deleted workflow ID: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) +} diff --git a/test/multi_command_test.go b/test/multi_command_test.go index c90b630a..91a87bce 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -155,6 +155,7 @@ func TestMultiCommandHappyPaths(t *testing.T) { multi_command_flows.RunWorkflowPrivateRegistryHappyPath(t, tc) multi_command_flows.RunWorkflowPausePrivateRegistryHappyPath(t, tc) multi_command_flows.RunWorkflowActivatePrivateRegistryHappyPath(t, tc) + multi_command_flows.RunWorkflowDeletePrivateRegistryHappyPath(t, tc) }) // Run Account Happy Path: Link -> List -> Unlink -> List (verify unlinked)