From c8367bda8cc88ab6c42de0aae40d96d491cc44c6 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Thu, 28 May 2026 19:31:29 +0100 Subject: [PATCH 01/14] Fix context propagation in execute operations --- cmd/common/compile.go | 15 +++++---- cmd/common/compile_test.go | 17 +++++----- cmd/common/fetch.go | 10 ++++-- cmd/common/fetch_test.go | 7 ++-- cmd/workflow/build/build.go | 7 ++-- cmd/workflow/deploy/artifacts.go | 8 +++-- cmd/workflow/deploy/auto_link.go | 10 ++++-- cmd/workflow/deploy/compile.go | 2 +- cmd/workflow/deploy/compile_test.go | 3 +- cmd/workflow/deploy/deploy.go | 12 +++++-- .../registry_deploy_strategy_onchain.go | 20 ++++++++++- cmd/workflow/hash/hash.go | 19 ++++++----- cmd/workflow/hash/hash_test.go | 17 +++++----- cmd/workflow/simulate/simulate.go | 10 +++--- cmd/workflow/simulate/simulate_test.go | 2 +- .../client/storageclient/storageclient.go | 33 +++++++++++-------- 16 files changed, 123 insertions(+), 69 deletions(-) diff --git a/cmd/common/compile.go b/cmd/common/compile.go index 2456ec77..d500e6e9 100644 --- a/cmd/common/compile.go +++ b/cmd/common/compile.go @@ -1,6 +1,7 @@ package common import ( + "context" "errors" "fmt" "os" @@ -33,7 +34,7 @@ type WorkflowCompileOptions struct { } // getBuildCmd returns a single step that builds the workflow and returns the WASM bytes. -func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCompileOptions) (func() ([]byte, error), error) { +func getBuildCmd(ctx context.Context, workflowRootFolder, mainFile, language string, opts WorkflowCompileOptions) (func() ([]byte, error), error) { tmpPath := filepath.Join(workflowRootFolder, ".cre_build_tmp.wasm") switch language { case constants.WorkflowLanguageTypeScript: @@ -41,7 +42,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCom if opts.SkipTypeChecks { args = append(args, SkipTypeChecksFlag) } - cmd := exec.Command("bun", args...) + cmd := exec.CommandContext(ctx, "bun", args...) cmd.Dir = workflowRootFolder return func() ([]byte, error) { out, err := cmd.CombinedOutput() @@ -67,7 +68,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCom if opts.StripSymbols { ldflags = "-buildid= -w -s" } - cmd := exec.Command( + cmd := exec.CommandContext(ctx, "go", "build", "-o", tmpPath, "-trimpath", @@ -92,7 +93,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCom if err != nil { return nil, err } - makeCmd := exec.Command("make", "build") + makeCmd := exec.CommandContext(ctx, "make", "build") makeCmd.Dir = makeRoot builtPath := filepath.Join(makeRoot, defaultWasmOutput) return func() ([]byte, error) { @@ -108,7 +109,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCom if opts.StripSymbols { ldflags = "-buildid= -w -s" } - cmd := exec.Command( + cmd := exec.CommandContext(ctx, "go", "build", "-o", tmpPath, "-trimpath", @@ -135,7 +136,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCom // opts.StripSymbols: for Go builds, true strips debug symbols (deploy); false keeps them (simulate). // opts.SkipTypeChecks: for TypeScript, passes SkipTypeChecksFlag to cre-compile. // For custom Makefile WASM builds, StripSymbols and SkipTypeChecks have no effect. -func CompileWorkflowToWasm(workflowPath string, opts WorkflowCompileOptions) ([]byte, error) { +func CompileWorkflowToWasm(ctx context.Context, workflowPath string, opts WorkflowCompileOptions) ([]byte, error) { workflowRootFolder, workflowMainFile, err := WorkflowPathRootAndMain(workflowPath) if err != nil { return nil, fmt.Errorf("workflow path: %w", err) @@ -167,7 +168,7 @@ func CompileWorkflowToWasm(workflowPath string, opts WorkflowCompileOptions) ([] return nil, fmt.Errorf("unsupported workflow language for file %s", workflowMainFile) } - buildStep, err := getBuildCmd(workflowRootFolder, workflowMainFile, language, opts) + buildStep, err := getBuildCmd(ctx, workflowRootFolder, workflowMainFile, language, opts) if err != nil { return nil, err } diff --git a/cmd/common/compile_test.go b/cmd/common/compile_test.go index fdc7dc3d..fa4aa42c 100644 --- a/cmd/common/compile_test.go +++ b/cmd/common/compile_test.go @@ -2,6 +2,7 @@ package common import ( "bytes" + "context" "io" "os" "os/exec" @@ -47,21 +48,21 @@ func TestFindMakefileRoot(t *testing.T) { func TestCompileWorkflowToWasm_Go_Success(t *testing.T) { t.Run("basic_workflow", func(t *testing.T) { path := deployTestdataPath("basic_workflow", "main.go") - wasm, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) + wasm, err := CompileWorkflowToWasm(context.Background(), path, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) }) t.Run("configless_workflow", func(t *testing.T) { path := deployTestdataPath("configless_workflow", "main.go") - wasm, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) + wasm, err := CompileWorkflowToWasm(context.Background(), path, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) }) t.Run("missing_go_mod", func(t *testing.T) { path := deployTestdataPath("missing_go_mod", "main.go") - wasm, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) + wasm, err := CompileWorkflowToWasm(context.Background(), path, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) }) @@ -69,7 +70,7 @@ func TestCompileWorkflowToWasm_Go_Success(t *testing.T) { func TestCompileWorkflowToWasm_Go_Malformed_Fails(t *testing.T) { path := deployTestdataPath("malformed_workflow", "main.go") - _, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) + _, err := CompileWorkflowToWasm(context.Background(), path, WorkflowCompileOptions{StripSymbols: true}) require.Error(t, err) assert.Contains(t, err.Error(), "failed to compile workflow") assert.Contains(t, err.Error(), "undefined: sdk.RemovedFunctionThatFailsCompilation") @@ -80,7 +81,7 @@ func TestCompileWorkflowToWasm_Wasm_Success(t *testing.T) { _ = os.Remove(wasmPath) t.Cleanup(func() { _ = os.Remove(wasmPath) }) - wasm, err := CompileWorkflowToWasm(wasmPath, WorkflowCompileOptions{StripSymbols: true}) + wasm, err := CompileWorkflowToWasm(context.Background(), wasmPath, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) @@ -96,14 +97,14 @@ func TestCompileWorkflowToWasm_Wasm_Fails(t *testing.T) { wasmPath := filepath.Join(wasmDir, "workflow.wasm") require.NoError(t, os.WriteFile(wasmPath, []byte("not really wasm"), 0600)) - _, err := CompileWorkflowToWasm(wasmPath, WorkflowCompileOptions{StripSymbols: true}) + _, err := CompileWorkflowToWasm(context.Background(), wasmPath, WorkflowCompileOptions{StripSymbols: true}) require.Error(t, err) assert.Contains(t, err.Error(), "no Makefile found") }) t.Run("make_build_fails", func(t *testing.T) { path := deployTestdataPath("wasm_make_fails", "wasm", "workflow.wasm") - _, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) + _, err := CompileWorkflowToWasm(context.Background(), path, WorkflowCompileOptions{StripSymbols: true}) require.Error(t, err) assert.Contains(t, err.Error(), "failed to compile workflow") assert.Contains(t, err.Error(), "build output:") @@ -138,7 +139,7 @@ func TestCompileWorkflowToWasm_TS_Success(t *testing.T) { "include": ["main.ts"] } `), 0600)) - wasm, err := CompileWorkflowToWasm(mainPath, WorkflowCompileOptions{StripSymbols: true}) + wasm, err := CompileWorkflowToWasm(context.Background(), mainPath, WorkflowCompileOptions{StripSymbols: true}) if err != nil { t.Skipf("TS compile failed (published cre-sdk may lack full layout): %v", err) } diff --git a/cmd/common/fetch.go b/cmd/common/fetch.go index 5f8ee4f4..bc5b69e0 100644 --- a/cmd/common/fetch.go +++ b/cmd/common/fetch.go @@ -1,6 +1,7 @@ package common import ( + "context" "fmt" "io" "net/http" @@ -28,8 +29,13 @@ func IsURL(s string) bool { } // FetchURL performs an HTTP GET and returns the response body bytes. -func FetchURL(url string) ([]byte, error) { - resp, err := http.Get(url) //nolint:gosec,noctx +func FetchURL(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("HTTP GET %s: %w", url, err) + } + + resp, err := http.DefaultClient.Do(req) //nolint:gosec if err != nil { return nil, fmt.Errorf("HTTP GET %s: %w", url, err) } diff --git a/cmd/common/fetch_test.go b/cmd/common/fetch_test.go index 10a6ade6..0291f3dd 100644 --- a/cmd/common/fetch_test.go +++ b/cmd/common/fetch_test.go @@ -1,6 +1,7 @@ package common import ( + "context" "net/http" "net/http/httptest" "testing" @@ -42,7 +43,7 @@ func TestFetchURL(t *testing.T) { })) defer srv.Close() - data, err := FetchURL(srv.URL) + data, err := FetchURL(context.Background(), srv.URL) require.NoError(t, err) assert.Equal(t, body, data) }) @@ -53,13 +54,13 @@ func TestFetchURL(t *testing.T) { })) defer srv.Close() - _, err := FetchURL(srv.URL) + _, err := FetchURL(context.Background(), srv.URL) require.Error(t, err) assert.Contains(t, err.Error(), "returned status 404") }) t.Run("unreachable host", func(t *testing.T) { - _, err := FetchURL("http://127.0.0.1:1") + _, err := FetchURL(context.Background(), "http://127.0.0.1:1") require.Error(t, err) }) } diff --git a/cmd/workflow/build/build.go b/cmd/workflow/build/build.go index f92f6973..63b79edd 100644 --- a/cmd/workflow/build/build.go +++ b/cmd/workflow/build/build.go @@ -1,6 +1,7 @@ package build import ( + "context" "fmt" "os" "path/filepath" @@ -26,7 +27,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { outputPath, _ := cmd.Flags().GetString("output") skipTypeChecks, _ := cmd.Flags().GetBool(cmdcommon.SkipTypeChecksCLIFlag) - return execute(args[0], outputPath, skipTypeChecks) + return execute(cmd.Context(), args[0], outputPath, skipTypeChecks) }, } buildCmd.Flags().StringP("output", "o", "", "Output file path for the compiled WASM binary (default: /binary.wasm)") @@ -34,7 +35,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return buildCmd } -func execute(workflowFolder, outputPath string, skipTypeChecks bool) error { +func execute(ctx context.Context, workflowFolder, outputPath string, skipTypeChecks bool) error { workflowDir, err := filepath.Abs(workflowFolder) if err != nil { return fmt.Errorf("resolve workflow folder: %w", err) @@ -60,7 +61,7 @@ func execute(workflowFolder, outputPath string, skipTypeChecks bool) error { outputPath = cmdcommon.EnsureWasmExtension(outputPath) ui.Dim("Compiling workflow...") - wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedPath, cmdcommon.WorkflowCompileOptions{ + wasmBytes, err := cmdcommon.CompileWorkflowToWasm(ctx, resolvedPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: true, SkipTypeChecks: skipTypeChecks, }) diff --git a/cmd/workflow/deploy/artifacts.go b/cmd/workflow/deploy/artifacts.go index 070b1421..0bd00a3c 100644 --- a/cmd/workflow/deploy/artifacts.go +++ b/cmd/workflow/deploy/artifacts.go @@ -9,6 +9,10 @@ import ( ) func (h *handler) uploadArtifacts() error { + if err := h.execCtx.Err(); err != nil { + return err + } + if h.workflowArtifact == nil { return fmt.Errorf("workflowArtifact is nil") } @@ -46,7 +50,7 @@ func (h *handler) uploadArtifacts() error { if !binaryFromURL { ui.Success(fmt.Sprintf("Loaded binary from: %s", h.inputs.OutputPath)) binaryResp, err := storageClient.UploadArtifactWithRetriesAndGetURL( - workflowID, storageclient.ArtifactTypeBinary, binaryData, "application/octet-stream") + h.execCtx, workflowID, storageclient.ArtifactTypeBinary, binaryData, "application/octet-stream") if err != nil { return fmt.Errorf("uploading binary artifact: %w", err) } @@ -59,7 +63,7 @@ func (h *handler) uploadArtifacts() error { ui.Success(fmt.Sprintf("Loaded config from: %s", h.inputs.ConfigPath)) var err error configURL, err = storageClient.UploadArtifactWithRetriesAndGetURL( - workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") + h.execCtx, workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") if err != nil { return fmt.Errorf("uploading config artifact: %w", err) } diff --git a/cmd/workflow/deploy/auto_link.go b/cmd/workflow/deploy/auto_link.go index 7e393dc3..b9254356 100644 --- a/cmd/workflow/deploy/auto_link.go +++ b/cmd/workflow/deploy/auto_link.go @@ -1,7 +1,6 @@ package deploy import ( - "context" "fmt" "strings" "time" @@ -137,7 +136,7 @@ func (h *handler) checkLinkStatusViaGraphQL(ownerAddr common.Address) (bool, err } gql := graphqlclient.New(h.credentials, h.environmentSet, h.log) - if err := gql.Execute(context.Background(), req, &resp); err != nil { + if err := gql.Execute(h.execCtx, req, &resp); err != nil { return false, fmt.Errorf("GraphQL query failed: %w", err) } @@ -181,7 +180,11 @@ func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { ui.Line() // Wait for 3 block confirmations before polling - time.Sleep(initialBlockWait) + select { + case <-time.After(initialBlockWait): + case <-h.execCtx.Done(): + return h.execCtx.Err() + } err := retry.Do( func() error { @@ -199,6 +202,7 @@ func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { retry.Delay(retryDelay), retry.DelayType(retry.FixedDelay), // Use fixed 3s delay between retries retry.LastErrorOnly(true), + retry.Context(h.execCtx), retry.OnRetry(func(n uint, err error) { h.log.Debug().Uint("attempt", n+1).Uint("maxAttempts", maxAttempts).Err(err).Msg("Retrying link status check") ui.Dim(fmt.Sprintf(" Waiting for verification... (attempt %d/%d)", n+1, maxAttempts)) diff --git a/cmd/workflow/deploy/compile.go b/cmd/workflow/deploy/compile.go index ecb3c064..5004e7f7 100644 --- a/cmd/workflow/deploy/compile.go +++ b/cmd/workflow/deploy/compile.go @@ -67,7 +67,7 @@ func (h *handler) Compile() error { h.runtimeContext.Workflow.Language = cmdcommon.GetWorkflowLanguage(workflowMainFile) } - wasmFile, err = cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + wasmFile, err = cmdcommon.CompileWorkflowToWasm(h.execCtx, resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: true, SkipTypeChecks: h.inputs.SkipTypeChecks, }) diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index 149ac19a..d6f6c025 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "encoding/base64" "errors" "io" @@ -286,7 +287,7 @@ func outputPathWithExtensions(path string) string { // file content equals CompileWorkflowToWasm(workflowPath) + brotli + base64. func assertCompileOutputMatchesUnderlying(t *testing.T, simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inputs, ownerType string) { t.Helper() - wasm, err := cmdcommon.CompileWorkflowToWasm(inputs.WorkflowPath, cmdcommon.WorkflowCompileOptions{ + wasm, err := cmdcommon.CompileWorkflowToWasm(context.Background(), inputs.WorkflowPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: true, SkipTypeChecks: inputs.SkipTypeChecks, }) diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index 34f68f9b..d41f3b0b 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -231,6 +231,10 @@ func (h *handler) Execute(ctx context.Context) error { return err } + if err := h.execCtx.Err(); err != nil { + return err + } + if err := adapter.RunPreDeployChecks(); err != nil { if errors.Is(err, errDeployHalted) { return nil @@ -274,6 +278,10 @@ func (h *handler) Execute(ctx context.Context) error { // Artifact upload is deferred to the deploy service so it runs after any // existing-workflow update confirmation. func (h *handler) prepareArtifacts() error { + if err := h.execCtx.Err(); err != nil { + return err + } + workflowcommon.DisplayWorkflowDetails( h.settings, h.runtimeContext, @@ -285,7 +293,7 @@ func (h *handler) prepareArtifacts() error { if cmdcommon.IsURL(h.inputs.WasmPath) { h.inputs.BinaryURL = h.inputs.WasmPath ui.Dim("Fetching binary from URL for workflow ID computation...") - fetched, err := cmdcommon.FetchURL(h.inputs.WasmPath) + fetched, err := cmdcommon.FetchURL(h.execCtx, h.inputs.WasmPath) if err != nil { return fmt.Errorf("failed to fetch binary from URL: %w", err) } @@ -302,7 +310,7 @@ func (h *handler) prepareArtifacts() error { h.inputs.ConfigURL = &url h.inputs.ConfigPath = "" ui.Dim("Fetching config from URL for workflow ID computation...") - fetched, err := cmdcommon.FetchURL(url) + fetched, err := cmdcommon.FetchURL(h.execCtx, url) if err != nil { return fmt.Errorf("failed to fetch config from URL: %w", err) } diff --git a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go index 032de838..ab33d666 100644 --- a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go +++ b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "fmt" "sync" @@ -44,10 +45,27 @@ func newOnchainRegistryDeployStrategy(h *handler) (*onchainRegistryDeployStrateg return a, nil } +func waitWithContext(ctx context.Context, wg *sync.WaitGroup) error { + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { h := a.h - a.wg.Wait() + if err := waitWithContext(a.h.execCtx, &a.wg); err != nil { + return err + } if a.initErr != nil { return a.initErr } diff --git a/cmd/workflow/hash/hash.go b/cmd/workflow/hash/hash.go index 49533870..7efdb434 100644 --- a/cmd/workflow/hash/hash.go +++ b/cmd/workflow/hash/hash.go @@ -1,6 +1,7 @@ package hash import ( + "context" "fmt" "os" "strings" @@ -62,7 +63,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { DerivedOwner: runtimeContext.DerivedWorkflowOwner, } - return Execute(inputs) + return Execute(cmd.Context(), inputs) }, } @@ -81,8 +82,8 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return hashCmd } -func Execute(inputs Inputs) error { - rawBinary, err := loadBinary(inputs.WasmPath, inputs.WorkflowPath, inputs.SkipTypeChecks) +func Execute(ctx context.Context, inputs Inputs) error { + rawBinary, err := loadBinary(ctx, inputs.WasmPath, inputs.WorkflowPath, inputs.SkipTypeChecks) if err != nil { return err } @@ -92,7 +93,7 @@ func Execute(inputs Inputs) error { return fmt.Errorf("failed to compress binary: %w", err) } - config, err := loadConfig(inputs.ConfigPath) + config, err := loadConfig(ctx, inputs.ConfigPath) if err != nil { return err } @@ -190,11 +191,11 @@ func isPrivateRegistryID(deploymentRegistry string) bool { return strings.EqualFold(deploymentRegistry, "private") } -func loadBinary(wasmFlag, workflowPathFromSettings string, skipTypeChecks bool) ([]byte, error) { +func loadBinary(ctx context.Context, wasmFlag, workflowPathFromSettings string, skipTypeChecks bool) ([]byte, error) { if wasmFlag != "" { if cmdcommon.IsURL(wasmFlag) { ui.Dim("Fetching WASM binary from URL...") - data, err := cmdcommon.FetchURL(wasmFlag) + data, err := cmdcommon.FetchURL(ctx, wasmFlag) if err != nil { return nil, fmt.Errorf("failed to fetch WASM from URL: %w", err) } @@ -221,7 +222,7 @@ func loadBinary(wasmFlag, workflowPathFromSettings string, skipTypeChecks bool) spinner := ui.NewSpinner() spinner.Start("Compiling workflow...") - wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + wasmBytes, err := cmdcommon.CompileWorkflowToWasm(ctx, resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: true, SkipTypeChecks: skipTypeChecks, }) @@ -235,13 +236,13 @@ func loadBinary(wasmFlag, workflowPathFromSettings string, skipTypeChecks bool) return wasmBytes, nil } -func loadConfig(configPath string) ([]byte, error) { +func loadConfig(ctx context.Context, configPath string) ([]byte, error) { if configPath == "" { return nil, nil } if cmdcommon.IsURL(configPath) { ui.Dim("Fetching config from URL...") - data, err := cmdcommon.FetchURL(configPath) + data, err := cmdcommon.FetchURL(ctx, configPath) if err != nil { return nil, fmt.Errorf("failed to fetch config from URL: %w", err) } diff --git a/cmd/workflow/hash/hash_test.go b/cmd/workflow/hash/hash_test.go index 0ed08a82..14809402 100644 --- a/cmd/workflow/hash/hash_test.go +++ b/cmd/workflow/hash/hash_test.go @@ -1,6 +1,7 @@ package hash import ( + "context" "crypto/sha256" "encoding/hex" "io" @@ -80,7 +81,7 @@ func TestExecute_WithForUser(t *testing.T) { WorkflowName: "test-workflow", } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.NoError(t, err) } @@ -94,7 +95,7 @@ func TestExecute_WithoutForUser_UsesPrivateKey(t *testing.T) { PrivateKey: testPrivateKey, } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.NoError(t, err) } @@ -107,7 +108,7 @@ func TestExecute_WithoutForUser_NoKey_Errors(t *testing.T) { WorkflowName: "test-workflow", } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.Error(t, err) assert.Contains(t, err.Error(), "--public_key") } @@ -173,7 +174,7 @@ func TestExecute_HashesAreDeterministic(t *testing.T) { "workflow ID should start with version byte 00") // Running Execute should succeed (hashes are printed via ui, verified above) - err = Execute(inputs) + err = Execute(context.Background(), inputs) require.NoError(t, err) } @@ -187,7 +188,7 @@ func TestExecute_EmptyConfig(t *testing.T) { WorkflowName: "test-workflow", } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.NoError(t, err) } @@ -201,7 +202,7 @@ func TestExecute_OffChainRequiresPublicKey(t *testing.T) { RegistryType: settings.RegistryTypeOffChain, } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.Error(t, err) assert.Contains(t, err.Error(), "--public_key") } @@ -218,7 +219,7 @@ func TestExecute_OffChainUsesPublicKey(t *testing.T) { DerivedOwner: testDerivedOwner, } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.NoError(t, err) } @@ -233,7 +234,7 @@ func TestExecute_OffChainUsesDerivedOwner(t *testing.T) { DerivedOwner: testDerivedOwner, } - err := Execute(inputs) + err := Execute(context.Background(), inputs) require.NoError(t, err) } diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 7d0ca445..fe95ed2b 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -90,7 +90,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { if err != nil { return err } - return handler.Execute(inputs) + return handler.Execute(cmd.Context(), inputs) }, } @@ -252,14 +252,14 @@ func (h *handler) ValidateInputs(inputs Inputs) error { return nil } -func (h *handler) Execute(inputs Inputs) error { +func (h *handler) Execute(ctx context.Context, inputs Inputs) error { var wasmFileBinary []byte var err error if inputs.WasmPath != "" { if cmdcommon.IsURL(inputs.WasmPath) { ui.Dim("Fetching WASM binary from URL...") - wasmFileBinary, err = cmdcommon.FetchURL(inputs.WasmPath) + wasmFileBinary, err = cmdcommon.FetchURL(ctx, inputs.WasmPath) if err != nil { return fmt.Errorf("failed to fetch WASM from URL: %w", err) } @@ -298,7 +298,7 @@ func (h *handler) Execute(inputs Inputs) error { spinner := ui.NewSpinner() spinner.Start("Compiling workflow...") - wasmFileBinary, err = cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + wasmFileBinary, err = cmdcommon.CompileWorkflowToWasm(ctx, resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: false, SkipTypeChecks: inputs.SkipTypeChecks, }) @@ -343,7 +343,7 @@ func (h *handler) Execute(inputs Inputs) error { var config []byte if cmdcommon.IsURL(inputs.ConfigPath) { ui.Dim("Fetching config from URL...") - config, err = cmdcommon.FetchURL(inputs.ConfigPath) + config, err = cmdcommon.FetchURL(ctx, inputs.ConfigPath) if err != nil { return fmt.Errorf("failed to fetch config from URL: %w", err) } diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index 847d7ae2..4879b8f3 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -98,7 +98,7 @@ func TestBlankWorkflowSimulation(t *testing.T) { require.NoError(t, err) // Execute the simulation. We expect this to compile the workflow and run the simulator successfully. - err = handler.Execute(inputs) + err = handler.Execute(context.Background(), inputs) require.NoError(t, err, "Execute should not return an error") } diff --git a/internal/client/storageclient/storageclient.go b/internal/client/storageclient/storageclient.go index de16d790..9a798684 100644 --- a/internal/client/storageclient/storageclient.go +++ b/internal/client/storageclient/storageclient.go @@ -69,15 +69,15 @@ func (c *Client) SetHTTPTimeout(timeout time.Duration) { c.httpTimeout = timeout } -func (c *Client) CreateServiceContextWithTimeout() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), c.serviceTimeout) //nolint:gosec // G118 -- cancel is deferred by all callers +func (c *Client) CreateServiceContextWithTimeout(parent context.Context) (context.Context, context.CancelFunc) { + return context.WithTimeout(parent, c.serviceTimeout) //nolint:gosec // G118 -- cancel is deferred by all callers } -func (c *Client) CreateHttpContextWithTimeout() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), c.httpTimeout) //nolint:gosec // G118 -- cancel is deferred by all callers +func (c *Client) CreateHttpContextWithTimeout(parent context.Context) (context.Context, context.CancelFunc) { + return context.WithTimeout(parent, c.httpTimeout) //nolint:gosec // G118 -- cancel is deferred by all callers } -func (c *Client) GeneratePostUrlForArtifact(workflowId string, artifactType ArtifactType, content []byte) (GeneratePresignedPostUrlForArtifactResponse, error) { +func (c *Client) GeneratePostUrlForArtifact(ctx context.Context, workflowId string, artifactType ArtifactType, content []byte) (GeneratePresignedPostUrlForArtifactResponse, error) { const mutation = ` mutation GeneratePresignedPostUrlForArtifact($artifact: GeneratePresignedPostUrlRequest!) { generatePresignedPostUrlForArtifact(artifact: $artifact) { @@ -102,7 +102,7 @@ mutation GeneratePresignedPostUrlForArtifact($artifact: GeneratePresignedPostUrl GeneratePresignedPostUrlForArtifact GeneratePresignedPostUrlForArtifactResponse `json:"generatePresignedPostUrlForArtifact"` } - ctx, cancel := c.CreateServiceContextWithTimeout() + ctx, cancel := c.CreateServiceContextWithTimeout(ctx) defer cancel() if err := c.graphql. @@ -116,7 +116,7 @@ mutation GeneratePresignedPostUrlForArtifact($artifact: GeneratePresignedPostUrl return container.GeneratePresignedPostUrlForArtifact, nil } -func (c *Client) GenerateUnsignedGetUrlForArtifact(workflowId string, artifactType ArtifactType) (GenerateUnsignedGetUrlForArtifactResponse, error) { +func (c *Client) GenerateUnsignedGetUrlForArtifact(ctx context.Context, workflowId string, artifactType ArtifactType) (GenerateUnsignedGetUrlForArtifactResponse, error) { const mutation = ` mutation GenerateUnsignedGetUrlForArtifact($artifact: GenerateUnsignedGetUrlRequest!) { generateUnsignedGetUrlForArtifact(artifact: $artifact) { @@ -134,7 +134,7 @@ mutation GenerateUnsignedGetUrlForArtifact($artifact: GenerateUnsignedGetUrlRequ GenerateUnsignedGetUrlForArtifact GenerateUnsignedGetUrlForArtifactResponse `json:"generateUnsignedGetUrlForArtifact"` } - ctx, cancel := c.CreateServiceContextWithTimeout() + ctx, cancel := c.CreateServiceContextWithTimeout(ctx) defer cancel() if err := c.graphql. @@ -154,7 +154,7 @@ func calculateContentHash(content []byte) string { return contentHash } -func (c *Client) UploadToOrigin(g GeneratePresignedPostUrlForArtifactResponse, content []byte, contentType string) error { +func (c *Client) UploadToOrigin(ctx context.Context, g GeneratePresignedPostUrlForArtifactResponse, content []byte, contentType string) error { c.log.Debug().Str("URL", g.PresignedPostURL).Msg("Uploading content to origin") var b bytes.Buffer @@ -197,7 +197,7 @@ func (c *Client) UploadToOrigin(g GeneratePresignedPostUrlForArtifactResponse, c return err } - ctx, cancel := c.CreateHttpContextWithTimeout() + ctx, cancel := c.CreateHttpContextWithTimeout(ctx) defer cancel() httpReq, err := http.NewRequestWithContext(ctx, "POST", g.PresignedPostURL, &b) @@ -231,10 +231,14 @@ func (c *Client) UploadToOrigin(g GeneratePresignedPostUrlForArtifactResponse, c } func (c *Client) UploadArtifactWithRetriesAndGetURL( + ctx context.Context, workflowID string, artifactType ArtifactType, content []byte, contentType string) (GenerateUnsignedGetUrlForArtifactResponse, error) { + if err := ctx.Err(); err != nil { + return GenerateUnsignedGetUrlForArtifactResponse{}, err + } if len(workflowID) == 0 { return GenerateUnsignedGetUrlForArtifactResponse{}, fmt.Errorf("workflowID is empty") } @@ -251,7 +255,7 @@ func (c *Client) UploadArtifactWithRetriesAndGetURL( err := retry.Do( func() error { var err error - g, err = c.GeneratePostUrlForArtifact(workflowID, artifactType, content) + g, err = c.GeneratePostUrlForArtifact(ctx, workflowID, artifactType, content) if err != nil { if strings.Contains(err.Error(), "already exists") { shouldUpload = false @@ -264,6 +268,7 @@ func (c *Client) UploadArtifactWithRetriesAndGetURL( }, retry.Attempts(3), retry.LastErrorOnly(true), + retry.Context(ctx), ) if err != nil { c.log.Error().Err(err).Msg("Failed to generate presigned post URL for artifact") @@ -276,10 +281,11 @@ func (c *Client) UploadArtifactWithRetriesAndGetURL( if shouldUpload { err = retry.Do( func() error { - return c.UploadToOrigin(g, content, contentType) + return c.UploadToOrigin(ctx, g, content, contentType) }, retry.Attempts(3), retry.LastErrorOnly(true), + retry.Context(ctx), ) if err != nil { c.log.Error().Err(err).Msg("Failed to upload content to origin") @@ -290,7 +296,7 @@ func (c *Client) UploadArtifactWithRetriesAndGetURL( var g2 GenerateUnsignedGetUrlForArtifactResponse err = retry.Do( func() error { - g2, err = c.GenerateUnsignedGetUrlForArtifact(workflowID, artifactType) + g2, err = c.GenerateUnsignedGetUrlForArtifact(ctx, workflowID, artifactType) if err != nil { return fmt.Errorf("generate unsigned get url: %w", err) } @@ -298,6 +304,7 @@ func (c *Client) UploadArtifactWithRetriesAndGetURL( }, retry.Attempts(3), retry.LastErrorOnly(true), + retry.Context(ctx), ) if err != nil { c.log.Error().Err(err).Msg("Failed to generate unsigned get URL for artifact") From c759f883d55bfe158bbd4a9037397c185e3b9177 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Fri, 29 May 2026 13:07:44 +0100 Subject: [PATCH 02/14] Fix unit test --- cmd/workflow/deploy/artifacts_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/workflow/deploy/artifacts_test.go b/cmd/workflow/deploy/artifacts_test.go index 24833d9c..e720a231 100644 --- a/cmd/workflow/deploy/artifacts_test.go +++ b/cmd/workflow/deploy/artifacts_test.go @@ -1,6 +1,7 @@ package deploy import ( + "context" //nolint:gosec "encoding/json" "errors" @@ -72,6 +73,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() h := newHandler(ctx, buf) + h.execCtx = context.Background() h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -148,6 +150,7 @@ func TestUploadArtifactToStorageService_OriginError(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) runtimeContext, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() h := newHandler(runtimeContext, buf) + h.execCtx = context.Background() h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -188,6 +191,7 @@ func TestUploadArtifactToStorageService_AlreadyExistsError(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) runtimeContext, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() h := newHandler(runtimeContext, buf) + h.execCtx = context.Background() h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -256,6 +260,7 @@ func TestUpload_UsesResolvedWorkflowOwnerForPresignedUrls(t *testing.T) { t.Cleanup(simulatedEnvironment.Close) ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() h := newHandler(ctx, buf) + h.execCtx = context.Background() h.inputs.WorkflowOwner = "0x2222222222222222222222222222222222222222" h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" From 106820648218757dc03dbb61a318d8557e22030b Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Fri, 29 May 2026 13:17:13 +0100 Subject: [PATCH 03/14] Fix second unit test --- cmd/workflow/deploy/compile_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index d6f6c025..273509c9 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -272,6 +272,7 @@ func runCompile(simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inpu return err } + handler.execCtx = context.Background() return handler.Compile() } @@ -434,6 +435,7 @@ func TestCompileWithWasmPath(t *testing.T) { WasmPath: "https://example.com/binary.wasm", } handler.validated = true + handler.execCtx = context.Background() // Compile() with URL wasm should return nil (skips compile entirely). err := handler.Compile() From 25a0a56c745890cc214879a3e31a0be395767269 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Fri, 29 May 2026 14:06:47 +0100 Subject: [PATCH 04/14] Refactor of ctx prop --- cmd/workflow/deploy/artifacts.go | 6 +++--- cmd/workflow/deploy/auto_link.go | 15 ++++++++------- cmd/workflow/deploy/auto_link_test.go | 3 +++ cmd/workflow/deploy/compile.go | 2 +- cmd/workflow/deploy/deploy.go | 17 +++++++++++++---- cmd/workflow/deploy/register.go | 2 +- .../deploy/registry_deploy_strategy_onchain.go | 8 ++++---- .../deploy/registry_deploy_strategy_private.go | 4 ++-- cmd/workflow/deploy/test_helpers_test.go | 17 +++++++++++++++++ 9 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 cmd/workflow/deploy/test_helpers_test.go diff --git a/cmd/workflow/deploy/artifacts.go b/cmd/workflow/deploy/artifacts.go index 0bd00a3c..7f7d6803 100644 --- a/cmd/workflow/deploy/artifacts.go +++ b/cmd/workflow/deploy/artifacts.go @@ -9,7 +9,7 @@ import ( ) func (h *handler) uploadArtifacts() error { - if err := h.execCtx.Err(); err != nil { + if err := h.executionContext().Err(); err != nil { return err } @@ -50,7 +50,7 @@ func (h *handler) uploadArtifacts() error { if !binaryFromURL { ui.Success(fmt.Sprintf("Loaded binary from: %s", h.inputs.OutputPath)) binaryResp, err := storageClient.UploadArtifactWithRetriesAndGetURL( - h.execCtx, workflowID, storageclient.ArtifactTypeBinary, binaryData, "application/octet-stream") + h.executionContext(), workflowID, storageclient.ArtifactTypeBinary, binaryData, "application/octet-stream") if err != nil { return fmt.Errorf("uploading binary artifact: %w", err) } @@ -63,7 +63,7 @@ func (h *handler) uploadArtifacts() error { ui.Success(fmt.Sprintf("Loaded config from: %s", h.inputs.ConfigPath)) var err error configURL, err = storageClient.UploadArtifactWithRetriesAndGetURL( - h.execCtx, workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") + h.executionContext(), workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") if err != nil { return fmt.Errorf("uploading config artifact: %w", err) } diff --git a/cmd/workflow/deploy/auto_link.go b/cmd/workflow/deploy/auto_link.go index b9254356..fbcadb0d 100644 --- a/cmd/workflow/deploy/auto_link.go +++ b/cmd/workflow/deploy/auto_link.go @@ -24,7 +24,7 @@ const ( func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) error { ownerAddr := common.HexToAddress(h.inputs.WorkflowOwner) - linked, err := h.wrc.IsOwnerLinked(h.execCtx, ownerAddr) + linked, err := h.wrc.IsOwnerLinked(h.executionContext(), ownerAddr) if err != nil { return fmt.Errorf("failed to check owner link status: %w", err) } @@ -65,7 +65,7 @@ func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) err func (h *handler) autoLinkMSIGAndExit(onChain *settings.OnChainRegistry) (halt bool, err error) { ownerAddr := common.HexToAddress(h.inputs.WorkflowOwner) - linked, err := h.wrc.IsOwnerLinked(h.execCtx, ownerAddr) + linked, err := h.wrc.IsOwnerLinked(h.executionContext(), ownerAddr) if err != nil { return false, fmt.Errorf("failed to check owner link status: %w", err) } @@ -106,7 +106,7 @@ func (h *handler) tryAutoLink(onChain *settings.OnChainRegistry) error { EnvironmentSet: h.environmentSet, } - return linkkey.Exec(h.execCtx, rtx, linkkey.Inputs{ + return linkkey.Exec(h.executionContext(), rtx, linkkey.Inputs{ WorkflowOwner: h.inputs.WorkflowOwner, WorkflowRegistryContractAddress: onChain.Address(), WorkflowOwnerLabel: h.inputs.OwnerLabel, @@ -136,7 +136,7 @@ func (h *handler) checkLinkStatusViaGraphQL(ownerAddr common.Address) (bool, err } gql := graphqlclient.New(h.credentials, h.environmentSet, h.log) - if err := gql.Execute(h.execCtx, req, &resp); err != nil { + if err := gql.Execute(h.executionContext(), req, &resp); err != nil { return false, fmt.Errorf("GraphQL query failed: %w", err) } @@ -180,10 +180,11 @@ func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { ui.Line() // Wait for 3 block confirmations before polling + ctx := h.executionContext() select { case <-time.After(initialBlockWait): - case <-h.execCtx.Done(): - return h.execCtx.Err() + case <-ctx.Done(): + return ctx.Err() } err := retry.Do( @@ -202,7 +203,7 @@ func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { retry.Delay(retryDelay), retry.DelayType(retry.FixedDelay), // Use fixed 3s delay between retries retry.LastErrorOnly(true), - retry.Context(h.execCtx), + retry.Context(ctx), retry.OnRetry(func(n uint, err error) { h.log.Debug().Uint("attempt", n+1).Uint("maxAttempts", maxAttempts).Err(err).Msg("Retrying link status check") ui.Dim(fmt.Sprintf(" Waiting for verification... (attempt %d/%d)", n+1, maxAttempts)) diff --git a/cmd/workflow/deploy/auto_link_test.go b/cmd/workflow/deploy/auto_link_test.go index e192ccfa..beb9719d 100644 --- a/cmd/workflow/deploy/auto_link_test.go +++ b/cmd/workflow/deploy/auto_link_test.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -159,6 +160,7 @@ func TestCheckLinkStatusViaGraphQL(t *testing.T) { IsValidated: true, } h := newHandler(ctx, nil) + h.execCtx = context.Background() h.inputs.WorkflowOwner = tt.ownerAddress h.environmentSet.GraphQLURL = server.URL + "/graphql" @@ -330,6 +332,7 @@ func TestWaitForBackendLinkProcessing(t *testing.T) { IsValidated: true, } h := newHandler(ctx, nil) + h.execCtx = context.Background() h.inputs.WorkflowOwner = tt.ownerAddress h.environmentSet.GraphQLURL = server.URL + "/graphql" diff --git a/cmd/workflow/deploy/compile.go b/cmd/workflow/deploy/compile.go index 5004e7f7..a3dda9c5 100644 --- a/cmd/workflow/deploy/compile.go +++ b/cmd/workflow/deploy/compile.go @@ -67,7 +67,7 @@ func (h *handler) Compile() error { h.runtimeContext.Workflow.Language = cmdcommon.GetWorkflowLanguage(workflowMainFile) } - wasmFile, err = cmdcommon.CompileWorkflowToWasm(h.execCtx, resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + wasmFile, err = cmdcommon.CompileWorkflowToWasm(h.executionContext(), resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: true, SkipTypeChecks: h.inputs.SkipTypeChecks, }) diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index d41f3b0b..87e7dd28 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -133,6 +133,15 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { return &h } +// executionContext returns the context from Execute(), or context.Background() +// when handler methods are invoked directly in unit tests. +func (h *handler) executionContext() context.Context { + if h.execCtx != nil { + return h.execCtx + } + return context.Background() +} + func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { var configURL *string if v.IsSet("config-url") { @@ -231,7 +240,7 @@ func (h *handler) Execute(ctx context.Context) error { return err } - if err := h.execCtx.Err(); err != nil { + if err := h.executionContext().Err(); err != nil { return err } @@ -278,7 +287,7 @@ func (h *handler) Execute(ctx context.Context) error { // Artifact upload is deferred to the deploy service so it runs after any // existing-workflow update confirmation. func (h *handler) prepareArtifacts() error { - if err := h.execCtx.Err(); err != nil { + if err := h.executionContext().Err(); err != nil { return err } @@ -293,7 +302,7 @@ func (h *handler) prepareArtifacts() error { if cmdcommon.IsURL(h.inputs.WasmPath) { h.inputs.BinaryURL = h.inputs.WasmPath ui.Dim("Fetching binary from URL for workflow ID computation...") - fetched, err := cmdcommon.FetchURL(h.execCtx, h.inputs.WasmPath) + fetched, err := cmdcommon.FetchURL(h.executionContext(), h.inputs.WasmPath) if err != nil { return fmt.Errorf("failed to fetch binary from URL: %w", err) } @@ -310,7 +319,7 @@ func (h *handler) prepareArtifacts() error { h.inputs.ConfigURL = &url h.inputs.ConfigPath = "" ui.Dim("Fetching config from URL for workflow ID computation...") - fetched, err := cmdcommon.FetchURL(h.execCtx, url) + fetched, err := cmdcommon.FetchURL(h.executionContext(), url) if err != nil { return fmt.Errorf("failed to fetch config from URL: %w", err) } diff --git a/cmd/workflow/deploy/register.go b/cmd/workflow/deploy/register.go index 29c6ac67..b65aad3a 100644 --- a/cmd/workflow/deploy/register.go +++ b/cmd/workflow/deploy/register.go @@ -57,7 +57,7 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters, onCha workflowName := h.inputs.WorkflowName workflowTag := h.inputs.WorkflowTag h.log.Debug().Interface("Workflow parameters", params).Msg("Registering workflow...") - txOut, err := h.wrc.UpsertWorkflow(h.execCtx, params) + txOut, err := h.wrc.UpsertWorkflow(h.executionContext(), params) if err != nil { return fmt.Errorf("failed to register workflow: %w", err) } diff --git a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go index ab33d666..1ed63bce 100644 --- a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go +++ b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go @@ -34,7 +34,7 @@ func newOnchainRegistryDeployStrategy(h *handler) (*onchainRegistryDeployStrateg a.wg.Add(1) go func() { defer a.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client(h.execCtx) + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client(h.executionContext()) if err != nil { a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) return @@ -63,7 +63,7 @@ func waitWithContext(ctx context.Context, wg *sync.WaitGroup) error { func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { h := a.h - if err := waitWithContext(a.h.execCtx, &a.wg); err != nil { + if err := waitWithContext(a.h.executionContext(), &a.wg); err != nil { return err } if a.initErr != nil { @@ -90,7 +90,7 @@ func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { } func (a *onchainRegistryDeployStrategy) CheckWorkflowExists(workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) { - workflow, err := a.wrc.GetWorkflow(a.h.execCtx, common.HexToAddress(workflowOwner), workflowName, workflowTag) + workflow, err := a.wrc.GetWorkflow(a.h.executionContext(), common.HexToAddress(workflowOwner), workflowName, workflowTag) if err != nil { return false, nil, err } @@ -110,7 +110,7 @@ func (a *onchainRegistryDeployStrategy) Upsert() error { h := a.h if err := checkUserDonLimitBeforeDeploy( - h.execCtx, + h.executionContext(), a.wrc, a.wrc, common.HexToAddress(h.inputs.WorkflowOwner), diff --git a/cmd/workflow/deploy/registry_deploy_strategy_private.go b/cmd/workflow/deploy/registry_deploy_strategy_private.go index 9909800e..2a77d2b9 100644 --- a/cmd/workflow/deploy/registry_deploy_strategy_private.go +++ b/cmd/workflow/deploy/registry_deploy_strategy_private.go @@ -34,7 +34,7 @@ func (a *privateRegistryDeployStrategy) RunPreDeployChecks() error { func (a *privateRegistryDeployStrategy) CheckWorkflowExists(_, workflowName, _, workflowID string) (bool, *uint8, error) { a.ensureClient() - workflow, err := a.prc.GetWorkflowByName(a.h.execCtx, workflowName) + workflow, err := a.prc.GetWorkflowByName(a.h.executionContext(), workflowName) if err == nil { if workflow.WorkflowID == workflowID { return true, offchainStatusToUint8(workflow.Status), fmt.Errorf("workflow with id %s is already registered and unchanged; re-deployment skipped: %w", workflowID, errWorkflowUnchanged) @@ -57,7 +57,7 @@ func (a *privateRegistryDeployStrategy) Upsert() error { ui.Line() ui.Dim(fmt.Sprintf("Registering workflow in private registry (workflowID: %s)...", input.WorkflowID)) - result, err := a.prc.UpsertWorkflowInRegistry(a.h.execCtx, input) + result, err := a.prc.UpsertWorkflowInRegistry(a.h.executionContext(), input) if err != nil { return fmt.Errorf("failed to register workflow in private registry: %w", err) } diff --git a/cmd/workflow/deploy/test_helpers_test.go b/cmd/workflow/deploy/test_helpers_test.go new file mode 100644 index 00000000..e321e3c6 --- /dev/null +++ b/cmd/workflow/deploy/test_helpers_test.go @@ -0,0 +1,17 @@ +package deploy + +import ( + "context" + "io" + + "github.com/smartcontractkit/cre-cli/internal/runtime" +) + +// newTestHandler returns a handler suitable for unit tests that call handler +// methods directly instead of going through Execute(). It pre-sets execCtx so +// cancellation-aware code paths behave like a normal CLI invocation. +func newTestHandler(ctx *runtime.Context, stdin io.Reader) *handler { + h := newHandler(ctx, stdin) + h.execCtx = context.Background() + return h +} From 46af6501a94bb04c4104ec4076c8bc9abf6249b5 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Fri, 29 May 2026 14:13:34 +0100 Subject: [PATCH 05/14] Use test handler to avoid ctx nil panics --- cmd/workflow/deploy/artifacts_test.go | 13 ++---- cmd/workflow/deploy/auto_link_test.go | 7 +-- cmd/workflow/deploy/compile_test.go | 9 ++-- cmd/workflow/deploy/private_registry_test.go | 4 +- cmd/workflow/deploy/register_test.go | 45 +++++++++----------- 5 files changed, 31 insertions(+), 47 deletions(-) diff --git a/cmd/workflow/deploy/artifacts_test.go b/cmd/workflow/deploy/artifacts_test.go index e720a231..1d18a759 100644 --- a/cmd/workflow/deploy/artifacts_test.go +++ b/cmd/workflow/deploy/artifacts_test.go @@ -1,7 +1,6 @@ package deploy import ( - "context" //nolint:gosec "encoding/json" "errors" @@ -72,8 +71,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newHandler(ctx, buf) - h.execCtx = context.Background() + h := newTestHandler(ctx, buf) h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -149,8 +147,7 @@ func TestUploadArtifactToStorageService_OriginError(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) runtimeContext, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newHandler(runtimeContext, buf) - h.execCtx = context.Background() + h := newTestHandler(runtimeContext, buf) h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -190,8 +187,7 @@ func TestUploadArtifactToStorageService_AlreadyExistsError(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) runtimeContext, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newHandler(runtimeContext, buf) - h.execCtx = context.Background() + h := newTestHandler(runtimeContext, buf) h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -259,8 +255,7 @@ func TestUpload_UsesResolvedWorkflowOwnerForPresignedUrls(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) t.Cleanup(simulatedEnvironment.Close) ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newHandler(ctx, buf) - h.execCtx = context.Background() + h := newTestHandler(ctx, buf) h.inputs.WorkflowOwner = "0x2222222222222222222222222222222222222222" h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" diff --git a/cmd/workflow/deploy/auto_link_test.go b/cmd/workflow/deploy/auto_link_test.go index beb9719d..aa4d8b3f 100644 --- a/cmd/workflow/deploy/auto_link_test.go +++ b/cmd/workflow/deploy/auto_link_test.go @@ -1,7 +1,6 @@ package deploy import ( - "context" "encoding/json" "net/http" "net/http/httptest" @@ -159,8 +158,7 @@ func TestCheckLinkStatusViaGraphQL(t *testing.T) { AuthType: credentials.AuthTypeApiKey, IsValidated: true, } - h := newHandler(ctx, nil) - h.execCtx = context.Background() + h := newTestHandler(ctx, nil) h.inputs.WorkflowOwner = tt.ownerAddress h.environmentSet.GraphQLURL = server.URL + "/graphql" @@ -331,8 +329,7 @@ func TestWaitForBackendLinkProcessing(t *testing.T) { AuthType: credentials.AuthTypeApiKey, IsValidated: true, } - h := newHandler(ctx, nil) - h.execCtx = context.Background() + h := newTestHandler(ctx, nil) h.inputs.WorkflowOwner = tt.ownerAddress h.environmentSet.GraphQLURL = server.URL + "/graphql" diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index 273509c9..ffd29c61 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -255,7 +255,7 @@ func createTestSettings(workflowOwnerAddress, workflowOwnerType, workflowName, w func runCompile(simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inputs, ownerType string) error { ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newHandler(ctx, buf) + handler := newTestHandler(ctx, buf) ctx.Settings = createTestSettings( inputs.WorkflowOwner, @@ -267,12 +267,10 @@ func runCompile(simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inpu handler.settings = ctx.Settings handler.inputs = inputs - err := handler.ValidateInputs() - if err != nil { + if err := handler.ValidateInputs(); err != nil { return err } - handler.execCtx = context.Background() return handler.Compile() } @@ -418,7 +416,7 @@ func TestCompileWithWasmPath(t *testing.T) { defer simulatedEnvironment.Close() ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newHandler(ctx, buf) + handler := newTestHandler(ctx, buf) ctx.Settings = createTestSettings( chainsim.TestAddress, constants.WorkflowOwnerTypeEOA, @@ -435,7 +433,6 @@ func TestCompileWithWasmPath(t *testing.T) { WasmPath: "https://example.com/binary.wasm", } handler.validated = true - handler.execCtx = context.Background() // Compile() with URL wasm should return nil (skips compile entirely). err := handler.Compile() diff --git a/cmd/workflow/deploy/private_registry_test.go b/cmd/workflow/deploy/private_registry_test.go index db0ed61b..e7f839a6 100644 --- a/cmd/workflow/deploy/private_registry_test.go +++ b/cmd/workflow/deploy/private_registry_test.go @@ -1,7 +1,6 @@ package deploy import ( - "context" "encoding/base64" "encoding/hex" "encoding/json" @@ -303,7 +302,7 @@ func TestCheckWorkflowExists_PrivateRegistry(t *testing.T) { defer simulatedEnvironment.Close() ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newHandler(ctx, buf) + h := newTestHandler(ctx, buf) h.credentials = makeAPIKeyCredentials(t) gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { @@ -313,7 +312,6 @@ func TestCheckWorkflowExists_PrivateRegistry(t *testing.T) { defer gqlServer.Close() h.environmentSet.GraphQLURL = gqlServer.URL - h.execCtx = context.Background() strategy := newPrivateRegistryDeployStrategy(h) exists, status, err := strategy.CheckWorkflowExists("", "jnowak-workflow-test-v5", "", tt.workflowID) diff --git a/cmd/workflow/deploy/register_test.go b/cmd/workflow/deploy/register_test.go index b039aaf5..0613998f 100644 --- a/cmd/workflow/deploy/register_test.go +++ b/cmd/workflow/deploy/register_test.go @@ -45,30 +45,27 @@ func TestWorkflowUpsert(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) defer simulatedEnvironment.Close() - ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newHandler(ctx, buf) - - wrc, err := handler.clientFactory.NewWorkflowRegistryV2Client(context.Background()) - require.NoError(t, err) - handler.wrc = wrc - - handler.inputs = tt.inputs - err = handler.ValidateInputs() - require.NoError(t, err) - - wfArt := workflowArtifact{ - BinaryData: []byte("0x1234"), - ConfigData: []byte("config"), - WorkflowID: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - } - - handler.workflowArtifact = &wfArt - handler.execCtx = context.Background() - - onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") - require.NoError(t, err) - err = handler.upsert(onChain) - require.NoError(t, err) + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + handler := newTestHandler(ctx, buf) + + wrc, err := handler.clientFactory.NewWorkflowRegistryV2Client(context.Background()) + require.NoError(t, err) + handler.wrc = wrc + + handler.inputs = tt.inputs + err = handler.ValidateInputs() + require.NoError(t, err) + + handler.workflowArtifact = &workflowArtifact{ + BinaryData: []byte("0x1234"), + ConfigData: []byte("config"), + WorkflowID: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + } + + onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") + require.NoError(t, err) + err = handler.upsert(onChain) + require.NoError(t, err) }) } }) From 173d08ea1247c76792d4df8703480c7f13f6c8ef Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Fri, 29 May 2026 15:05:34 +0100 Subject: [PATCH 06/14] lint --- cmd/workflow/deploy/register_test.go | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cmd/workflow/deploy/register_test.go b/cmd/workflow/deploy/register_test.go index 0613998f..be843bd7 100644 --- a/cmd/workflow/deploy/register_test.go +++ b/cmd/workflow/deploy/register_test.go @@ -45,27 +45,27 @@ func TestWorkflowUpsert(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) defer simulatedEnvironment.Close() - ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newTestHandler(ctx, buf) - - wrc, err := handler.clientFactory.NewWorkflowRegistryV2Client(context.Background()) - require.NoError(t, err) - handler.wrc = wrc - - handler.inputs = tt.inputs - err = handler.ValidateInputs() - require.NoError(t, err) - - handler.workflowArtifact = &workflowArtifact{ - BinaryData: []byte("0x1234"), - ConfigData: []byte("config"), - WorkflowID: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - } - - onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") - require.NoError(t, err) - err = handler.upsert(onChain) - require.NoError(t, err) + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + handler := newTestHandler(ctx, buf) + + wrc, err := handler.clientFactory.NewWorkflowRegistryV2Client(context.Background()) + require.NoError(t, err) + handler.wrc = wrc + + handler.inputs = tt.inputs + err = handler.ValidateInputs() + require.NoError(t, err) + + handler.workflowArtifact = &workflowArtifact{ + BinaryData: []byte("0x1234"), + ConfigData: []byte("config"), + WorkflowID: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + } + + onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") + require.NoError(t, err) + err = handler.upsert(onChain) + require.NoError(t, err) }) } }) From d047162005a9d2330f6cc8fb3918141337064726 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Mon, 1 Jun 2026 17:01:06 +0100 Subject: [PATCH 07/14] Remove test handler --- .tool-versions | 2 +- cmd/workflow/deploy/artifacts.go | 9 +++-- cmd/workflow/deploy/artifacts_test.go | 25 +++++++------ cmd/workflow/deploy/auto_link.go | 32 ++++++++-------- cmd/workflow/deploy/auto_link_test.go | 9 +++-- cmd/workflow/deploy/compile.go | 5 ++- cmd/workflow/deploy/compile_test.go | 8 ++-- cmd/workflow/deploy/deploy.go | 37 ++++++------------- cmd/workflow/deploy/private_registry_test.go | 5 ++- cmd/workflow/deploy/register.go | 9 +++-- cmd/workflow/deploy/register_test.go | 4 +- .../deploy/registry_deploy_strategy.go | 11 +++--- .../registry_deploy_strategy_onchain.go | 22 +++++------ .../registry_deploy_strategy_private.go | 11 +++--- cmd/workflow/deploy/test_helpers_test.go | 17 --------- 15 files changed, 92 insertions(+), 114 deletions(-) delete mode 100644 cmd/workflow/deploy/test_helpers_test.go diff --git a/.tool-versions b/.tool-versions index 9a45258b..71d117e2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -golang 1.25.5 +golang 1.26.2 golangci-lint 2.11.2 goreleaser 2.0.1 python 3.10.5 diff --git a/cmd/workflow/deploy/artifacts.go b/cmd/workflow/deploy/artifacts.go index 7f7d6803..1bcb6467 100644 --- a/cmd/workflow/deploy/artifacts.go +++ b/cmd/workflow/deploy/artifacts.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "fmt" "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" @@ -8,8 +9,8 @@ import ( "github.com/smartcontractkit/cre-cli/internal/ui" ) -func (h *handler) uploadArtifacts() error { - if err := h.executionContext().Err(); err != nil { +func (h *handler) uploadArtifacts(ctx context.Context) error { + if err := ctx.Err(); err != nil { return err } @@ -50,7 +51,7 @@ func (h *handler) uploadArtifacts() error { if !binaryFromURL { ui.Success(fmt.Sprintf("Loaded binary from: %s", h.inputs.OutputPath)) binaryResp, err := storageClient.UploadArtifactWithRetriesAndGetURL( - h.executionContext(), workflowID, storageclient.ArtifactTypeBinary, binaryData, "application/octet-stream") + ctx, workflowID, storageclient.ArtifactTypeBinary, binaryData, "application/octet-stream") if err != nil { return fmt.Errorf("uploading binary artifact: %w", err) } @@ -63,7 +64,7 @@ func (h *handler) uploadArtifacts() error { ui.Success(fmt.Sprintf("Loaded config from: %s", h.inputs.ConfigPath)) var err error configURL, err = storageClient.UploadArtifactWithRetriesAndGetURL( - h.executionContext(), workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") + ctx, workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") if err != nil { return fmt.Errorf("uploading config artifact: %w", err) } diff --git a/cmd/workflow/deploy/artifacts_test.go b/cmd/workflow/deploy/artifacts_test.go index 1d18a759..a70e6405 100644 --- a/cmd/workflow/deploy/artifacts_test.go +++ b/cmd/workflow/deploy/artifacts_test.go @@ -2,6 +2,7 @@ package deploy import ( //nolint:gosec + "context" "encoding/json" "errors" "io" @@ -71,7 +72,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newTestHandler(ctx, buf) + h := newHandler(ctx, buf) h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -99,7 +100,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { ConfigData: []byte("configdata"), WorkflowID: "workflow-id", } - err := h.uploadArtifacts() + err := h.uploadArtifacts(context.Background()) require.NoError(t, err) require.Equal(t, "http://origin/get", h.inputs.BinaryURL) require.Equal(t, "http://origin/get", *h.inputs.ConfigURL) @@ -110,12 +111,12 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { ConfigData: nil, WorkflowID: "workflow-id", } - err = h.uploadArtifacts() + err = h.uploadArtifacts(context.Background()) require.NoError(t, err) // Error: workflowArtifact is nil h.workflowArtifact = nil - err = h.uploadArtifacts() + err = h.uploadArtifacts(context.Background()) require.ErrorContains(t, err, "workflowArtifact is nil") // Error: empty BinaryData @@ -124,7 +125,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { ConfigData: []byte("configdata"), WorkflowID: "workflow-id", } - err = h.uploadArtifacts() + err = h.uploadArtifacts(context.Background()) require.ErrorContains(t, err, "uploading binary artifact: content is empty for artifactType BINARY") // Error: workflowID is empty @@ -133,7 +134,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { ConfigData: []byte("configdata"), WorkflowID: "", } - err = h.uploadArtifacts() + err = h.uploadArtifacts(context.Background()) require.ErrorContains(t, err, "workflowID is empty") } @@ -147,7 +148,7 @@ func TestUploadArtifactToStorageService_OriginError(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) runtimeContext, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newTestHandler(runtimeContext, buf) + h := newHandler(runtimeContext, buf) h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -174,7 +175,7 @@ func TestUploadArtifactToStorageService_OriginError(t *testing.T) { ConfigData: []byte("configdata"), WorkflowID: "workflow-id", } - err := h.uploadArtifacts() + err := h.uploadArtifacts(context.Background()) require.ErrorContains(t, err, "upload to origin") } @@ -187,7 +188,7 @@ func TestUploadArtifactToStorageService_AlreadyExistsError(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) runtimeContext, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newTestHandler(runtimeContext, buf) + h := newHandler(runtimeContext, buf) h.inputs.WorkflowOwner = chainsim.TestAddress h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -240,7 +241,7 @@ func TestUploadArtifactToStorageService_AlreadyExistsError(t *testing.T) { ConfigData: []byte("configdata"), WorkflowID: "workflow-id", } - err := h.uploadArtifacts() + err := h.uploadArtifacts(context.Background()) require.NoError(t, err) require.Equal(t, "http://origin/get", h.inputs.BinaryURL) require.Equal(t, "http://origin/get", *h.inputs.ConfigURL) @@ -255,7 +256,7 @@ func TestUpload_UsesResolvedWorkflowOwnerForPresignedUrls(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) t.Cleanup(simulatedEnvironment.Close) ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newTestHandler(ctx, buf) + h := newHandler(ctx, buf) h.inputs.WorkflowOwner = "0x2222222222222222222222222222222222222222" h.inputs.WorkflowName = "test_workflow" h.inputs.DonFamily = "test_label" @@ -291,7 +292,7 @@ func TestUpload_UsesResolvedWorkflowOwnerForPresignedUrls(t *testing.T) { WorkflowID: "workflow-id", } - err := h.uploadArtifacts() + err := h.uploadArtifacts(context.Background()) require.NoError(t, err) require.NotEmpty(t, ownersUsed) for _, owner := range ownersUsed { diff --git a/cmd/workflow/deploy/auto_link.go b/cmd/workflow/deploy/auto_link.go index fbcadb0d..e350033f 100644 --- a/cmd/workflow/deploy/auto_link.go +++ b/cmd/workflow/deploy/auto_link.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "fmt" "strings" "time" @@ -21,10 +22,10 @@ const ( ) // ensureOwnerLinkedOrFail checks if the owner is linked and attempts auto-link if needed -func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) error { +func (h *handler) ensureOwnerLinkedOrFail(ctx context.Context, onChain *settings.OnChainRegistry) error { ownerAddr := common.HexToAddress(h.inputs.WorkflowOwner) - linked, err := h.wrc.IsOwnerLinked(h.executionContext(), ownerAddr) + linked, err := h.wrc.IsOwnerLinked(ctx, ownerAddr) if err != nil { return fmt.Errorf("failed to check owner link status: %w", err) } @@ -33,7 +34,7 @@ func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) err if linked { // Owner is linked on contract, now verify it's linked to the current user's account - linkedToCurrentUser, err := h.checkLinkStatusViaGraphQL(ownerAddr) + linkedToCurrentUser, err := h.checkLinkStatusViaGraphQL(ctx, ownerAddr) if err != nil { return fmt.Errorf("failed to validate key ownership: %w", err) } @@ -47,14 +48,14 @@ func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) err } ui.Dim(fmt.Sprintf("Owner not linked. Attempting auto-link: owner=%s", ownerAddr.Hex())) - if err := h.tryAutoLink(onChain); err != nil { + if err := h.tryAutoLink(ctx, onChain); err != nil { return fmt.Errorf("auto-link attempt failed: %w", err) } ui.Success(fmt.Sprintf("Auto-link successful: owner=%s", ownerAddr.Hex())) // Wait for linking process to complete - if err := h.waitForBackendLinkProcessing(ownerAddr); err != nil { + if err := h.waitForBackendLinkProcessing(ctx, ownerAddr); err != nil { return fmt.Errorf("linking process failed: %w", err) } @@ -62,17 +63,17 @@ func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) err } // autoLinkMSIGAndExit handles MSIG auto-link and exits if manual intervention is needed -func (h *handler) autoLinkMSIGAndExit(onChain *settings.OnChainRegistry) (halt bool, err error) { +func (h *handler) autoLinkMSIGAndExit(ctx context.Context, onChain *settings.OnChainRegistry) (halt bool, err error) { ownerAddr := common.HexToAddress(h.inputs.WorkflowOwner) - linked, err := h.wrc.IsOwnerLinked(h.executionContext(), ownerAddr) + linked, err := h.wrc.IsOwnerLinked(ctx, ownerAddr) if err != nil { return false, fmt.Errorf("failed to check owner link status: %w", err) } if linked { // Owner is linked on contract, now verify it's linked to the current user's account - linkedToCurrentUser, err := h.checkLinkStatusViaGraphQL(ownerAddr) + linkedToCurrentUser, err := h.checkLinkStatusViaGraphQL(ctx, ownerAddr) if err != nil { return false, fmt.Errorf("failed to validate MSIG key ownership: %w", err) } @@ -88,7 +89,7 @@ func (h *handler) autoLinkMSIGAndExit(onChain *settings.OnChainRegistry) (halt b ui.Dim(fmt.Sprintf("MSIG workflow owner link status: owner=%s, linked=%v", ownerAddr.Hex(), linked)) ui.Dim(fmt.Sprintf("MSIG owner: attempting auto-link... owner=%s", ownerAddr.Hex())) - if err := h.tryAutoLink(onChain); err != nil { + if err := h.tryAutoLink(ctx, onChain); err != nil { return false, fmt.Errorf("MSIG auto-link attempt failed: %w", err) } @@ -97,7 +98,7 @@ func (h *handler) autoLinkMSIGAndExit(onChain *settings.OnChainRegistry) (halt b } // tryAutoLink executes the auto-link process using the link-key command -func (h *handler) tryAutoLink(onChain *settings.OnChainRegistry) error { +func (h *handler) tryAutoLink(ctx context.Context, onChain *settings.OnChainRegistry) error { rtx := &runtime.Context{ Settings: h.settings, Credentials: h.credentials, @@ -106,7 +107,7 @@ func (h *handler) tryAutoLink(onChain *settings.OnChainRegistry) error { EnvironmentSet: h.environmentSet, } - return linkkey.Exec(h.executionContext(), rtx, linkkey.Inputs{ + return linkkey.Exec(ctx, rtx, linkkey.Inputs{ WorkflowOwner: h.inputs.WorkflowOwner, WorkflowRegistryContractAddress: onChain.Address(), WorkflowOwnerLabel: h.inputs.OwnerLabel, @@ -114,7 +115,7 @@ func (h *handler) tryAutoLink(onChain *settings.OnChainRegistry) error { } // checkLinkStatusViaGraphQL checks if the owner is linked and verified by querying the service -func (h *handler) checkLinkStatusViaGraphQL(ownerAddr common.Address) (bool, error) { +func (h *handler) checkLinkStatusViaGraphQL(ctx context.Context, ownerAddr common.Address) (bool, error) { const query = ` query { listWorkflowOwners(filters: { linkStatus: LINKED_ONLY }) { @@ -136,7 +137,7 @@ func (h *handler) checkLinkStatusViaGraphQL(ownerAddr common.Address) (bool, err } gql := graphqlclient.New(h.credentials, h.environmentSet, h.log) - if err := gql.Execute(h.executionContext(), req, &resp); err != nil { + if err := gql.Execute(ctx, req, &resp); err != nil { return false, fmt.Errorf("GraphQL query failed: %w", err) } @@ -168,7 +169,7 @@ func (h *handler) checkLinkStatusViaGraphQL(ownerAddr common.Address) (bool, err } // waitForBackendLinkProcessing polls the service until the link is processed -func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { +func (h *handler) waitForBackendLinkProcessing(ctx context.Context, ownerAddr common.Address) error { const maxAttempts = 5 const retryDelay = 3 * time.Second const initialBlockWait = 36 * time.Second // Wait for 3 block confirmations (~12s per block) @@ -180,7 +181,6 @@ func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { ui.Line() // Wait for 3 block confirmations before polling - ctx := h.executionContext() select { case <-time.After(initialBlockWait): case <-ctx.Done(): @@ -189,7 +189,7 @@ func (h *handler) waitForBackendLinkProcessing(ownerAddr common.Address) error { err := retry.Do( func() error { - linked, err := h.checkLinkStatusViaGraphQL(ownerAddr) + linked, err := h.checkLinkStatusViaGraphQL(ctx, ownerAddr) if err != nil { h.log.Warn().Err(err).Msg("Failed to check link status") return err // Return error to trigger retry diff --git a/cmd/workflow/deploy/auto_link_test.go b/cmd/workflow/deploy/auto_link_test.go index aa4d8b3f..f12d767b 100644 --- a/cmd/workflow/deploy/auto_link_test.go +++ b/cmd/workflow/deploy/auto_link_test.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -158,13 +159,13 @@ func TestCheckLinkStatusViaGraphQL(t *testing.T) { AuthType: credentials.AuthTypeApiKey, IsValidated: true, } - h := newTestHandler(ctx, nil) + h := newHandler(ctx, nil) h.inputs.WorkflowOwner = tt.ownerAddress h.environmentSet.GraphQLURL = server.URL + "/graphql" // Test the function ownerAddr := common.HexToAddress(tt.ownerAddress) - result, err := h.checkLinkStatusViaGraphQL(ownerAddr) + result, err := h.checkLinkStatusViaGraphQL(context.Background(), ownerAddr) if tt.expectError { assert.Error(t, err) @@ -329,13 +330,13 @@ func TestWaitForBackendLinkProcessing(t *testing.T) { AuthType: credentials.AuthTypeApiKey, IsValidated: true, } - h := newTestHandler(ctx, nil) + h := newHandler(ctx, nil) h.inputs.WorkflowOwner = tt.ownerAddress h.environmentSet.GraphQLURL = server.URL + "/graphql" // Test the function ownerAddr := common.HexToAddress(tt.ownerAddress) - err := h.waitForBackendLinkProcessing(ownerAddr) + err := h.waitForBackendLinkProcessing(context.Background(), ownerAddr) if tt.expectError { assert.Error(t, err) diff --git a/cmd/workflow/deploy/compile.go b/cmd/workflow/deploy/compile.go index a3dda9c5..ffa42bdf 100644 --- a/cmd/workflow/deploy/compile.go +++ b/cmd/workflow/deploy/compile.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "fmt" "os" @@ -9,7 +10,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/ui" ) -func (h *handler) Compile() error { +func (h *handler) Compile(ctx context.Context) error { if !h.validated { return fmt.Errorf("handler h.inputs not validated") } @@ -67,7 +68,7 @@ func (h *handler) Compile() error { h.runtimeContext.Workflow.Language = cmdcommon.GetWorkflowLanguage(workflowMainFile) } - wasmFile, err = cmdcommon.CompileWorkflowToWasm(h.executionContext(), resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + wasmFile, err = cmdcommon.CompileWorkflowToWasm(ctx, resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ StripSymbols: true, SkipTypeChecks: h.inputs.SkipTypeChecks, }) diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index ffd29c61..b8abe6ee 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -255,7 +255,7 @@ func createTestSettings(workflowOwnerAddress, workflowOwnerType, workflowName, w func runCompile(simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inputs, ownerType string) error { ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newTestHandler(ctx, buf) + handler := newHandler(ctx, buf) ctx.Settings = createTestSettings( inputs.WorkflowOwner, @@ -271,7 +271,7 @@ func runCompile(simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inpu return err } - return handler.Compile() + return handler.Compile(context.Background()) } // outputPathWithExtensions returns the path with .wasm.br.b64 appended as in Compile(). @@ -416,7 +416,7 @@ func TestCompileWithWasmPath(t *testing.T) { defer simulatedEnvironment.Close() ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newTestHandler(ctx, buf) + handler := newHandler(ctx, buf) ctx.Settings = createTestSettings( chainsim.TestAddress, constants.WorkflowOwnerTypeEOA, @@ -435,7 +435,7 @@ func TestCompileWithWasmPath(t *testing.T) { handler.validated = true // Compile() with URL wasm should return nil (skips compile entirely). - err := handler.Compile() + err := handler.Compile(context.Background()) require.NoError(t, err) }) diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index 87e7dd28..11952c21 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -73,8 +73,6 @@ type handler struct { // existingWorkflowStatus stores the status of an existing workflow when updating. // nil means this is a new workflow, otherwise it contains the current status (0=active, 1=paused). existingWorkflowStatus *uint8 - - execCtx context.Context } var defaultOutputPath = "./binary.wasm.br.b64" @@ -133,15 +131,6 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { return &h } -// executionContext returns the context from Execute(), or context.Background() -// when handler methods are invoked directly in unit tests. -func (h *handler) executionContext() context.Context { - if h.execCtx != nil { - return h.execCtx - } - return context.Background() -} - func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { var configURL *string if v.IsSet("config-url") { @@ -220,8 +209,6 @@ func (h *handler) Execute(ctx context.Context) error { return fmt.Errorf("handler inputs not validated") } - h.execCtx = ctx - deployAccess, err := h.credentials.GetDeploymentAccessStatus() if err != nil { return fmt.Errorf("failed to check deployment access: %w", err) @@ -231,27 +218,27 @@ func (h *handler) Execute(ctx context.Context) error { return h.accessRequester.PromptAndSubmitRequest(ctx) } - adapter, err := newRegistryDeployStrategy(h.runtimeContext.ResolvedRegistry, h) + adapter, err := newRegistryDeployStrategy(ctx, h.runtimeContext.ResolvedRegistry, h) if err != nil { return err } - if err := h.prepareArtifacts(); err != nil { + if err := h.prepareArtifacts(ctx); err != nil { return err } - if err := h.executionContext().Err(); err != nil { + if err := ctx.Err(); err != nil { return err } - if err := adapter.RunPreDeployChecks(); err != nil { + if err := adapter.RunPreDeployChecks(ctx); err != nil { if errors.Is(err, errDeployHalted) { return nil } return err } - exists, existingStatus, err := adapter.CheckWorkflowExists( + exists, existingStatus, err := adapter.CheckWorkflowExists(ctx, h.inputs.WorkflowOwner, h.inputs.WorkflowName, h.inputs.WorkflowTag, @@ -272,11 +259,11 @@ func (h *handler) Execute(ctx context.Context) error { ui.Line() ui.Dim("Uploading files...") - if err := h.uploadArtifacts(); err != nil { + if err := h.uploadArtifacts(ctx); err != nil { return fmt.Errorf("failed to upload workflow: %w", err) } - err = adapter.Upsert() + err = adapter.Upsert(ctx) if err == nil { warnIfPausedWorkflowUpdate(h.existingWorkflowStatus) } @@ -286,8 +273,8 @@ func (h *handler) Execute(ctx context.Context) error { // prepareArtifacts handles compile/fetch, artifact preparation, and hashing. // Artifact upload is deferred to the deploy service so it runs after any // existing-workflow update confirmation. -func (h *handler) prepareArtifacts() error { - if err := h.executionContext().Err(); err != nil { +func (h *handler) prepareArtifacts(ctx context.Context) error { + if err := ctx.Err(); err != nil { return err } @@ -302,14 +289,14 @@ func (h *handler) prepareArtifacts() error { if cmdcommon.IsURL(h.inputs.WasmPath) { h.inputs.BinaryURL = h.inputs.WasmPath ui.Dim("Fetching binary from URL for workflow ID computation...") - fetched, err := cmdcommon.FetchURL(h.executionContext(), h.inputs.WasmPath) + fetched, err := cmdcommon.FetchURL(ctx, h.inputs.WasmPath) if err != nil { return fmt.Errorf("failed to fetch binary from URL: %w", err) } h.urlBinaryData = fetched ui.Success(fmt.Sprintf("Using binary URL: %s", h.inputs.WasmPath)) } else { - if err := h.Compile(); err != nil { + if err := h.Compile(ctx); err != nil { return fmt.Errorf("failed to compile workflow: %w", err) } } @@ -319,7 +306,7 @@ func (h *handler) prepareArtifacts() error { h.inputs.ConfigURL = &url h.inputs.ConfigPath = "" ui.Dim("Fetching config from URL for workflow ID computation...") - fetched, err := cmdcommon.FetchURL(h.executionContext(), url) + fetched, err := cmdcommon.FetchURL(ctx, url) if err != nil { return fmt.Errorf("failed to fetch config from URL: %w", err) } diff --git a/cmd/workflow/deploy/private_registry_test.go b/cmd/workflow/deploy/private_registry_test.go index e7f839a6..098b669b 100644 --- a/cmd/workflow/deploy/private_registry_test.go +++ b/cmd/workflow/deploy/private_registry_test.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "encoding/base64" "encoding/hex" "encoding/json" @@ -302,7 +303,7 @@ func TestCheckWorkflowExists_PrivateRegistry(t *testing.T) { defer simulatedEnvironment.Close() ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - h := newTestHandler(ctx, buf) + h := newHandler(ctx, buf) h.credentials = makeAPIKeyCredentials(t) gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { @@ -314,7 +315,7 @@ func TestCheckWorkflowExists_PrivateRegistry(t *testing.T) { h.environmentSet.GraphQLURL = gqlServer.URL strategy := newPrivateRegistryDeployStrategy(h) - exists, status, err := strategy.CheckWorkflowExists("", "jnowak-workflow-test-v5", "", tt.workflowID) + exists, status, err := strategy.CheckWorkflowExists(context.Background(), "", "jnowak-workflow-test-v5", "", tt.workflowID) if tt.wantErr { require.Error(t, err) if tt.errMsg != "" { diff --git a/cmd/workflow/deploy/register.go b/cmd/workflow/deploy/register.go index b65aad3a..1d884bde 100644 --- a/cmd/workflow/deploy/register.go +++ b/cmd/workflow/deploy/register.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "encoding/hex" "fmt" "time" @@ -14,7 +15,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/ui" ) -func (h *handler) upsert(onChain *settings.OnChainRegistry) error { +func (h *handler) upsert(ctx context.Context, onChain *settings.OnChainRegistry) error { if !h.validated { return fmt.Errorf("handler inputs not validated") } @@ -23,7 +24,7 @@ func (h *handler) upsert(onChain *settings.OnChainRegistry) error { if err != nil { return err } - return h.handleUpsert(params, onChain) + return h.handleUpsert(ctx, params, onChain) } func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, error) { @@ -53,11 +54,11 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er }, nil } -func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters, onChain *settings.OnChainRegistry) error { +func (h *handler) handleUpsert(ctx context.Context, params client.RegisterWorkflowV2Parameters, onChain *settings.OnChainRegistry) error { workflowName := h.inputs.WorkflowName workflowTag := h.inputs.WorkflowTag h.log.Debug().Interface("Workflow parameters", params).Msg("Registering workflow...") - txOut, err := h.wrc.UpsertWorkflow(h.executionContext(), params) + txOut, err := h.wrc.UpsertWorkflow(ctx, params) if err != nil { return fmt.Errorf("failed to register workflow: %w", err) } diff --git a/cmd/workflow/deploy/register_test.go b/cmd/workflow/deploy/register_test.go index be843bd7..60e296ea 100644 --- a/cmd/workflow/deploy/register_test.go +++ b/cmd/workflow/deploy/register_test.go @@ -46,7 +46,7 @@ func TestWorkflowUpsert(t *testing.T) { defer simulatedEnvironment.Close() ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newTestHandler(ctx, buf) + handler := newHandler(ctx, buf) wrc, err := handler.clientFactory.NewWorkflowRegistryV2Client(context.Background()) require.NoError(t, err) @@ -64,7 +64,7 @@ func TestWorkflowUpsert(t *testing.T) { onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") require.NoError(t, err) - err = handler.upsert(onChain) + err = handler.upsert(context.Background(), onChain) require.NoError(t, err) }) } diff --git a/cmd/workflow/deploy/registry_deploy_strategy.go b/cmd/workflow/deploy/registry_deploy_strategy.go index 34a5c431..419d4c41 100644 --- a/cmd/workflow/deploy/registry_deploy_strategy.go +++ b/cmd/workflow/deploy/registry_deploy_strategy.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "errors" "github.com/smartcontractkit/cre-cli/internal/settings" @@ -22,23 +23,23 @@ type registryDeployStrategy interface { // RunPreDeployChecks validates readiness and runs registry-specific // prechecks (ownership linking, duplicate detection, etc.). // Return errDeployHalted to stop the deploy without returning an error. - RunPreDeployChecks() error + RunPreDeployChecks(ctx context.Context) error // CheckWorkflowExists returns whether a same-name workflow exists for this // registry target and includes the existing workflow status for updates. // When the existing workflow ID matches workflowID, exists is true and // errWorkflowUnchanged is returned to block redeployment of identical artifacts. - CheckWorkflowExists(workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) + CheckWorkflowExists(ctx context.Context, workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) // Upsert registers or updates the workflow in the target registry // and displays the result. - Upsert() error + Upsert(ctx context.Context) error } // newRegistryDeployStrategy returns the appropriate strategy for the given target. -func newRegistryDeployStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryDeployStrategy, error) { +func newRegistryDeployStrategy(ctx context.Context, resolvedRegistry settings.ResolvedRegistry, h *handler) (registryDeployStrategy, error) { if resolvedRegistry.Type() == settings.RegistryTypeOffChain { return newPrivateRegistryDeployStrategy(h), nil } - return newOnchainRegistryDeployStrategy(h) + return newOnchainRegistryDeployStrategy(ctx, h) } diff --git a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go index 1ed63bce..cd1eafc8 100644 --- a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go +++ b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go @@ -24,7 +24,7 @@ type onchainRegistryDeployStrategy struct { initErr error } -func newOnchainRegistryDeployStrategy(h *handler) (*onchainRegistryDeployStrategy, error) { +func newOnchainRegistryDeployStrategy(ctx context.Context, h *handler) (*onchainRegistryDeployStrategy, error) { onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "deploy") if err != nil { return nil, err @@ -34,7 +34,7 @@ func newOnchainRegistryDeployStrategy(h *handler) (*onchainRegistryDeployStrateg a.wg.Add(1) go func() { defer a.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client(h.executionContext()) + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client(ctx) if err != nil { a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) return @@ -60,10 +60,10 @@ func waitWithContext(ctx context.Context, wg *sync.WaitGroup) error { } } -func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { +func (a *onchainRegistryDeployStrategy) RunPreDeployChecks(ctx context.Context) error { h := a.h - if err := waitWithContext(a.h.executionContext(), &a.wg); err != nil { + if err := waitWithContext(ctx, &a.wg); err != nil { return err } if a.initErr != nil { @@ -73,7 +73,7 @@ func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { ui.Line() ui.Dim("Verifying ownership...") if h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerType == constants.WorkflowOwnerTypeMSIG { - halt, err := h.autoLinkMSIGAndExit(a.onChain) + halt, err := h.autoLinkMSIGAndExit(ctx, a.onChain) if err != nil { return fmt.Errorf("failed to check/handle MSIG owner link status: %w", err) } @@ -81,7 +81,7 @@ func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { return errDeployHalted } } else { - if err := h.ensureOwnerLinkedOrFail(a.onChain); err != nil { + if err := h.ensureOwnerLinkedOrFail(ctx, a.onChain); err != nil { return err } } @@ -89,8 +89,8 @@ func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { return nil } -func (a *onchainRegistryDeployStrategy) CheckWorkflowExists(workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) { - workflow, err := a.wrc.GetWorkflow(a.h.executionContext(), common.HexToAddress(workflowOwner), workflowName, workflowTag) +func (a *onchainRegistryDeployStrategy) CheckWorkflowExists(ctx context.Context, workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) { + workflow, err := a.wrc.GetWorkflow(ctx, common.HexToAddress(workflowOwner), workflowName, workflowTag) if err != nil { return false, nil, err } @@ -106,11 +106,11 @@ func (a *onchainRegistryDeployStrategy) CheckWorkflowExists(workflowOwner, workf return false, nil, nil } -func (a *onchainRegistryDeployStrategy) Upsert() error { +func (a *onchainRegistryDeployStrategy) Upsert(ctx context.Context) error { h := a.h if err := checkUserDonLimitBeforeDeploy( - h.executionContext(), + ctx, a.wrc, a.wrc, common.HexToAddress(h.inputs.WorkflowOwner), @@ -124,7 +124,7 @@ func (a *onchainRegistryDeployStrategy) Upsert() error { ui.Line() ui.Dim("Preparing deployment transaction...") - if err := h.upsert(a.onChain); err != nil { + if err := h.upsert(ctx, a.onChain); err != nil { return fmt.Errorf("failed to register workflow: %w", err) } return nil diff --git a/cmd/workflow/deploy/registry_deploy_strategy_private.go b/cmd/workflow/deploy/registry_deploy_strategy_private.go index 2a77d2b9..1369fd8a 100644 --- a/cmd/workflow/deploy/registry_deploy_strategy_private.go +++ b/cmd/workflow/deploy/registry_deploy_strategy_private.go @@ -1,6 +1,7 @@ package deploy import ( + "context" "fmt" "strings" @@ -27,14 +28,14 @@ func (a *privateRegistryDeployStrategy) ensureClient() { } } -func (a *privateRegistryDeployStrategy) RunPreDeployChecks() error { +func (a *privateRegistryDeployStrategy) RunPreDeployChecks(_ context.Context) error { return nil } -func (a *privateRegistryDeployStrategy) CheckWorkflowExists(_, workflowName, _, workflowID string) (bool, *uint8, error) { +func (a *privateRegistryDeployStrategy) CheckWorkflowExists(ctx context.Context, _, workflowName, _, workflowID string) (bool, *uint8, error) { a.ensureClient() - workflow, err := a.prc.GetWorkflowByName(a.h.executionContext(), workflowName) + workflow, err := a.prc.GetWorkflowByName(ctx, workflowName) if err == nil { if workflow.WorkflowID == workflowID { return true, offchainStatusToUint8(workflow.Status), fmt.Errorf("workflow with id %s is already registered and unchanged; re-deployment skipped: %w", workflowID, errWorkflowUnchanged) @@ -48,7 +49,7 @@ func (a *privateRegistryDeployStrategy) CheckWorkflowExists(_, workflowName, _, return false, nil, err } -func (a *privateRegistryDeployStrategy) Upsert() error { +func (a *privateRegistryDeployStrategy) Upsert(ctx context.Context) error { a.ensureClient() h := a.h @@ -57,7 +58,7 @@ func (a *privateRegistryDeployStrategy) Upsert() error { ui.Line() ui.Dim(fmt.Sprintf("Registering workflow in private registry (workflowID: %s)...", input.WorkflowID)) - result, err := a.prc.UpsertWorkflowInRegistry(a.h.executionContext(), input) + result, err := a.prc.UpsertWorkflowInRegistry(ctx, input) if err != nil { return fmt.Errorf("failed to register workflow in private registry: %w", err) } diff --git a/cmd/workflow/deploy/test_helpers_test.go b/cmd/workflow/deploy/test_helpers_test.go deleted file mode 100644 index e321e3c6..00000000 --- a/cmd/workflow/deploy/test_helpers_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package deploy - -import ( - "context" - "io" - - "github.com/smartcontractkit/cre-cli/internal/runtime" -) - -// newTestHandler returns a handler suitable for unit tests that call handler -// methods directly instead of going through Execute(). It pre-sets execCtx so -// cancellation-aware code paths behave like a normal CLI invocation. -func newTestHandler(ctx *runtime.Context, stdin io.Reader) *handler { - h := newHandler(ctx, stdin) - h.execCtx = context.Background() - return h -} From 9154b499a9df886a348526f319b986072dd0cbde Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 00:32:55 +0100 Subject: [PATCH 08/14] Add capability registry address to user context --- internal/tenantctx/tenantctx.go | 48 +++++++++++++++---- internal/tenantctx/tenantctx_test.go | 24 ++++++++++ .../workflow_private_registry.go | 16 +++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/internal/tenantctx/tenantctx.go b/internal/tenantctx/tenantctx.go index ec3f2064..6d45c189 100644 --- a/internal/tenantctx/tenantctx.go +++ b/internal/tenantctx/tenantctx.go @@ -38,13 +38,20 @@ type Forwarder struct { Address string `yaml:"address" json:"address"` } +// OnChainContract is a chain selector and contract address pair. +type OnChainContract struct { + ChainSelector uint64 `yaml:"chain_selector" json:"chainSelector"` + Address string `yaml:"address" json:"address"` +} + // EnvironmentContext holds user context for a single CLI environment. type EnvironmentContext struct { - TenantID string `yaml:"tenant_id"` - DefaultDonFamily string `yaml:"default_don_family"` - VaultGatewayURL string `yaml:"vault_gateway_url"` - Registries []*Registry `yaml:"registries"` - Forwarders []Forwarder `yaml:"forwarders,omitempty"` + TenantID string `yaml:"tenant_id"` + DefaultDonFamily string `yaml:"default_don_family"` + VaultGatewayURL string `yaml:"vault_gateway_url"` + CapabilitiesRegistry *OnChainContract `yaml:"capabilities_registry,omitempty"` + Registries []*Registry `yaml:"registries"` + Forwarders []Forwarder `yaml:"forwarders,omitempty"` } type gqlForwarder struct { @@ -52,11 +59,17 @@ type gqlForwarder struct { Address string `json:"address"` } +type gqlOnChainContract struct { + ChainSelector json.RawMessage `json:"chainSelector"` + Address string `json:"address"` +} + type getTenantConfigResponse struct { GetTenantConfig struct { - TenantID string `json:"tenantId"` - DefaultDonFamily string `json:"defaultDonFamily"` - VaultGatewayURL string `json:"vaultGatewayUrl"` + TenantID string `json:"tenantId"` + DefaultDonFamily string `json:"defaultDonFamily"` + VaultGatewayURL string `json:"vaultGatewayUrl"` + CapabilitiesRegistry gqlOnChainContract `json:"capabilitiesRegistry"` Registries []struct { ID string `json:"id"` Label string `json:"label"` @@ -74,6 +87,10 @@ const getTenantConfigQuery = `query GetTenantConfig { tenantId defaultDonFamily vaultGatewayUrl + capabilitiesRegistry { + chainSelector + address + } registries { id label @@ -142,11 +159,24 @@ func FetchAndWriteContext(ctx context.Context, gqlClient *graphqlclient.Client, forwarders = append(forwarders, Forwarder{ChainSelector: sel, Address: addr}) } + capRegSel, err := parseChainSelectorJSON(tc.CapabilitiesRegistry.ChainSelector) + if err != nil { + return fmt.Errorf("invalid capabilitiesRegistry chainSelector: %w", err) + } + capRegAddr := strings.TrimSpace(tc.CapabilitiesRegistry.Address) + if capRegAddr == "" { + return fmt.Errorf("capabilitiesRegistry address is empty") + } + envCtx := &EnvironmentContext{ TenantID: tc.TenantID, DefaultDonFamily: tc.DefaultDonFamily, VaultGatewayURL: tc.VaultGatewayURL, - Registries: registries, + CapabilitiesRegistry: &OnChainContract{ + ChainSelector: capRegSel, + Address: capRegAddr, + }, + Registries: registries, Forwarders: forwarders, } diff --git a/internal/tenantctx/tenantctx_test.go b/internal/tenantctx/tenantctx_test.go index 683c2f20..62f11480 100644 --- a/internal/tenantctx/tenantctx_test.go +++ b/internal/tenantctx/tenantctx_test.go @@ -43,6 +43,10 @@ func gqlResponseOnChainAndPrivate() map[string]any { "tenantId": "42", "defaultDonFamily": "zone-a", "vaultGatewayUrl": "https://gateway.example.com/", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "16015286601757825753", + "address": "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, "registries": []any{ map[string]any{ "id": "ethereum-testnet-sepolia", @@ -77,6 +81,10 @@ func gqlResponsePrivateOnly() map[string]any { "tenantId": "99", "defaultDonFamily": "zone-b", "vaultGatewayUrl": "https://gateway-private.example.com/", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "5009297550715157269", + "address": "0x76c9cf548b4179F8901cda1f8623568b58215E62", + }, "registries": []any{ map[string]any{ "id": "private", @@ -177,6 +185,16 @@ func TestFetchAndWriteContext_OnChainAndPrivate(t *testing.T) { if f.Address != "0x15fC6ae953E024d975e77382eEeC56A9101f9F88" { t.Errorf("forwarder address = %q, want Sepolia mock forwarder", f.Address) } + + if envCtx.CapabilitiesRegistry == nil { + t.Fatal("expected capabilitiesRegistry to be populated") + } + if envCtx.CapabilitiesRegistry.ChainSelector != 16015286601757825753 { + t.Errorf("capabilitiesRegistry chain selector = %d, want %d", envCtx.CapabilitiesRegistry.ChainSelector, uint64(16015286601757825753)) + } + if envCtx.CapabilitiesRegistry.Address != "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f" { + t.Errorf("capabilitiesRegistry address = %q, want staging mainline cap reg", envCtx.CapabilitiesRegistry.Address) + } } func TestFetchAndWriteContext_PrivateOnly(t *testing.T) { @@ -205,6 +223,12 @@ func TestFetchAndWriteContext_PrivateOnly(t *testing.T) { if len(envCtx.Forwarders) != 0 { t.Errorf("expected 0 forwarders, got %d", len(envCtx.Forwarders)) } + if envCtx.CapabilitiesRegistry == nil { + t.Fatal("expected capabilitiesRegistry to be populated") + } + if envCtx.CapabilitiesRegistry.ChainSelector != 5009297550715157269 { + t.Errorf("capabilitiesRegistry chain selector = %d, want %d", envCtx.CapabilitiesRegistry.ChainSelector, uint64(5009297550715157269)) + } } func TestParseChainSelectorJSON(t *testing.T) { diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 92720350..b2004cb9 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -110,6 +110,10 @@ func workflowDeployPrivateRegistry(t *testing.T, tc TestConfig) string { "tenantId": "42", "defaultDonFamily": "test-don", "vaultGatewayUrl": "https://vault.example.test", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + }, "registries": []map[string]any{ { "id": "reg-test", @@ -325,6 +329,10 @@ func workflowPausePrivateRegistry(t *testing.T, tc TestConfig) string { "tenantId": "42", "defaultDonFamily": "test-don", "vaultGatewayUrl": "https://vault.example.test", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + }, "registries": []map[string]any{ { "id": "reg-test", @@ -495,6 +503,10 @@ func workflowActivatePrivateRegistry(t *testing.T, tc TestConfig) string { "tenantId": "42", "defaultDonFamily": "test-don", "vaultGatewayUrl": "https://vault.example.test", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + }, "registries": []map[string]any{ { "id": "reg-test", @@ -665,6 +677,10 @@ func workflowDeletePrivateRegistry(t *testing.T, tc TestConfig) string { "tenantId": "42", "defaultDonFamily": "test-don", "vaultGatewayUrl": "https://vault.example.test", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + }, "registries": []map[string]any{ { "id": "reg-test", From c3f0c3ca669ad5fd3d32a8cc19a6e8637e3508d6 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 11:41:30 +0100 Subject: [PATCH 09/14] Lint --- cmd/secrets/common/browser_flow.go | 2 +- cmd/workflow/hash/hash.go | 2 +- internal/tenantctx/tenantctx.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/secrets/common/browser_flow.go b/cmd/secrets/common/browser_flow.go index 8fe264af..fbb00e24 100644 --- a/cmd/secrets/common/browser_flow.go +++ b/cmd/secrets/common/browser_flow.go @@ -224,7 +224,7 @@ func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method s }) var exchangeResp struct { ExchangeAuthCodeToToken struct { - AccessToken string `json:"accessToken"` + AccessToken string `json:"accessToken"` // #nosec G117 -- OAuth token exchange response field ExpiresIn int `json:"expiresIn"` } `json:"exchangeAuthCodeToToken"` } diff --git a/cmd/workflow/hash/hash.go b/cmd/workflow/hash/hash.go index 7efdb434..eb7aed4c 100644 --- a/cmd/workflow/hash/hash.go +++ b/cmd/workflow/hash/hash.go @@ -24,7 +24,7 @@ type Inputs struct { WorkflowName string WorkflowPath string OwnerFromSettings string - PrivateKey string + PrivateKey string // #nosec G117 -- workflow owner private key flag value, not persisted SkipTypeChecks bool RegistryType settings.RegistryType DerivedOwner string diff --git a/internal/tenantctx/tenantctx.go b/internal/tenantctx/tenantctx.go index 6d45c189..cc810dce 100644 --- a/internal/tenantctx/tenantctx.go +++ b/internal/tenantctx/tenantctx.go @@ -66,11 +66,11 @@ type gqlOnChainContract struct { type getTenantConfigResponse struct { GetTenantConfig struct { - TenantID string `json:"tenantId"` - DefaultDonFamily string `json:"defaultDonFamily"` - VaultGatewayURL string `json:"vaultGatewayUrl"` + TenantID string `json:"tenantId"` + DefaultDonFamily string `json:"defaultDonFamily"` + VaultGatewayURL string `json:"vaultGatewayUrl"` CapabilitiesRegistry gqlOnChainContract `json:"capabilitiesRegistry"` - Registries []struct { + Registries []struct { ID string `json:"id"` Label string `json:"label"` Type string `json:"type"` @@ -177,7 +177,7 @@ func FetchAndWriteContext(ctx context.Context, gqlClient *graphqlclient.Client, Address: capRegAddr, }, Registries: registries, - Forwarders: forwarders, + Forwarders: forwarders, } contextMap := map[string]*EnvironmentContext{ From f33073441925f9378dc117af559ce5795fe3f255 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Wed, 3 Jun 2026 23:50:38 +0100 Subject: [PATCH 10/14] Add RPC resolution and consent for capability registry --- cmd/client/eth_client.go | 1 + cmd/secrets/common/handler.go | 15 ++ cmd/secrets/common/vault_validation.go | 87 ++++++++++++ cmd/secrets/common/vault_validation_test.go | 118 ++++++++++++++++ cmd/secrets/delete/delete.go | 4 + cmd/secrets/execute/execute.go | 4 + cmd/secrets/list/list.go | 4 + internal/rpc/chainid.go | 57 ++++++++ internal/rpc/chainid_test.go | 60 ++++++++ internal/rpc/url.go | 23 ++++ internal/rpc/url_test.go | 37 +++++ .../settings/capabilities_registry_rpc.go | 48 +++++++ .../capabilities_registry_rpc_test.go | 130 ++++++++++++++++++ internal/settings/settings_get.go | 3 + internal/settings/workflow_settings.go | 19 +-- 15 files changed, 595 insertions(+), 15 deletions(-) create mode 100644 cmd/secrets/common/vault_validation.go create mode 100644 cmd/secrets/common/vault_validation_test.go create mode 100644 internal/rpc/chainid.go create mode 100644 internal/rpc/chainid_test.go create mode 100644 internal/rpc/url.go create mode 100644 internal/rpc/url_test.go create mode 100644 internal/settings/capabilities_registry_rpc.go create mode 100644 internal/settings/capabilities_registry_rpc_test.go diff --git a/cmd/client/eth_client.go b/cmd/client/eth_client.go index 7ee1a785..22076dcc 100644 --- a/cmd/client/eth_client.go +++ b/cmd/client/eth_client.go @@ -183,6 +183,7 @@ func readSethConfigFromFile(configPath string) (*seth.Config, error) { return &sethConfig, nil } +// TODO(DEVSVCS-5178) func getChainID(rpcURL string) (uint64, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() diff --git a/cmd/secrets/common/handler.go b/cmd/secrets/common/handler.go index b30352c1..48c9d209 100644 --- a/cmd/secrets/common/handler.go +++ b/cmd/secrets/common/handler.go @@ -19,6 +19,7 @@ import ( "github.com/google/uuid" "github.com/machinebox/graphql" "github.com/rs/zerolog" + "github.com/spf13/viper" "google.golang.org/protobuf/encoding/protojson" "gopkg.in/yaml.v2" @@ -38,6 +39,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" @@ -67,6 +69,8 @@ type SecretsYamlConfig struct { type Handler struct { Log *zerolog.Logger ClientFactory client.Factory + Viper *viper.Viper + TenantContext *tenantctx.EnvironmentContext SecretsFilePath string PrivateKey *ecdsa.PrivateKey OwnerAddress string @@ -78,6 +82,11 @@ type Handler struct { Credentials *credentials.Credentials Settings *settings.Settings execCtx context.Context + + vaultValidationDecided bool + skipVaultValidation bool + capRegRPCURL string + capRegChainName string } // NewHandler creates a new handler instance. @@ -100,6 +109,8 @@ func NewHandler(execCtx context.Context, ctx *runtime.Context, secretsFilePath, h := &Handler{ Log: ctx.Logger, ClientFactory: ctx.ClientFactory, + Viper: ctx.Viper, + TenantContext: ctx.TenantContext, SecretsFilePath: secretsFilePath, PrivateKey: pk, OwnerAddress: ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, @@ -427,6 +438,10 @@ func (h *Handler) Execute( defer ZeroUpsertSecretValues(inputs) h.execCtx = ctx + if _, err := h.EnsureVaultValidationOrConsent(ctx); err != nil { + return err + } + if IsBrowserFlow(secretsAuth) { return h.executeBrowserUpsert(ctx, inputs, method) } diff --git a/cmd/secrets/common/vault_validation.go b/cmd/secrets/common/vault_validation.go new file mode 100644 index 00000000..7682a417 --- /dev/null +++ b/cmd/secrets/common/vault_validation.go @@ -0,0 +1,87 @@ +package common + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +const vaultValidationSkippedWarning = "Vault gateway validation skipped; the encryption key and response signatures will not be verified independently of the gateway." + +// EnsureVaultValidationOrConsent resolves CapabilitiesRegistry RPC settings and either +// enables on-chain validation (skipValidation=false) or obtains explicit consent to +// proceed without validation. The result is cached for the lifetime of the Handler so +// encrypt and response parsing in the same command only prompt once. +func (h *Handler) EnsureVaultValidationOrConsent(ctx context.Context) (skipValidation bool, err error) { + _ = ctx + if h.vaultValidationDecided { + return h.skipVaultValidation, nil + } + + rpcURL, chainName, ok, err := settings.ResolveCapabilitiesRegistryRPC(h.Viper, h.TenantContext) + if err != nil { + return false, err + } + + if ok { + h.capRegRPCURL = rpcURL + h.capRegChainName = chainName + h.skipVaultValidation = false + h.vaultValidationDecided = true + return false, nil + } + + if h.Viper.GetBool(settings.Flags.NonInteractive.Name) && !h.Viper.GetBool(settings.Flags.SkipConfirmation.Name) { + ui.ErrorWithSuggestions( + fmt.Sprintf("Vault gateway validation requires an RPC for %s in your project settings", chainName), + []string{"--yes"}, + ) + return false, fmt.Errorf("missing RPC for capabilities registry chain %q", chainName) + } + + if h.Viper.GetBool(settings.Flags.SkipConfirmation.Name) { + ui.Warning(vaultValidationSkippedWarning) + h.capRegChainName = chainName + h.skipVaultValidation = true + h.vaultValidationDecided = true + return true, nil + } + + prompt := fmt.Sprintf( + "Vault gateway responses cannot be validated without an RPC for %s in your project settings. Proceeding without validation means the CLI cannot verify the encryption key or DON signatures independently of the gateway. Proceed anyway?", + chainName, + ) + proceed, err := ui.Confirm(prompt) + if err != nil { + return false, err + } + if !proceed { + return false, fmt.Errorf("aborted: vault gateway validation requires an RPC for %q", chainName) + } + + ui.Warning(vaultValidationSkippedWarning) + h.capRegChainName = chainName + h.skipVaultValidation = true + h.vaultValidationDecided = true + return true, nil +} + +// SkipVaultValidation reports whether the current command opted out of on-chain validation. +func (h *Handler) SkipVaultValidation() bool { + return h.skipVaultValidation +} + +// CapabilitiesRegistryRPC returns the validated RPC URL when validation is enabled. +func (h *Handler) CapabilitiesRegistryRPC() (rpcURL string, ok bool) { + if h.skipVaultValidation || h.capRegRPCURL == "" { + return "", false + } + return h.capRegRPCURL, true +} + +// CapabilitiesRegistryChainName returns the chain name for the tenant CapabilitiesRegistry. +func (h *Handler) CapabilitiesRegistryChainName() string { + return h.capRegChainName +} diff --git a/cmd/secrets/common/vault_validation_test.go b/cmd/secrets/common/vault_validation_test.go new file mode 100644 index 00000000..a0d38e49 --- /dev/null +++ b/cmd/secrets/common/vault_validation_test.go @@ -0,0 +1,118 @@ +package common + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +func testHandlerWithCapReg(t *testing.T, v *viper.Viper, tenantCtx *tenantctx.EnvironmentContext) *Handler { + t.Helper() + h, _, _ := newMockHandler(t) + h.Viper = v + h.TenantContext = tenantCtx + return h +} + +func TestEnsureVaultValidationOrConsent_RPCConfigured(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + w.Header().Set("Content-Type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": req.ID, + "result": "0xaa36a7", + })) + })) + t.Cleanup(server.Close) + + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set("staging.rpcs", []map[string]string{ + {"chain-name": "ethereum-testnet-sepolia", "url": server.URL}, + }) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: 16015286601757825753, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + h := testHandlerWithCapReg(t, v, tenantCtx) + + skip, err := h.EnsureVaultValidationOrConsent(context.Background()) + require.NoError(t, err) + require.False(t, skip) + require.False(t, h.SkipVaultValidation()) + + rpcURL, ok := h.CapabilitiesRegistryRPC() + require.True(t, ok) + require.Equal(t, server.URL, rpcURL) + + skipCached, err := h.EnsureVaultValidationOrConsent(context.Background()) + require.NoError(t, err) + require.False(t, skipCached) +} + +func TestEnsureVaultValidationOrConsent_SkipConfirmationWithoutRPC(t *testing.T) { + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set(settings.Flags.SkipConfirmation.Name, true) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: 16015286601757825753, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + h := testHandlerWithCapReg(t, v, tenantCtx) + + skip, err := h.EnsureVaultValidationOrConsent(context.Background()) + require.NoError(t, err) + require.True(t, skip) + require.True(t, h.SkipVaultValidation()) + _, ok := h.CapabilitiesRegistryRPC() + require.False(t, ok) +} + +func TestEnsureVaultValidationOrConsent_NonInteractiveWithoutRPC(t *testing.T) { + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set(settings.Flags.NonInteractive.Name, true) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: 16015286601757825753, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + h := testHandlerWithCapReg(t, v, tenantCtx) + + _, err := h.EnsureVaultValidationOrConsent(context.Background()) + require.Error(t, err) + require.Contains(t, err.Error(), "missing RPC for capabilities registry chain") +} + +func TestEnsureVaultValidationOrConsent_MissingCapabilitiesRegistry(t *testing.T) { + v := viper.New() + h := testHandlerWithCapReg(t, v, &tenantctx.EnvironmentContext{}) + + _, err := h.EnsureVaultValidationOrConsent(context.Background()) + require.Error(t, err) + require.Contains(t, err.Error(), "capabilities registry is not configured") +} diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index 57126591..56165534 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -117,6 +117,10 @@ func New(ctx *runtime.Context) *cobra.Command { // - MSIG step 1: build request, compute digest, write bundle, print steps // - EOA: allowlist if needed, then POST to gateway func Execute(ctx context.Context, h *common.Handler, inputs DeleteSecretsInputs, duration time.Duration, secretsAuth string) error { + if _, err := h.EnsureVaultValidationOrConsent(ctx); err != nil { + return err + } + if !common.IsBrowserFlow(secretsAuth) { if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil { return err diff --git a/cmd/secrets/execute/execute.go b/cmd/secrets/execute/execute.go index 8c89f303..697199d6 100644 --- a/cmd/secrets/execute/execute.go +++ b/cmd/secrets/execute/execute.go @@ -69,6 +69,10 @@ func New(ctx *runtime.Context) *cobra.Command { return err } + if _, err := h.EnsureVaultValidationOrConsent(cmd.Context()); err != nil { + return err + } + ownerAddr := ethcommon.HexToAddress(h.OwnerAddress) allowlisted, err := h.Wrc.IsRequestAllowlisted(cmd.Context(), ownerAddr, digest) diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index 6a4a58b8..bc370c57 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -87,6 +87,10 @@ func New(ctx *runtime.Context) *cobra.Command { // Execute performs: build request → (MSIG step 1 bundle OR EOA allowlist+post) → parse. func Execute(ctx context.Context, h *common.Handler, namespace string, duration time.Duration, secretsAuth string) error { + if _, err := h.EnsureVaultValidationOrConsent(ctx); err != nil { + return err + } + if !common.IsBrowserFlow(secretsAuth) { if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil { return err diff --git a/internal/rpc/chainid.go b/internal/rpc/chainid.go new file mode 100644 index 00000000..aab64d8d --- /dev/null +++ b/internal/rpc/chainid.go @@ -0,0 +1,57 @@ +package rpc + +import ( + "context" + "fmt" + "strconv" + "time" + + gethrpc "github.com/ethereum/go-ethereum/rpc" + + chainSelectors "github.com/smartcontractkit/chain-selectors" +) + +// QueryEthChainID dials rpcURL and returns the chain ID from eth_chainId. +func QueryEthChainID(rpcURL string) (uint64, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + client, err := gethrpc.DialContext(ctx, rpcURL) + if err != nil { + return 0, err + } + defer client.Close() + + var chainIDHex string + if err := client.CallContext(ctx, &chainIDHex, "eth_chainId"); err != nil { + return 0, err + } + + return strconv.ParseUint(chainIDHex, 0, 64) +} + +// ValidateMatchesSelector verifies the RPC's eth_chainId matches expectedSelector. +func ValidateMatchesSelector(rpcURL string, expectedSelector uint64) error { + rpcChainID, err := QueryEthChainID(rpcURL) + if err != nil { + return fmt.Errorf("failed to verify RPC chain ID: %w", err) + } + + expectedChainIDRaw, err := chainSelectors.GetChainIDFromSelector(expectedSelector) + if err != nil { + return fmt.Errorf("invalid chain selector %d: %w", expectedSelector, err) + } + expectedChainID, err := strconv.ParseUint(expectedChainIDRaw, 10, 64) + if err != nil { + return fmt.Errorf("invalid chain ID %q for selector %d: %w", expectedChainIDRaw, expectedSelector, err) + } + + if rpcChainID != expectedChainID { + return fmt.Errorf( + "RPC URL points to chain ID %d, but expected chain ID %d (selector %d); check your project RPC settings", + rpcChainID, expectedChainID, expectedSelector, + ) + } + + return nil +} diff --git a/internal/rpc/chainid_test.go b/internal/rpc/chainid_test.go new file mode 100644 index 00000000..68008d95 --- /dev/null +++ b/internal/rpc/chainid_test.go @@ -0,0 +1,60 @@ +package rpc_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/rpc" +) + +const sepoliaChainSelector uint64 = 16015286601757825753 + +func newEthChainIDServer(t *testing.T, chainIDHex string) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + require.Equal(t, "eth_chainId", req.Method) + + w.Header().Set("Content-Type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": req.ID, + "result": chainIDHex, + })) + })) +} + +func TestQueryEthChainID(t *testing.T) { + server := newEthChainIDServer(t, "0xaa36a7") + t.Cleanup(server.Close) + + chainID, err := rpc.QueryEthChainID(server.URL) + require.NoError(t, err) + require.Equal(t, uint64(11155111), chainID) +} + +func TestValidateMatchesSelector(t *testing.T) { + t.Run("matching chain ID", func(t *testing.T) { + server := newEthChainIDServer(t, "0xaa36a7") + t.Cleanup(server.Close) + + require.NoError(t, rpc.ValidateMatchesSelector(server.URL, sepoliaChainSelector)) + }) + + t.Run("mismatched chain ID", func(t *testing.T) { + server := newEthChainIDServer(t, "0x1") + t.Cleanup(server.Close) + + err := rpc.ValidateMatchesSelector(server.URL, sepoliaChainSelector) + require.Error(t, err) + require.Contains(t, err.Error(), "RPC URL points to chain ID") + }) +} diff --git a/internal/rpc/url.go b/internal/rpc/url.go new file mode 100644 index 00000000..75fa9cb8 --- /dev/null +++ b/internal/rpc/url.go @@ -0,0 +1,23 @@ +package rpc + +import ( + "fmt" + "net/url" +) + +// IsValidURL checks that rpcURL has an http or https scheme and a non-empty host. +func IsValidURL(rpcURL string) error { + parsedURL, err := url.Parse(rpcURL) + if err != nil { + return fmt.Errorf("failed to parse RPC URL: invalid format") + } + + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return fmt.Errorf("invalid scheme in RPC URL: %s", parsedURL.Scheme) + } + if parsedURL.Host == "" { + return fmt.Errorf("invalid host in RPC URL: %s", parsedURL.Host) + } + + return nil +} diff --git a/internal/rpc/url_test.go b/internal/rpc/url_test.go new file mode 100644 index 00000000..de5c15b3 --- /dev/null +++ b/internal/rpc/url_test.go @@ -0,0 +1,37 @@ +package rpc_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/rpc" +) + +func TestIsValidURL(t *testing.T) { + t.Run("accepts https URL", func(t *testing.T) { + require.NoError(t, rpc.IsValidURL("https://rpc.example.com")) + }) + + t.Run("accepts http URL", func(t *testing.T) { + require.NoError(t, rpc.IsValidURL("http://127.0.0.1:8545")) + }) + + t.Run("rejects invalid scheme", func(t *testing.T) { + err := rpc.IsValidURL("ftp://rpc.example.com") + require.Error(t, err) + require.Contains(t, err.Error(), "invalid scheme") + }) + + t.Run("rejects missing host", func(t *testing.T) { + err := rpc.IsValidURL("https://") + require.Error(t, err) + require.Contains(t, err.Error(), "invalid host") + }) + + t.Run("rejects URL without scheme", func(t *testing.T) { + err := rpc.IsValidURL("not-a-valid-url") + require.Error(t, err) + require.Contains(t, err.Error(), "invalid scheme") + }) +} diff --git a/internal/settings/capabilities_registry_rpc.go b/internal/settings/capabilities_registry_rpc.go new file mode 100644 index 00000000..0c9a3b09 --- /dev/null +++ b/internal/settings/capabilities_registry_rpc.go @@ -0,0 +1,48 @@ +package settings + +import ( + "fmt" + "strings" + + "github.com/spf13/viper" + + "github.com/smartcontractkit/cre-cli/internal/rpc" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +// ResolveCapabilitiesRegistryRPC looks up the project RPC URL for the tenant's +// CapabilitiesRegistry chain. When no RPC is configured, ok is false and err is nil. +// When an RPC is configured, its URL format and eth_chainId are validated against +// the tenant chain selector before returning ok=true. +// +// TODO(DEVSVCS-5178) +func ResolveCapabilitiesRegistryRPC(v *viper.Viper, tenantCtx *tenantctx.EnvironmentContext) (rpcURL, chainName string, ok bool, err error) { + if tenantCtx == nil || tenantCtx.CapabilitiesRegistry == nil { + return "", "", false, fmt.Errorf("capabilities registry is not configured in your user context; run `cre login` to refresh %s", tenantctx.ContextFile) + } + + expectedSelector := tenantCtx.CapabilitiesRegistry.ChainSelector + + chainName, err = GetChainNameByChainSelector(expectedSelector) + if err != nil { + return "", "", false, fmt.Errorf("capabilities registry chain selector %d: %w", expectedSelector, err) + } + + rpcURL, err = GetRpcUrlSettings(v, chainName) + if err != nil { + if strings.Contains(err.Error(), "rpc url not found") { + return "", chainName, false, nil + } + return "", chainName, false, err + } + + if err := rpc.IsValidURL(rpcURL); err != nil { + return "", chainName, false, fmt.Errorf("invalid RPC URL for %s: %w", chainName, err) + } + + if err := rpc.ValidateMatchesSelector(rpcURL, expectedSelector); err != nil { + return "", chainName, false, err + } + + return rpcURL, chainName, true, nil +} diff --git a/internal/settings/capabilities_registry_rpc_test.go b/internal/settings/capabilities_registry_rpc_test.go new file mode 100644 index 00000000..733fbb8b --- /dev/null +++ b/internal/settings/capabilities_registry_rpc_test.go @@ -0,0 +1,130 @@ +package settings_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +const sepoliaChainSelector uint64 = 16015286601757825753 + +func newEthChainIDServer(t *testing.T, chainIDHex string) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + require.Equal(t, "eth_chainId", req.Method) + + w.Header().Set("Content-Type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": req.ID, + "result": chainIDHex, + })) + })) +} + +func TestResolveCapabilitiesRegistryRPC_MissingTenantContext(t *testing.T) { + _, _, ok, err := settings.ResolveCapabilitiesRegistryRPC(viper.New(), nil) + require.Error(t, err) + require.False(t, ok) + require.Contains(t, err.Error(), "capabilities registry is not configured") +} + +func TestResolveCapabilitiesRegistryRPC_NoRPCConfigured(t *testing.T) { + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set("staging.rpcs", []map[string]string{ + {"chain-name": "ethereum-mainnet", "url": "https://example.invalid"}, + }) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: sepoliaChainSelector, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + rpcURL, chainName, ok, err := settings.ResolveCapabilitiesRegistryRPC(v, tenantCtx) + require.NoError(t, err) + require.False(t, ok) + require.Empty(t, rpcURL) + require.Equal(t, "ethereum-testnet-sepolia", chainName) +} + +func TestResolveCapabilitiesRegistryRPC_ValidRPC(t *testing.T) { + server := newEthChainIDServer(t, "0xaa36a7") // Sepolia + t.Cleanup(server.Close) + + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set("staging.rpcs", []map[string]string{ + {"chain-name": "ethereum-testnet-sepolia", "url": server.URL}, + }) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: sepoliaChainSelector, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + rpcURL, chainName, ok, err := settings.ResolveCapabilitiesRegistryRPC(v, tenantCtx) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, server.URL, rpcURL) + require.Equal(t, "ethereum-testnet-sepolia", chainName) +} + +func TestResolveCapabilitiesRegistryRPC_WrongChainID(t *testing.T) { + server := newEthChainIDServer(t, "0x1") // mainnet + t.Cleanup(server.Close) + + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set("staging.rpcs", []map[string]string{ + {"chain-name": "ethereum-testnet-sepolia", "url": server.URL}, + }) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: sepoliaChainSelector, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + _, _, ok, err := settings.ResolveCapabilitiesRegistryRPC(v, tenantCtx) + require.Error(t, err) + require.False(t, ok) + require.Contains(t, err.Error(), "RPC URL points to chain ID") +} + +func TestResolveCapabilitiesRegistryRPC_InvalidRPCURL(t *testing.T) { + v := viper.New() + v.Set(settings.CreTargetEnvVar, "staging") + v.Set("staging.rpcs", []map[string]string{ + {"chain-name": "ethereum-testnet-sepolia", "url": "not-a-valid-url"}, + }) + + tenantCtx := &tenantctx.EnvironmentContext{ + CapabilitiesRegistry: &tenantctx.OnChainContract{ + ChainSelector: sepoliaChainSelector, + Address: "0x7f3191EaF73429177bAB3bAc5c36Ed2D5E39985f", + }, + } + + _, _, ok, err := settings.ResolveCapabilitiesRegistryRPC(v, tenantCtx) + require.Error(t, err) + require.False(t, ok) + require.Contains(t, err.Error(), "invalid RPC URL") +} diff --git a/internal/settings/settings_get.go b/internal/settings/settings_get.go index f83fb79b..cf8541b0 100644 --- a/internal/settings/settings_get.go +++ b/internal/settings/settings_get.go @@ -47,6 +47,9 @@ type ExperimentalChain struct { Forwarder string `mapstructure:"forwarder" yaml:"forwarder"` } +// GetRpcUrlSettings resolves the RPC URL for chainName from the current project target. +// +// TODO(DEVSVCS-5178) func GetRpcUrlSettings(v *viper.Viper, chainName string) (string, error) { target, err := GetTarget(v) if err != nil { diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index 992a9f3b..058f5cd6 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -2,7 +2,6 @@ package settings import ( "fmt" - "net/url" "os" "strings" @@ -12,6 +11,7 @@ import ( "github.com/spf13/viper" "sigs.k8s.io/yaml" + "github.com/smartcontractkit/cre-cli/internal/rpc" "github.com/smartcontractkit/cre-cli/internal/constants" ) @@ -277,20 +277,7 @@ func validateSettings(config *WorkflowSettings, allowUnknownChains bool) error { } func isValidRpcUrl(rpcURL string) error { - parsedURL, err := url.Parse(rpcURL) - if err != nil { - return fmt.Errorf("failed to parse RPC URL: invalid format") - } - - // Check if the URL has a valid scheme and host - if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { - return fmt.Errorf("invalid scheme in RPC URL: %s", parsedURL.Scheme) - } - if parsedURL.Host == "" { - return fmt.Errorf("invalid host in RPC URL: %s", parsedURL.Host) - } - - return nil + return rpc.IsValidURL(rpcURL) } func IsValidChainName(name string) error { @@ -335,6 +322,8 @@ func ShouldSkipGetOwner(cmd *cobra.Command) bool { // ValidateDeploymentRPC ensures project settings define a valid RPC URL for chainName (e.g. the workflow // registry chain). It is a no-op when chainName is empty. Used during settings load and from secrets owner-key flows. +// +// TODO(DEVSVCS-5178) func ValidateDeploymentRPC(config *WorkflowSettings, chainName string) error { if chainName == "" { return nil From 778116b8ae97d9c0bf96e94aba2222d18d1ed737 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Wed, 3 Jun 2026 23:55:55 +0100 Subject: [PATCH 11/14] fix test --- internal/testutil/graphql_mock.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/testutil/graphql_mock.go b/internal/testutil/graphql_mock.go index 888f6f7e..c4740869 100644 --- a/internal/testutil/graphql_mock.go +++ b/internal/testutil/graphql_mock.go @@ -36,6 +36,10 @@ func MockGetTenantConfigGraphQLPayload() map[string]any { "tenantId": "test-tenant-id", "defaultDonFamily": "test-don", "vaultGatewayUrl": "https://vault.example.test", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "6433500567565415381", + "address": "0x76c9cf548b4179F8901cda1f8623568b58215E62", + }, "registries": []map[string]any{ { "id": "anvil-devnet", From f42100829adeb96866132d0fd5b345421f4c9cf7 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Thu, 4 Jun 2026 13:07:29 +0100 Subject: [PATCH 12/14] Fix test --- cmd/secrets/execute/execute.go | 1 + test/multi_command_flows/secrets_happy_path.go | 1 + 2 files changed, 2 insertions(+) diff --git a/cmd/secrets/execute/execute.go b/cmd/secrets/execute/execute.go index 697199d6..dcdc95af 100644 --- a/cmd/secrets/execute/execute.go +++ b/cmd/secrets/execute/execute.go @@ -98,6 +98,7 @@ func New(ctx *runtime.Context) *cobra.Command { } settings.AddTxnTypeFlags(cmd) + settings.AddSkipConfirmation(cmd) return cmd } diff --git a/test/multi_command_flows/secrets_happy_path.go b/test/multi_command_flows/secrets_happy_path.go index 71974060..b8ce7f1a 100644 --- a/test/multi_command_flows/secrets_happy_path.go +++ b/test/multi_command_flows/secrets_happy_path.go @@ -315,6 +315,7 @@ func secretsListMsig(t *testing.T, tc TestConfig) string { tc.GetCliEnvFlag(), tc.GetProjectRootFlag(), "--unsigned", + "--" + settings.Flags.SkipConfirmation.Name, } cmd := exec.Command(CLIPath, args...) // Let CLI handle context switching - don't set cmd.Dir manually From 1156fbc5ca41d0247f9df796e0dd078890ba43e9 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Thu, 4 Jun 2026 13:13:45 +0100 Subject: [PATCH 13/14] gendoc --- docs/cre_secrets_execute.md | 1 + internal/settings/workflow_settings.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/cre_secrets_execute.md b/docs/cre_secrets_execute.md index 464bd617..5a8534dc 100644 --- a/docs/cre_secrets_execute.md +++ b/docs/cre_secrets_execute.md @@ -17,6 +17,7 @@ cre secrets execute 157364...af4d5.json ``` -h, --help help for execute --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index 058f5cd6..17aba9c4 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -11,8 +11,8 @@ import ( "github.com/spf13/viper" "sigs.k8s.io/yaml" - "github.com/smartcontractkit/cre-cli/internal/rpc" "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/rpc" ) // GetWorkflowPathFromFile reads workflow-path from a workflow.yaml file (same value deploy/simulate get from Settings). From 79369e590643e9208cbf41c9fe523ab9fe65cc55 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Thu, 4 Jun 2026 23:03:28 +0100 Subject: [PATCH 14/14] Isolate CLI config from user home during tests --- Makefile | 2 +- cmd/login/login_test.go | 7 +- cmd/logout/logout_test.go | 16 +- internal/creconfig/creconfig.go | 8 + internal/creconfig/creconfig_test.go | 19 ++ internal/credentials/credentials_test.go | 17 +- .../templateconfig/templateconfig_test.go | 18 +- internal/tenantctx/tenantctx_test.go | 19 +- internal/testutil/cretest/cretest.go | 216 ++++++++++++ internal/testutil/cretest/cretest_test.go | 126 +++++++ test/cli_run.go | 29 ++ test/convert_simulate_helper.go | 60 ++-- test/error_output_test.go | 42 +-- ...binding_generation_and_simulate_go_test.go | 28 +- test/init_and_simulate_ts_test.go | 47 +-- test/init_convert_simulate_go_test.go | 32 +- test/init_convert_simulate_ts_test.go | 31 +- test/main_test.go | 9 +- .../multi_command_flows/account_happy_path.go | 40 +-- test/multi_command_flows/cli_run.go | 21 ++ .../multi_command_flows/secrets_happy_path.go | 56 +--- .../workflow_happy_path_1.go | 79 +---- .../workflow_happy_path_2.go | 40 +-- .../workflow_happy_path_3.go | 53 +-- .../workflow_private_registry.go | 153 +-------- .../workflow_simulator_path.go | 18 +- test/multi_command_test.go | 313 +++++++----------- 27 files changed, 711 insertions(+), 788 deletions(-) create mode 100644 internal/testutil/cretest/cretest.go create mode 100644 internal/testutil/cretest/cretest_test.go create mode 100644 test/cli_run.go create mode 100644 test/multi_command_flows/cli_run.go diff --git a/Makefile b/Makefile index 1d55f8c6..670b53d4 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ test-e2e: # test-quick: run tests with 30s timeout, skipping slow/flaky e2e tests. Use -short so TestE2EInit_ConvertToCustomBuild_TS is skipped. test-quick: - $(GOTEST) ./... -v -short -skip 'MultiCommandHappyPaths|TestPostToGateway|TestBlankWorkflowSimulation|TestWaitForBackendLinkProcessing|TestTryAutoLink|TestCheckLinkStatusViaGraphQL|Fails to run tests with invalid Go code' -timeout 30s + $(GOTEST) ./... -v -short -skip 'TestWorkflow_|TestSecrets_|TestAccount_|TestPostToGateway|TestBlankWorkflowSimulation|TestWaitForBackendLinkProcessing|TestTryAutoLink|TestCheckLinkStatusViaGraphQL|Fails to run tests with invalid Go code' -timeout 30s clean: $(GOCLEAN) diff --git a/cmd/login/login_test.go b/cmd/login/login_test.go index 0435ba87..0d4488c3 100644 --- a/cmd/login/login_test.go +++ b/cmd/login/login_test.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/oauth" "github.com/smartcontractkit/cre-cli/internal/tenantctx" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -32,8 +33,7 @@ func TestFetchTenantConfig_GQLError_ReturnsError(t *testing.T) { })) defer srv.Close() - tmp := t.TempDir() - t.Setenv("HOME", tmp) + cretest.IsolateConfig(t) log := zerolog.Nop() h := &handler{ log: &log, @@ -92,8 +92,7 @@ func TestLogin_NonInteractive_ReturnsError(t *testing.T) { } func TestSaveCredentials_WritesYAML(t *testing.T) { - tmp := t.TempDir() - t.Setenv("HOME", tmp) + cretest.IsolateConfig(t) tokenSet := &credentials.CreLoginTokenSet{ AccessToken: "a", diff --git a/cmd/logout/logout_test.go b/cmd/logout/logout_test.go index 3f5f4432..e6c34e74 100644 --- a/cmd/logout/logout_test.go +++ b/cmd/logout/logout_test.go @@ -14,9 +14,10 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) -func setupCredentialFile(t *testing.T, home string, token string) { +func setupCredentialFile(t *testing.T, token string) { t.Helper() dir, err := creconfig.EnsureDir() if err != nil { @@ -42,8 +43,7 @@ func setupCredentialFile(t *testing.T, home string, token string) { } func TestExecute_NoCredentialsFile(t *testing.T) { - tDir := t.TempDir() - t.Setenv("HOME", tDir) + cretest.IsolateConfig(t) creds := credentials.Credentials{ Tokens: &credentials.CreLoginTokenSet{}, @@ -64,10 +64,9 @@ func TestExecute_NoCredentialsFile(t *testing.T) { } func TestExecute_SuccessRevocationAndRemoval(t *testing.T) { - tDir := t.TempDir() - t.Setenv("HOME", tDir) + cretest.IsolateConfig(t) token := "test-refresh-token" - setupCredentialFile(t, tDir, token) + setupCredentialFile(t, token) var received bool ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -123,10 +122,9 @@ func TestExecute_SuccessRevocationAndRemoval(t *testing.T) { } func TestExecute_RevocationFails_StillRemovesFile(t *testing.T) { - tDir := t.TempDir() - t.Setenv("HOME", tDir) + cretest.IsolateConfig(t) token := "bad-refresh-token" - setupCredentialFile(t, tDir, token) + setupCredentialFile(t, token) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) diff --git a/internal/creconfig/creconfig.go b/internal/creconfig/creconfig.go index b9bd846c..214c7967 100644 --- a/internal/creconfig/creconfig.go +++ b/internal/creconfig/creconfig.go @@ -4,12 +4,20 @@ import ( "fmt" "os" "path/filepath" + "strings" ) const Dir = ".cre" +// ConfigDirEnvVar overrides the CLI config directory (absolute path to the directory +// that contains context.yaml, cre.yaml, etc.). When unset, config lives under $HOME/.cre. +const ConfigDirEnvVar = "CRE_CONFIG_DIR" + // DirPath returns the absolute path to the CLI config directory. func DirPath() (string, error) { + if dir := strings.TrimSpace(os.Getenv(ConfigDirEnvVar)); dir != "" { + return filepath.Abs(dir) + } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("get home dir: %w", err) diff --git a/internal/creconfig/creconfig_test.go b/internal/creconfig/creconfig_test.go index adfc5ec1..5c0342a9 100644 --- a/internal/creconfig/creconfig_test.go +++ b/internal/creconfig/creconfig_test.go @@ -6,9 +6,24 @@ import ( "testing" ) +func TestDirPath_CRE_CONFIG_DIR(t *testing.T) { + override := filepath.Join(t.TempDir(), "isolated-cre") + t.Setenv(ConfigDirEnvVar, override) + + got, err := DirPath() + if err != nil { + t.Fatalf("DirPath() error: %v", err) + } + want, _ := filepath.Abs(override) + if got != want { + t.Fatalf("DirPath() = %q, want %q", got, want) + } +} + func TestDirPath(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) + t.Setenv(ConfigDirEnvVar, "") got, err := DirPath() if err != nil { @@ -23,6 +38,7 @@ func TestDirPath(t *testing.T) { func TestFilePath(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) + t.Setenv(ConfigDirEnvVar, "") got, err := FilePath("context.yaml") if err != nil { @@ -37,6 +53,7 @@ func TestFilePath(t *testing.T) { func TestJoinPath(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) + t.Setenv(ConfigDirEnvVar, "") got, err := JoinPath("template-cache", "list.json") if err != nil { @@ -51,6 +68,7 @@ func TestJoinPath(t *testing.T) { func TestFilePathHint(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) + t.Setenv(ConfigDirEnvVar, "") got := FilePathHint("context.yaml") want := filepath.Join(home, Dir, "context.yaml") @@ -72,6 +90,7 @@ func TestFilePathHint_FallsBackToRelPath(t *testing.T) { func TestEnsureDir(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) + t.Setenv(ConfigDirEnvVar, "") dir, err := EnsureDir() if err != nil { diff --git a/internal/credentials/credentials_test.go b/internal/credentials/credentials_test.go index b679c12b..d384ee0a 100644 --- a/internal/credentials/credentials_test.go +++ b/internal/credentials/credentials_test.go @@ -13,7 +13,7 @@ import ( func TestNew_Default(t *testing.T) { t.Setenv(CreApiKeyVar, "") - t.Setenv("HOME", t.TempDir()) + isolateCREConfig(t) logger := testutil.NewTestLogger() _, err := New(logger) @@ -24,7 +24,7 @@ func TestNew_Default(t *testing.T) { func TestNew_WithEnvAPIKey(t *testing.T) { t.Setenv(CreApiKeyVar, "env-key") - t.Setenv("HOME", t.TempDir()) + isolateCREConfig(t) logger := testutil.NewTestLogger() cfg, err := New(logger) @@ -40,8 +40,7 @@ func TestNew_WithEnvAPIKey(t *testing.T) { } func TestNew_WithConfigFile(t *testing.T) { t.Setenv(CreApiKeyVar, "") - tDir := t.TempDir() - t.Setenv("HOME", tDir) + isolateCREConfig(t) dir, err := creconfig.EnsureDir() if err != nil { @@ -413,6 +412,16 @@ func TestCheckIsUngatedOrganization_InvalidJWTFormat(t *testing.T) { } } +func isolateCREConfig(t *testing.T) string { + t.Helper() + dir := filepath.Join(t.TempDir(), creconfig.Dir) + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatalf("mkdir config dir: %v", err) + } + t.Setenv(creconfig.ConfigDirEnvVar, dir) + return dir +} + func TestSecureRemove(t *testing.T) { t.Run("missing file is no-op", func(t *testing.T) { path := filepath.Join(t.TempDir(), "missing.yaml") diff --git a/internal/templateconfig/templateconfig_test.go b/internal/templateconfig/templateconfig_test.go index 2b5a61c4..1dd1f295 100644 --- a/internal/templateconfig/templateconfig_test.go +++ b/internal/templateconfig/templateconfig_test.go @@ -11,6 +11,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) func TestParseRepoString(t *testing.T) { @@ -46,7 +47,7 @@ func TestLoadTemplateSourcesDefault(t *testing.T) { logger := testutil.NewTestLogger() // Point HOME to a temp dir with no config file - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) sources := LoadTemplateSources(logger) require.Len(t, sources, len(DefaultSources)) @@ -57,8 +58,7 @@ func TestLoadTemplateSourcesDefault(t *testing.T) { func TestLoadTemplateSourcesFromConfigFile(t *testing.T) { logger := testutil.NewTestLogger() - homeDir := t.TempDir() - t.Setenv("HOME", homeDir) + cretest.IsolateConfig(t) configDir, err := creconfig.DirPath() require.NoError(t, err) @@ -85,8 +85,7 @@ func TestLoadTemplateSourcesFromConfigFile(t *testing.T) { func TestSaveTemplateSources(t *testing.T) { logger := testutil.NewTestLogger() - homeDir := t.TempDir() - t.Setenv("HOME", homeDir) + cretest.IsolateConfig(t) sources := []templaterepo.RepoSource{ {Owner: "org1", Repo: "repo1", Ref: "main"}, @@ -116,8 +115,7 @@ func TestEnsureDefaultConfig(t *testing.T) { logger := testutil.NewTestLogger() t.Run("creates file when missing", func(t *testing.T) { - homeDir := t.TempDir() - t.Setenv("HOME", homeDir) + cretest.IsolateConfig(t) require.NoError(t, EnsureDefaultConfig(logger)) @@ -130,8 +128,7 @@ func TestEnsureDefaultConfig(t *testing.T) { }) t.Run("no-op when file exists", func(t *testing.T) { - homeDir := t.TempDir() - t.Setenv("HOME", homeDir) + cretest.IsolateConfig(t) // Write custom config first custom := []templaterepo.RepoSource{ @@ -151,8 +148,7 @@ func TestEnsureDefaultConfig(t *testing.T) { func TestAddRepoToExisting(t *testing.T) { logger := testutil.NewTestLogger() - homeDir := t.TempDir() - t.Setenv("HOME", homeDir) + cretest.IsolateConfig(t) // Start with defaults require.NoError(t, SaveTemplateSources(DefaultSources)) diff --git a/internal/tenantctx/tenantctx_test.go b/internal/tenantctx/tenantctx_test.go index 8a1433dc..ef439131 100644 --- a/internal/tenantctx/tenantctx_test.go +++ b/internal/tenantctx/tenantctx_test.go @@ -17,6 +17,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) func newMockGQLServer(t *testing.T, response map[string]any) *httptest.Server { @@ -120,7 +121,7 @@ func TestFetchAndWriteContext_OnChainAndPrivate(t *testing.T) { srv := newMockGQLServer(t, gqlResponseOnChainAndPrivate()) defer srv.Close() - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() client := newGQLClient(t, srv.URL) @@ -201,7 +202,7 @@ func TestFetchAndWriteContext_PrivateOnly(t *testing.T) { srv := newMockGQLServer(t, gqlResponsePrivateOnly()) defer srv.Close() - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() client := newGQLClient(t, srv.URL) @@ -266,7 +267,7 @@ func TestFetchAndWriteContext_GQLError(t *testing.T) { })) defer srv.Close() - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() client := newGQLClient(t, srv.URL) @@ -280,7 +281,7 @@ func TestFetchAndWriteContext_EnvNameUppercased(t *testing.T) { srv := newMockGQLServer(t, gqlResponsePrivateOnly()) defer srv.Close() - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() client := newGQLClient(t, srv.URL) @@ -370,8 +371,7 @@ func TestEnsureContext_APIKeyAlwaysFetches(t *testing.T) { srv := newCountingGQLServer(t, &callCount, gqlResponsePrivateOnly()) defer srv.Close() - tmpHome := t.TempDir() - t.Setenv("HOME", tmpHome) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() creds := &credentials.Credentials{AuthType: credentials.AuthTypeApiKey, APIKey: "test-key"} @@ -399,8 +399,7 @@ func TestEnsureContext_BearerUsesCached(t *testing.T) { srv := newCountingGQLServer(t, &callCount, gqlResponsePrivateOnly()) defer srv.Close() - tmpHome := t.TempDir() - t.Setenv("HOME", tmpHome) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() creds := &credentials.Credentials{ @@ -430,7 +429,7 @@ func TestEnsureContext_DefaultsToProduction(t *testing.T) { srv := newMockGQLServer(t, gqlResponsePrivateOnly()) defer srv.Close() - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() creds := &credentials.Credentials{AuthType: credentials.AuthTypeApiKey, APIKey: "test-key"} envSet := &environments.EnvironmentSet{EnvName: "", GraphQLURL: srv.URL} @@ -479,7 +478,7 @@ func TestFetchAndWriteContext_PersistsUnknownRegistryType(t *testing.T) { srv := newMockGQLServer(t, response) defer srv.Close() - t.Setenv("HOME", t.TempDir()) + cretest.IsolateConfig(t) log := testutil.NewTestLogger() client := newGQLClient(t, srv.URL) diff --git a/internal/testutil/cretest/cretest.go b/internal/testutil/cretest/cretest.go new file mode 100644 index 00000000..eddc84be --- /dev/null +++ b/internal/testutil/cretest/cretest.go @@ -0,0 +1,216 @@ +package cretest + +import ( + "bytes" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/creconfig" + "github.com/smartcontractkit/cre-cli/internal/testutil/testjwt" +) + +const credentialsFile = "cre.yaml" + +var cliBinary string + +// SetCLIBinary sets the path to the cre CLI binary built for integration tests. +// Call from test.TestMain after building the binary. +func SetCLIBinary(path string) { + cliBinary = path +} + +// CLIBinary returns the integration-test CLI binary path. +func CLIBinary() string { + return cliBinary +} + +// Env holds an isolated CLI config directory for a test. +type Env struct { + ConfigDir string +} + +// NewEnv creates a temp config directory, sets CRE_CONFIG_DIR for the test process, +// and returns an Env for subprocess CLI runs in the same test. +func NewEnv(t *testing.T) *Env { + t.Helper() + dir := filepath.Join(t.TempDir(), creconfig.Dir) + require.NoError(t, os.MkdirAll(dir, 0o700)) + t.Setenv(creconfig.ConfigDirEnvVar, dir) + return &Env{ConfigDir: dir} +} + +// IsolateConfig is an alias for NewEnv for in-process tests that write under ~/.cre. +func IsolateConfig(t *testing.T) string { + t.Helper() + return NewEnv(t).ConfigDir +} + +// PinGoCacheForProcess keeps GOPATH/GOMODCACHE on the real user paths when tests +// override HOME or use temp directories. +func PinGoCacheForProcess(t *testing.T) { + t.Helper() + gopath, gomodcache := realGoCacheDirs(t) + t.Setenv("GOPATH", gopath) + t.Setenv("GOMODCACHE", gomodcache) +} + +func realGoCacheDirs(t *testing.T) (gopath, gomodcache string) { + t.Helper() + realHome, err := os.UserHomeDir() + require.NoError(t, err) + + gopath = os.Getenv("GOPATH") + if gopath == "" { + gopath = filepath.Join(realHome, "go") + } + gomodcache = os.Getenv("GOMODCACHE") + if gomodcache == "" { + gomodcache = filepath.Join(gopath, "pkg", "mod") + } + return gopath, gomodcache +} + +// SeedBearerCredentials writes cre.yaml with a test JWT into configDir. +func SeedBearerCredentials(t *testing.T, configDir, orgID string) { + t.Helper() + require.NoError(t, os.MkdirAll(configDir, 0o700)) + jwt := testjwt.CreateTestJWT(orgID) + creConfig := "AccessToken: " + jwt + "\n" + + "IDToken: test-id-token\n" + + "RefreshToken: test-refresh-token\n" + + "ExpiresIn: 3600\n" + + "TokenType: Bearer\n" + path := filepath.Join(configDir, credentialsFile) + require.NoError(t, os.WriteFile(path, []byte(creConfig), 0o600)) +} + +// CLIEnv builds subprocess environment with isolated CRE_CONFIG_DIR and pinned Go caches. +func CLIEnv(t *testing.T, configDir string) []string { + t.Helper() + gopath, gomodcache := realGoCacheDirs(t) + prefix := creconfig.ConfigDirEnvVar + "=" + + childEnv := make([]string, 0, len(os.Environ())+3) + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, prefix) || + strings.HasPrefix(entry, "GOPATH=") || + strings.HasPrefix(entry, "GOMODCACHE=") { + continue + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, + creconfig.ConfigDirEnvVar+"="+configDir, + "GOPATH="+gopath, + "GOMODCACHE="+gomodcache, + ) + if runtime.GOOS == "windows" { + childEnv = append(childEnv, "USERPROFILE="+os.Getenv("USERPROFILE")) + } + return childEnv +} + +func configDirForCLI(t *testing.T, env *Env) string { + t.Helper() + if env != nil && env.ConfigDir != "" { + return env.ConfigDir + } + if dir := strings.TrimSpace(os.Getenv(creconfig.ConfigDirEnvVar)); dir != "" { + return dir + } + return NewEnv(t).ConfigDir +} + +// RunOption configures RunCLI. +type RunOption func(*runConfig) + +type runConfig struct { + env *Env + dir string + stdin io.Reader + bearerOrg string + extraEnv []string +} + +// WithEnv uses an existing isolated config directory. +func WithEnv(env *Env) RunOption { + return func(c *runConfig) { c.env = env } +} + +// WithDir sets the subprocess working directory. +func WithDir(dir string) RunOption { + return func(c *runConfig) { c.dir = dir } +} + +// WithStdin sets subprocess stdin. +func WithStdin(r io.Reader) RunOption { + return func(c *runConfig) { c.stdin = r } +} + +// WithBearerCredentials seeds cre.yaml before running the CLI. +func WithBearerCredentials(orgID string) RunOption { + return func(c *runConfig) { c.bearerOrg = orgID } +} + +// WithExtraEnv appends additional KEY=value entries to the subprocess environment. +func WithExtraEnv(entries ...string) RunOption { + return func(c *runConfig) { c.extraEnv = append(c.extraEnv, entries...) } +} + +// Result holds CLI subprocess output. +type Result struct { + Stdout string + Stderr string +} + +// Combined returns stdout and stderr concatenated. +func (r Result) Combined() string { + return r.Stdout + r.Stderr +} + +// RunCLI runs the cre binary with isolated CRE_CONFIG_DIR. binary may be empty to use SetCLIBinary path. +func RunCLI(t *testing.T, binary string, args []string, opts ...RunOption) (Result, error) { + t.Helper() + if binary == "" { + binary = cliBinary + } + require.NotEmpty(t, binary, "cretest: CLI binary path not set; call cretest.SetCLIBinary in TestMain") + + var cfg runConfig + for _, opt := range opts { + opt(&cfg) + } + + configDir := configDirForCLI(t, cfg.env) + if cfg.bearerOrg != "" { + SeedBearerCredentials(t, configDir, cfg.bearerOrg) + } + + cmd := exec.Command(binary, args...) + cmd.Env = CLIEnv(t, configDir) + if len(cfg.extraEnv) > 0 { + cmd.Env = append(cmd.Env, cfg.extraEnv...) + } + if cfg.dir != "" { + cmd.Dir = cfg.dir + } + if cfg.stdin != nil { + cmd.Stdin = cfg.stdin + } + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + return Result{ + Stdout: stdout.String(), + Stderr: stderr.String(), + }, err +} diff --git a/internal/testutil/cretest/cretest_test.go b/internal/testutil/cretest/cretest_test.go new file mode 100644 index 00000000..fee11b20 --- /dev/null +++ b/internal/testutil/cretest/cretest_test.go @@ -0,0 +1,126 @@ +package cretest + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/creconfig" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/rs/zerolog" + + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +// mockTenantConfigPayload matches test GraphQL mock data used by E2E tests. +func mockTenantConfigPayload() map[string]any { + return map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "test-tenant-id", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "capabilitiesRegistry": map[string]any{ + "chainSelector": "6433500567565415381", + "address": "0x76c9cf548b4179F8901cda1f8623568b58215E62", + }, + "registries": []map[string]any{ + { + "id": "anvil-devnet", + "label": "anvil-devnet", + "type": "ON_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"OWNER_KEY_SIGNING"}, + }, + { + "id": "private", + "label": "Private (Chainlink-hosted)", + "type": "OFF_CHAIN", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + "forwarders": []any{}, + }, + }, + } +} + +// TestFetchAndWriteContext_DoesNotModifyRealHomeConfig ensures tenant context is written +// only under CRE_CONFIG_DIR, not the developer's ~/.cre directory. +func TestFetchAndWriteContext_DoesNotModifyRealHomeConfig(t *testing.T) { + realHome, err := os.UserHomeDir() + if err != nil { + t.Skipf("cannot resolve home dir: %v", err) + } + realContextPath := filepath.Join(realHome, creconfig.Dir, tenantctx.ContextFile) + + var before []byte + var beforeStat os.FileInfo + if st, statErr := os.Stat(realContextPath); statErr == nil { + beforeStat = st + before, _ = os.ReadFile(realContextPath) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(mockTenantConfigPayload()) + })) + defer srv.Close() + + IsolateConfig(t) + PinGoCacheForProcess(t) + + l := zerolog.Nop() + log := &l + client := graphqlclient.New( + &credentials.Credentials{AuthType: credentials.AuthTypeApiKey, APIKey: "test-key"}, + &environments.EnvironmentSet{GraphQLURL: srv.URL}, + log, + ) + + if err := tenantctx.FetchAndWriteContext(context.Background(), client, "STAGING", log); err != nil { + t.Fatalf("FetchAndWriteContext: %v", err) + } + + isolatedPath, err := creconfig.FilePath(tenantctx.ContextFile) + if err != nil { + t.Fatalf("isolated context path: %v", err) + } + isolatedData, err := os.ReadFile(isolatedPath) + if err != nil { + t.Fatalf("read isolated context: %v", err) + } + isolated := string(isolatedData) + if !strings.Contains(isolated, "test-tenant-id") || !strings.Contains(isolated, "anvil-devnet") { + t.Fatalf("isolated context missing mock payload: %s", isolated) + } + + afterStat, statErr := os.Stat(realContextPath) + if beforeStat == nil { + if statErr == nil { + t.Fatalf("real %s was created during test", realContextPath) + } + return + } + if statErr != nil { + t.Fatalf("real %s disappeared during test", realContextPath) + } + if !afterStat.ModTime().Equal(beforeStat.ModTime()) { + t.Fatalf("real %s mtime changed during test", realContextPath) + } + after, err := os.ReadFile(realContextPath) + if err != nil { + t.Fatalf("read real context after test: %v", err) + } + if string(after) != string(before) { + t.Fatalf("real %s content changed during test", realContextPath) + } +} diff --git a/test/cli_run.go b/test/cli_run.go new file mode 100644 index 00000000..4680ebf4 --- /dev/null +++ b/test/cli_run.go @@ -0,0 +1,29 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" +) + +// isolatedEnv sets up an isolated CRE config directory for integration tests. +func isolatedEnv(t *testing.T) *cretest.Env { + t.Helper() + return cretest.NewEnv(t) +} + +// runCLI executes the cre binary with isolated CRE_CONFIG_DIR. +func runCLI(t *testing.T, args []string, opts ...cretest.RunOption) (cretest.Result, error) { + t.Helper() + return cretest.RunCLI(t, CLIPath, args, opts...) +} + +// requireCLI runs the cre binary and fails the test on non-zero exit. +func requireCLI(t *testing.T, msg string, args []string, opts ...cretest.RunOption) cretest.Result { + t.Helper() + res, err := runCLI(t, args, opts...) + require.NoError(t, err, "%s:\nSTDOUT:\n%s\nSTDERR:\n%s", msg, res.Stdout, res.Stderr) + return res +} diff --git a/test/convert_simulate_helper.go b/test/convert_simulate_helper.go index 441d0426..9e82bf43 100644 --- a/test/convert_simulate_helper.go +++ b/test/convert_simulate_helper.go @@ -1,45 +1,38 @@ package test import ( - "bytes" "os/exec" "testing" "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) func convertSimulateCaptureOutput(t *testing.T, projectRoot, workflowName string) string { t.Helper() - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "workflow", "simulate", workflowName, - "--project-root", projectRoot, - "--non-interactive", "--trigger-index=0", - "--target=staging-settings", + res := requireCLI(t, "simulate (before convert) failed", + []string{"workflow", "simulate", workflowName, + "--project-root", projectRoot, + "--non-interactive", "--trigger-index=0", + "--target=staging-settings", + }, + cretest.WithDir(projectRoot), ) - cmd.Dir = projectRoot - cmd.Stdout = &stdout - cmd.Stderr = &stderr - require.NoError(t, cmd.Run(), - "simulate (before convert) failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), stderr.String()) - return stdout.String() + return res.Stdout } func convertSimulateRequireOutputContains(t *testing.T, projectRoot, workflowName, expectedSubstring string) { t.Helper() - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "workflow", "simulate", workflowName, - "--project-root", projectRoot, - "--non-interactive", "--trigger-index=0", - "--target=staging-settings", + res := requireCLI(t, "simulate (after convert) failed", + []string{"workflow", "simulate", workflowName, + "--project-root", projectRoot, + "--non-interactive", "--trigger-index=0", + "--target=staging-settings", + }, + cretest.WithDir(projectRoot), ) - cmd.Dir = projectRoot - cmd.Stdout = &stdout - cmd.Stderr = &stderr - require.NoError(t, cmd.Run(), - "simulate (after convert) failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), stderr.String()) - require.Contains(t, stdout.String(), expectedSubstring, + require.Contains(t, res.Stdout, expectedSubstring, "simulate output after convert should contain %q", expectedSubstring) } @@ -56,24 +49,17 @@ func ConvertSimulateBeforeAfter(t *testing.T, projectRoot, workflowDir, workflow func convertRunConvert(t *testing.T, projectRoot, workflowDir string) { t.Helper() - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "workflow", "custom-build", workflowDir, "-f") - cmd.Dir = projectRoot - cmd.Stdout = &stdout - cmd.Stderr = &stderr - require.NoError(t, cmd.Run(), - "convert failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + requireCLI(t, "convert failed", + []string{"workflow", "custom-build", workflowDir, "-f"}, + cretest.WithDir(projectRoot), + ) } func convertRunMakeBuild(t *testing.T, workflowDir string, makeArgs ...string) { t.Helper() - var stdout, stderr bytes.Buffer args := []string{"build"} args = append(args, makeArgs...) cmd := exec.Command("make", args...) cmd.Dir = workflowDir - cmd.Stdout = &stdout - cmd.Stderr = &stderr - require.NoError(t, cmd.Run(), - "make build failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + require.NoError(t, cmd.Run(), "make build failed") } diff --git a/test/error_output_test.go b/test/error_output_test.go index eb6d2cc0..16976bd5 100644 --- a/test/error_output_test.go +++ b/test/error_output_test.go @@ -1,60 +1,42 @@ package test import ( - "bytes" - "os/exec" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ) // TestErrorOutput_UnknownCommand verifies that running an unknown command // produces an error message on stderr and exits with a non-zero code. // This guards against regressions from SilenceErrors: true in root.go. func TestErrorOutput_UnknownCommand(t *testing.T) { - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "nonexistent-command") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() + isolatedEnv(t) + res, err := runCLI(t, []string{"nonexistent-command"}) require.Error(t, err, "expected non-zero exit code for unknown command") - stderrStr := stderr.String() - assert.Contains(t, stderrStr, "unknown command", "expected 'unknown command' error on stderr, got:\nSTDOUT: %s\nSTDERR: %s", stdout.String(), stderrStr) - assert.NotContains(t, stdout.String(), "unknown command", "error message should be on stderr, not stdout") + assert.Contains(t, res.Stderr, "unknown command", "expected 'unknown command' error on stderr, got:\nSTDOUT: %s\nSTDERR: %s", res.Stdout, res.Stderr) + assert.NotContains(t, res.Stdout, "unknown command", "error message should be on stderr, not stdout") } // TestErrorOutput_UnknownFlag verifies that an unknown flag produces an // error message on stderr and exits with a non-zero code. func TestErrorOutput_UnknownFlag(t *testing.T) { - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "--nonexistent-flag") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() + isolatedEnv(t) + res, err := runCLI(t, []string{"--nonexistent-flag"}) require.Error(t, err, "expected non-zero exit code for unknown flag") - stderrStr := stderr.String() - assert.Contains(t, stderrStr, "unknown flag", "expected 'unknown flag' error on stderr, got:\nSTDOUT: %s\nSTDERR: %s", stdout.String(), stderrStr) - assert.NotContains(t, stdout.String(), "unknown flag", "error message should be on stderr, not stdout") + assert.Contains(t, res.Stderr, "unknown flag", "expected 'unknown flag' error on stderr, got:\nSTDOUT: %s\nSTDERR: %s", res.Stdout, res.Stderr) + assert.NotContains(t, res.Stdout, "unknown flag", "error message should be on stderr, not stdout") } // TestErrorOutput_MissingRequiredArg verifies that a subcommand requiring // an argument produces an error on stderr when called without one. func TestErrorOutput_MissingRequiredArg(t *testing.T) { - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "workflow", "simulate") - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() + isolatedEnv(t) + res, err := runCLI(t, []string{"workflow", "simulate"}) require.Error(t, err, "expected non-zero exit code for missing required arg") - stderrStr := stderr.String() - // Cobra may say "accepts 1 arg(s)" or "requires" depending on the command definition. - // We just verify stderr is non-empty and stdout doesn't contain the error. - assert.NotEmpty(t, stderrStr, "expected error output on stderr, got nothing.\nSTDOUT: %s", stdout.String()) + assert.NotEmpty(t, res.Stderr, "expected error output on stderr, got nothing.\nSTDOUT: %s", res.Stdout) } diff --git a/test/init_and_binding_generation_and_simulate_go_test.go b/test/init_and_binding_generation_and_simulate_go_test.go index 53cf7a60..3b8686f4 100644 --- a/test/init_and_binding_generation_and_simulate_go_test.go +++ b/test/init_and_binding_generation_and_simulate_go_test.go @@ -12,9 +12,11 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) func TestE2EInit_DevPoRTemplate(t *testing.T) { + isolatedEnv(t) tempDir := t.TempDir() projectName := "e2e-init-test" workflowName := "devPoRWorkflow" @@ -38,19 +40,8 @@ func TestE2EInit_DevPoRTemplate(t *testing.T) { "--template", templateName, "--workflow-name", workflowName, } + requireCLI(t, "cre init failed", initArgs, cretest.WithDir(tempDir)) var stdout, stderr bytes.Buffer - initCmd := exec.Command(CLIPath, initArgs...) - initCmd.Dir = tempDir - initCmd.Stdout = &stdout - initCmd.Stderr = &stderr - - require.NoError( - t, - initCmd.Run(), - "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) require.FileExists(t, filepath.Join(projectRoot, constants.DefaultEnvFileName)) @@ -105,16 +96,5 @@ func TestE2EInit_DevPoRTemplate(t *testing.T) { "--trigger-index=0", "--target=staging-settings", } - simulateCmd := exec.Command(CLIPath, simulateArgs...) - simulateCmd.Dir = projectRoot - simulateCmd.Stdout = &stdout - simulateCmd.Stderr = &stderr - - require.NoError( - t, - simulateCmd.Run(), - "cre workflow simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) + requireCLI(t, "cre workflow simulate failed", simulateArgs, cretest.WithDir(projectRoot)) } diff --git a/test/init_and_simulate_ts_test.go b/test/init_and_simulate_ts_test.go index d0e4a0e4..09aa1c4f 100644 --- a/test/init_and_simulate_ts_test.go +++ b/test/init_and_simulate_ts_test.go @@ -11,9 +11,11 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) func TestE2EInit_DevPoRTemplateTS(t *testing.T) { + isolatedEnv(t) tempDir := t.TempDir() projectName := "e2e-init-test" workflowName := "devPoRWorkflow" @@ -23,8 +25,6 @@ func TestE2EInit_DevPoRTemplateTS(t *testing.T) { ethKey := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" t.Setenv(settings.EthPrivateKeyEnvVar, ethKey) - - // Set dummy API key t.Setenv(credentials.CreApiKeyVar, "test-api") gqlSrv := NewGraphQLMockServerGetOrganization(t) @@ -37,19 +37,7 @@ func TestE2EInit_DevPoRTemplateTS(t *testing.T) { "--template", templateName, "--workflow-name", workflowName, } - var stdout, stderr bytes.Buffer - initCmd := exec.Command(CLIPath, initArgs...) - initCmd.Dir = tempDir - initCmd.Stdout = &stdout - initCmd.Stderr = &stderr - - require.NoError( - t, - initCmd.Run(), - "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) + requireCLI(t, "cre init failed", initArgs, cretest.WithDir(tempDir)) require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) require.FileExists(t, filepath.Join(projectRoot, constants.DefaultEnvFileName)) @@ -60,25 +48,13 @@ func TestE2EInit_DevPoRTemplateTS(t *testing.T) { require.FileExists(t, filepath.Join(workflowDirectory, f), "missing workflow file %q", f) } - // --- bun install in the workflow directory --- - stdout.Reset() - stderr.Reset() + var stdout, stderr bytes.Buffer bunCmd := exec.Command("bun", "install") bunCmd.Dir = workflowDirectory bunCmd.Stdout = &stdout bunCmd.Stderr = &stderr + require.NoError(t, bunCmd.Run(), "bun install failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) - require.NoError( - t, - bunCmd.Run(), - "bun install failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - // --- cre workflow simulate devPoRWorkflow --- - stdout.Reset() - stderr.Reset() simulateArgs := []string{ "workflow", "simulate", workflowName, @@ -87,16 +63,5 @@ func TestE2EInit_DevPoRTemplateTS(t *testing.T) { "--trigger-index=0", "--target=staging-settings", } - simulateCmd := exec.Command(CLIPath, simulateArgs...) - simulateCmd.Dir = projectRoot - simulateCmd.Stdout = &stdout - simulateCmd.Stderr = &stderr - - require.NoError( - t, - simulateCmd.Run(), - "cre workflow simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) + requireCLI(t, "cre workflow simulate failed", simulateArgs, cretest.WithDir(projectRoot)) } diff --git a/test/init_convert_simulate_go_test.go b/test/init_convert_simulate_go_test.go index ab7c2f96..76067fc9 100644 --- a/test/init_convert_simulate_go_test.go +++ b/test/init_convert_simulate_go_test.go @@ -14,11 +14,13 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) // TestE2EInit_ConvertToCustomBuild_Go: init (blank Go), simulate (capture), convert, make build, simulate (require match), // then add FlagProof/constA/constB/Makefile FLAG, make with FLAG=customFlag/differentFlag, simulate and assert. func TestE2EInit_ConvertToCustomBuild_Go(t *testing.T) { + isolatedEnv(t) tempDir := t.TempDir() projectName := "e2e-convert-go" workflowName := "goWorkflow" @@ -33,17 +35,13 @@ func TestE2EInit_ConvertToCustomBuild_Go(t *testing.T) { defer gqlSrv.Close() // --- cre init with blank Go template --- - var stdout, stderr bytes.Buffer - initCmd := exec.Command(CLIPath, "init", + requireCLI(t, "cre init failed", []string{"init", "--project-root", tempDir, "--project-name", projectName, "--template-id", templateID, "--workflow-name", workflowName, - ) - initCmd.Dir = tempDir - initCmd.Stdout = &stdout - initCmd.Stderr = &stderr - require.NoError(t, initCmd.Run(), "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + }, cretest.WithDir(tempDir)) + var stdout, stderr bytes.Buffer require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) require.DirExists(t, workflowDirectory) @@ -104,19 +102,15 @@ const FlagProof = "unset" func convertGoBuildWithFlagAndAssert(t *testing.T, projectRoot, workflowDir, workflowName, envVar, wantSubstr, wantSubstr2 string) { t.Helper() convertRunMakeBuild(t, workflowDir, envVar) - var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "workflow", "simulate", workflowName, + opts := []cretest.RunOption{cretest.WithDir(projectRoot)} + if envVar != "" { + opts = append(opts, cretest.WithExtraEnv(envVar)) + } + res := requireCLI(t, "simulate failed", []string{"workflow", "simulate", workflowName, "--project-root", projectRoot, "--non-interactive", "--trigger-index=0", "--target=staging-settings", - ) - cmd.Dir = projectRoot - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if envVar != "" { - cmd.Env = append(os.Environ(), envVar) - } - require.NoError(t, cmd.Run(), "simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) - require.Contains(t, stdout.String(), wantSubstr) - require.Contains(t, stdout.String(), wantSubstr2) + }, opts...) + require.Contains(t, res.Stdout, wantSubstr) + require.Contains(t, res.Stdout, wantSubstr2) } diff --git a/test/init_convert_simulate_ts_test.go b/test/init_convert_simulate_ts_test.go index 1a552f7b..b0739af6 100644 --- a/test/init_convert_simulate_ts_test.go +++ b/test/init_convert_simulate_ts_test.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) // TestE2EInit_ConvertToCustomBuild_TS: init (typescriptSimpleExample), bun install, simulate (capture), @@ -20,6 +21,7 @@ import ( // workflow-wrapper, write custom compile-to-js with define section in Bun.build, patch main.ts, Makefile. // make with FLAG=customFlag/differentFlag, simulate and assert. func TestE2EInit_ConvertToCustomBuild_TS(t *testing.T) { + isolatedEnv(t) tempDir := t.TempDir() projectName := "e2e-convert-ts" workflowName := "tsWorkflow" @@ -34,17 +36,13 @@ func TestE2EInit_ConvertToCustomBuild_TS(t *testing.T) { defer gqlSrv.Close() // --- cre init with typescriptSimpleExample --- - var stdout, stderr bytes.Buffer - initCmd := exec.Command(CLIPath, "init", + requireCLI(t, "cre init failed", []string{"init", "--project-root", tempDir, "--project-name", projectName, "--template-id", templateID, "--workflow-name", workflowName, - ) - initCmd.Dir = tempDir - initCmd.Stdout = &stdout - initCmd.Stderr = &stderr - require.NoError(t, initCmd.Run(), "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + }, cretest.WithDir(tempDir)) + var stdout, stderr bytes.Buffer require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) require.DirExists(t, workflowDirectory) @@ -134,21 +132,16 @@ build: func convertTSBuildWithFlagAndAssert(t *testing.T, projectRoot, workflowDir, workflowName, envVar, wantSubstr string) { t.Helper() convertRunMakeBuild(t, workflowDir, envVar) - var stdout, stderr bytes.Buffer workflowDirAbs, err := filepath.Abs(workflowDir) require.NoError(t, err) - cmd := exec.Command(CLIPath, "workflow", "simulate", workflowDirAbs, + opts := []cretest.RunOption{cretest.WithDir(projectRoot)} + if envVar != "" { + opts = append(opts, cretest.WithExtraEnv(envVar)) + } + res := requireCLI(t, "simulate failed", []string{"workflow", "simulate", workflowDirAbs, "--project-root", projectRoot, "--non-interactive", "--trigger-index=0", "--target=staging-settings", - ) - cmd.Dir = projectRoot - cmd.Stdout = &stdout - cmd.Stderr = &stderr - // Simulate runs CompileWorkflowToWasm which runs make build again; pass env so the rebuild uses the same FLAG - if envVar != "" { - cmd.Env = append(os.Environ(), envVar) - } - require.NoError(t, cmd.Run(), "simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) - require.Contains(t, stdout.String(), wantSubstr) + }, opts...) + require.Contains(t, res.Stdout, wantSubstr) } diff --git a/test/main_test.go b/test/main_test.go index 29590fdd..bf9f3dc7 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -7,7 +7,9 @@ import ( "path/filepath" "testing" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) // buildBinary builds the Go binary from the specified source file. @@ -45,16 +47,19 @@ func TestMain(m *testing.M) { fmt.Printf("Error while preparing binary: %s", err.Error()) os.Exit(1) } + cretest.SetCLIBinary(CLIPath) // Store contents of env vars found in the shell environment and unset them - // That way they won't leak into tests + // so they do not leak into tests or write under the developer's real ~/.cre. ethPrivateKeyValue := LookupAndUnsetEnvVar(settings.EthPrivateKeyEnvVar) + configDirValue := LookupAndUnsetEnvVar(creconfig.ConfigDirEnvVar) // Run all tests exitCode := m.Run() - // Restore env var that were previously present in this user's shell environment + // Restore env vars that were previously present in this user's shell environment RestoreEnvVar(settings.EthPrivateKeyEnvVar, ethPrivateKeyValue) + RestoreEnvVar(creconfig.ConfigDirEnvVar, configDirValue) // Exit with the appropriate code os.Exit(exitCode) diff --git a/test/multi_command_flows/account_happy_path.go b/test/multi_command_flows/account_happy_path.go index c31aba3b..5484e7f2 100644 --- a/test/multi_command_flows/account_happy_path.go +++ b/test/multi_command_flows/account_happy_path.go @@ -1,7 +1,6 @@ package multi_command_flows import ( - "bytes" "context" "crypto/sha256" "encoding/hex" @@ -10,7 +9,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "strconv" "strings" "testing" @@ -221,13 +219,8 @@ func RunAccountHappyPath(t *testing.T, tc TestConfig, testEthURL, chainName stri "-l", "owner-label-1", "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - err := cmd.Run() - out := StripANSI(stdout.String() + stderr.String()) + res, err := runCLI(t, args) + out := StripANSI(res.Combined()) // Test CLI behavior - GraphQL interaction and response parsing require.Contains(t, out, "Starting linking", "should announce linking start") @@ -263,14 +256,8 @@ func RunAccountHappyPath(t *testing.T, tc TestConfig, testEthURL, chainName stri tc.GetCliEnvFlag(), tc.GetProjectRootFlag(), } - cmd := exec.Command(CLIPath, args...) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError(t, cmd.Run(), "list-key should not fail") - - out := StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "list-key should not fail", args) + out := StripANSI(res.Combined()) require.Contains(t, out, "Workflow owners retrieved successfully", "should show success message") // Check for linked owner (if link succeeded) or empty list (if link failed at contract level) @@ -293,13 +280,8 @@ func RunAccountHappyPath(t *testing.T, tc TestConfig, testEthURL, chainName stri tc.GetProjectRootFlag(), "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - err := cmd.Run() - out := StripANSI(stdout.String() + stderr.String()) + res, err := runCLI(t, args) + out := StripANSI(res.Combined()) // Test CLI behavior for unlink require.Contains(t, out, "Starting unlinking", "should announce unlinking start") @@ -331,14 +313,8 @@ func RunAccountHappyPath(t *testing.T, tc TestConfig, testEthURL, chainName stri tc.GetCliEnvFlag(), tc.GetProjectRootFlag(), } - cmd := exec.Command(CLIPath, args...) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError(t, cmd.Run(), "list-key should not fail") - - out := StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "list-key should not fail", args) + out := StripANSI(res.Combined()) require.Contains(t, out, "Workflow owners retrieved successfully", "should show success message") // After unlink, should show no linked owners diff --git a/test/multi_command_flows/cli_run.go b/test/multi_command_flows/cli_run.go new file mode 100644 index 00000000..be651c60 --- /dev/null +++ b/test/multi_command_flows/cli_run.go @@ -0,0 +1,21 @@ +package multi_command_flows + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" +) + +func runCLI(t *testing.T, args []string, opts ...cretest.RunOption) (cretest.Result, error) { + t.Helper() + return cretest.RunCLI(t, "", args, opts...) +} + +func requireCLI(t *testing.T, msg string, args []string, opts ...cretest.RunOption) cretest.Result { + t.Helper() + res, err := runCLI(t, args, opts...) + require.NoError(t, err, "%s:\nSTDOUT:\n%s\nSTDERR:\n%s", msg, res.Stdout, res.Stderr) + return res +} diff --git a/test/multi_command_flows/secrets_happy_path.go b/test/multi_command_flows/secrets_happy_path.go index b8ce7f1a..d80728ec 100644 --- a/test/multi_command_flows/secrets_happy_path.go +++ b/test/multi_command_flows/secrets_happy_path.go @@ -1,12 +1,10 @@ package multi_command_flows import ( - "bytes" "encoding/json" "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -317,15 +315,8 @@ func secretsListMsig(t *testing.T, tc TestConfig) string { "--unsigned", "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - _ = cmd.Run() - out := stdout.String() + stderr.String() + res, _ := runCLI(t, args) + out := res.Combined() return StripANSI(out) } @@ -360,15 +351,8 @@ func secretsCreateEoa(t *testing.T, tc TestConfig) (bool, string) { tc.GetProjectRootFlag(), "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - _ = cmd.Run() - out := stdout.String() + stderr.String() + res, _ := runCLI(t, args) + out := res.Combined() allowed := strings.Contains(out, "Digest allowlisted; proceeding to gateway POST") || strings.Contains(out, "Digest already allowlisted; skipping on-chain allowlist") @@ -406,15 +390,8 @@ func secretsUpdateEoa(t *testing.T, tc TestConfig) (bool, string) { tc.GetProjectRootFlag(), "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - _ = cmd.Run() - out := stdout.String() + stderr.String() + res, _ := runCLI(t, args) + out := res.Combined() allowed := strings.Contains(out, "Digest allowlisted; proceeding to gateway POST") || strings.Contains(out, "Digest already allowlisted; skipping on-chain allowlist") @@ -435,15 +412,8 @@ func secretsListEoa(t *testing.T, tc TestConfig, ns string) (bool, string) { tc.GetProjectRootFlag(), "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - _ = cmd.Run() - out := stdout.String() + stderr.String() + res, _ := runCLI(t, args) + out := res.Combined() allowed := strings.Contains(out, "Digest allowlisted; proceeding to gateway POST") || strings.Contains(out, "Digest already allowlisted; skipping on-chain allowlist") @@ -475,14 +445,8 @@ func secretsDeleteEoa(t *testing.T, tc TestConfig, ns string) (bool, string) { tc.GetProjectRootFlag(), "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - _ = cmd.Run() - - out := stdout.String() + stderr.String() + res, _ := runCLI(t, args) + out := res.Combined() allowed := strings.Contains(out, "Digest allowlisted; proceeding to gateway POST") || strings.Contains(out, "Digest already allowlisted; skipping on-chain allowlist") return allowed, StripANSI(out) diff --git a/test/multi_command_flows/workflow_happy_path_1.go b/test/multi_command_flows/workflow_happy_path_1.go index 236cb652..f29fd79d 100644 --- a/test/multi_command_flows/workflow_happy_path_1.go +++ b/test/multi_command_flows/workflow_happy_path_1.go @@ -1,12 +1,9 @@ package multi_command_flows import ( - "bytes" "encoding/json" "net/http" "net/http/httptest" - "os" - "os/exec" "regexp" "strings" "testing" @@ -25,14 +22,6 @@ type TestConfig interface { GetProjectRootFlag() string } -// CLI path for testing -var CLIPath = os.TempDir() + string(os.PathSeparator) + "cre" + func() string { - if os.PathSeparator == '\\' { - return ".exe" - } - return "" -}() - // Regular expression to strip ANSI escape codes from output var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`) @@ -148,21 +137,8 @@ func workflowDeployEoaWithMockStorage(t *testing.T, tc TestConfig) (output strin "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow deploy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - output = StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "cre workflow deploy failed", args) + output = StripANSI(res.Combined()) return } @@ -183,21 +159,8 @@ func workflowPauseEoa(t *testing.T, tc TestConfig, gqlURL string) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // CLI will handle context switching automatically - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow pause failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - return StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "cre workflow pause failed", args) + return StripANSI(res.Combined()) } // workflowActivateEoa activates the workflow (by owner+name) via CLI. @@ -217,21 +180,8 @@ func workflowActivateEoa(t *testing.T, tc TestConfig, gqlURL string) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // CLI will handle context switching automatically - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow activate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - return StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "cre workflow activate failed", args) + return StripANSI(res.Combined()) } // workflowDeleteEoa deletes for the current owner+name via CLI (non-interactive). @@ -251,21 +201,8 @@ func workflowDeleteEoa(t *testing.T, tc TestConfig, gqlURL string) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // CLI will handle context switching automatically - - 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(), - ) - - return StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "cre workflow delete failed", args) + return StripANSI(res.Combined()) } // RunHappyPath1Workflow runs the complete happy path 1 workflow: diff --git a/test/multi_command_flows/workflow_happy_path_2.go b/test/multi_command_flows/workflow_happy_path_2.go index eed85044..e6b44c7d 100644 --- a/test/multi_command_flows/workflow_happy_path_2.go +++ b/test/multi_command_flows/workflow_happy_path_2.go @@ -1,12 +1,10 @@ package multi_command_flows import ( - "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "testing" @@ -119,23 +117,8 @@ func workflowDeployEoa(t *testing.T, tc TestConfig) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow deploy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - out := StripANSI(stdout.String() + stderr.String()) - - return out + res := requireCLI(t, "cre workflow deploy failed", args) + return StripANSI(res.Combined()) } // workflowDeployUpdateWithConfig deploys a workflow update with config via CLI, mocking GraphQL + Origin. @@ -237,23 +220,8 @@ func workflowDeployUpdateWithConfig(t *testing.T, tc TestConfig) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow deploy update failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - out := StripANSI(stdout.String() + stderr.String()) - - return out + res := requireCLI(t, "cre workflow deploy update failed", args) + return StripANSI(res.Combined()) } // RunHappyPath2Workflow runs the complete happy path 2 workflow: diff --git a/test/multi_command_flows/workflow_happy_path_3.go b/test/multi_command_flows/workflow_happy_path_3.go index 5e125e74..8c7c09dd 100644 --- a/test/multi_command_flows/workflow_happy_path_3.go +++ b/test/multi_command_flows/workflow_happy_path_3.go @@ -1,12 +1,10 @@ package multi_command_flows import ( - "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "testing" @@ -17,6 +15,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" ) // workflowInit runs cre init to initialize a new workflow project from scratch @@ -61,25 +60,10 @@ func workflowInit(t *testing.T, projectRootFlag, projectName, workflowName strin "--template", "hello-world-go", // Use the built-in Go template } - cmd := exec.Command(CLIPath, args...) - - // Set working directory to where the project should be created parts := strings.Split(projectRootFlag, "=") require.Len(t, parts, 2, "invalid project root flag format") - cmd.Dir = parts[1] - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - output = StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "cre init failed", args, cretest.WithDir(parts[1])) + output = StripANSI(res.Combined()) return } @@ -187,16 +171,8 @@ func workflowDeployUnsigned(t *testing.T, tc TestConfig, projectRootFlag, workfl "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - err := cmd.Run() - out := StripANSI(stdout.String() + stderr.String()) - - return out, err + res, err := runCLI(t, args) + return StripANSI(res.Combined()), err } // workflowDeployWithConfigAndLinkedKey deploys a workflow with config using a pre-linked address @@ -299,23 +275,8 @@ func workflowDeployWithConfigAndLinkedKey(t *testing.T, tc TestConfig, projectRo "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - // Let CLI handle context switching - don't set cmd.Dir manually - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow deploy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - out := StripANSI(stdout.String() + stderr.String()) - - return out + res := requireCLI(t, "cre workflow deploy failed", args) + return StripANSI(res.Combined()) } // updateProjectSettings updates the project.yaml file with test settings diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 8813c4f3..132b846d 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -1,14 +1,11 @@ package multi_command_flows import ( - "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" - "os/exec" - "path/filepath" "strings" "sync/atomic" "testing" @@ -19,13 +16,13 @@ import ( "github.com/smartcontractkit/cre-cli/internal/authvalidation" "github.com/smartcontractkit/cre-cli/internal/constants" - "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/cretest" "github.com/smartcontractkit/cre-cli/internal/testutil/testjwt" ) @@ -56,79 +53,6 @@ func mockGetCreOrganizationInfoGraphQLPayload() map[string]any { } } -// CreateTestBearerCredentialsHome writes JWT bearer credentials under the CLI config directory for subprocess CLI tests. -func CreateTestBearerCredentialsHome(t *testing.T) string { - t.Helper() - - homeDir := t.TempDir() - creDir := filepath.Join(homeDir, creconfig.Dir) - require.NoError(t, os.MkdirAll(creDir, 0o700), "failed to create config dir") - - jwt := createTestJWT("test-org-id") - creConfig := "AccessToken: " + jwt + "\n" + - "IDToken: test-id-token\n" + - "RefreshToken: test-refresh-token\n" + - "ExpiresIn: 3600\n" + - "TokenType: Bearer\n" - - require.NoError(t, os.WriteFile(filepath.Join(creDir, credentials.ConfigFile), []byte(creConfig), 0o600), "failed to write test credentials") - - return homeDir -} - -// realGoCacheEnv returns GOPATH and GOMODCACHE locations outside t.TempDir()-backed HOME dirs. -// Overriding HOME makes Go default GOPATH to $HOME/go; module files are read-only and break TempDir cleanup. -func realGoCacheEnv(t *testing.T) (gopath, gomodcache string) { - t.Helper() - - realHome, err := os.UserHomeDir() - require.NoError(t, err, "failed to get real home dir") - - gopath = os.Getenv("GOPATH") - if gopath == "" { - gopath = filepath.Join(realHome, "go") - } - - gomodcache = os.Getenv("GOMODCACHE") - if gomodcache == "" { - gomodcache = filepath.Join(gopath, "pkg", "mod") - } - - return gopath, gomodcache -} - -// pinGoCacheForTestHome keeps module cache out of temp HOME directories in the test process. -func pinGoCacheForTestHome(t *testing.T) { - t.Helper() - gopath, gomodcache := realGoCacheEnv(t) - t.Setenv("GOPATH", gopath) - t.Setenv("GOMODCACHE", gomodcache) -} - -// cliChildEnv builds subprocess env with isolated HOME for credentials and pinned Go cache paths. -func cliChildEnv(t *testing.T, testHome string) []string { - t.Helper() - gopath, gomodcache := realGoCacheEnv(t) - - childEnv := make([]string, 0, len(os.Environ())+4) - for _, entry := range os.Environ() { - if strings.HasPrefix(entry, "HOME=") || - strings.HasPrefix(entry, "USERPROFILE=") || - strings.HasPrefix(entry, "GOPATH=") || - strings.HasPrefix(entry, "GOMODCACHE=") { - continue - } - childEnv = append(childEnv, entry) - } - childEnv = append(childEnv, - "HOME="+testHome, - "USERPROFILE="+testHome, - "GOPATH="+gopath, - "GOMODCACHE="+gomodcache, - ) - return childEnv -} - func createTestJWT(orgID string) string { return testjwt.CreateTestJWT(orgID) } @@ -295,25 +219,12 @@ func workflowDeployPrivateRegistry(t *testing.T, tc TestConfig) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - testHome := CreateTestBearerCredentialsHome(t) - cmd.Env = cliChildEnv(t, testHome) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow deploy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) + res := requireCLI(t, "cre workflow deploy failed", args, cretest.WithBearerCredentials("test-org-id")) require.True(t, presignedPostCalled.Load(), "expected GeneratePresignedPostUrlForArtifact to be called") require.True(t, uploadCalled.Load(), "expected artifact upload endpoint to be called") require.True(t, upsertCalled.Load(), "expected UpsertOffchainWorkflow to be called") - return StripANSI(stdout.String() + stderr.String()) + return StripANSI(res.Combined()) } // RunWorkflowPrivateRegistryHappyPath runs the workflow deploy happy path for private registry. @@ -454,24 +365,11 @@ func workflowPausePrivateRegistry(t *testing.T, tc TestConfig) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - testHome := CreateTestBearerCredentialsHome(t) - cmd.Env = cliChildEnv(t, testHome) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow pause failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) + res := requireCLI(t, "cre workflow pause failed", args, cretest.WithBearerCredentials("test-org-id")) require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") require.True(t, pauseWorkflowCalled.Load(), "expected PauseOffchainWorkflow to be called") - return StripANSI(stdout.String() + stderr.String()) + return StripANSI(res.Combined()) } // RunWorkflowPausePrivateRegistryHappyPath runs the workflow pause happy path for private registry. @@ -609,24 +507,11 @@ func workflowActivatePrivateRegistry(t *testing.T, tc TestConfig) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - testHome := CreateTestBearerCredentialsHome(t) - cmd.Env = cliChildEnv(t, testHome) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow activate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) + res := requireCLI(t, "cre workflow activate failed", args, cretest.WithBearerCredentials("test-org-id")) require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") require.True(t, activateWorkflowCalled.Load(), "expected ActivateOffchainWorkflow to be called") - return StripANSI(stdout.String() + stderr.String()) + return StripANSI(res.Combined()) } // RunWorkflowActivatePrivateRegistryHappyPath runs the workflow activate happy path for private registry. @@ -752,24 +637,11 @@ func workflowDeletePrivateRegistry(t *testing.T, tc TestConfig) string { "--" + settings.Flags.SkipConfirmation.Name, } - cmd := exec.Command(CLIPath, args...) - testHome := CreateTestBearerCredentialsHome(t) - cmd.Env = cliChildEnv(t, testHome) - - 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(), - ) + res := requireCLI(t, "cre workflow delete failed", args, cretest.WithBearerCredentials("test-org-id")) 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()) + return StripANSI(res.Combined()) } // RunWorkflowDeletePrivateRegistryHappyPath runs the workflow delete happy path for private registry. @@ -843,10 +715,9 @@ func RunPrivateRegistryAuthAndSettingsFinalize(t *testing.T, envPath, blankWorkf defer orgSrv.Close() t.Setenv(environments.EnvVarGraphQLURL, orgSrv.URL+"/graphql") - bearerHome := CreateTestBearerCredentialsHome(t) - t.Setenv("HOME", bearerHome) - t.Setenv("USERPROFILE", bearerHome) - pinGoCacheForTestHome(t) + env := cretest.NewEnv(t) + cretest.SeedBearerCredentials(t, env.ConfigDir, "test-org-id") + cretest.PinGoCacheForProcess(t) logger := testutil.NewTestLogger() creds, err := credentials.New(logger) diff --git a/test/multi_command_flows/workflow_simulator_path.go b/test/multi_command_flows/workflow_simulator_path.go index 3d64cd43..153c38f3 100644 --- a/test/multi_command_flows/workflow_simulator_path.go +++ b/test/multi_command_flows/workflow_simulator_path.go @@ -1,12 +1,10 @@ package multi_command_flows import ( - "bytes" "encoding/json" "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "testing" "time" @@ -100,20 +98,8 @@ func RunSimulationHappyPath(t *testing.T, tc TestConfig, projectDir string) { "--target=staging-settings", } - cmd := exec.Command(CLIPath, args...) - - var stdout, stderr bytes.Buffer - cmd.Stdout, cmd.Stderr = &stdout, &stderr - - require.NoError( - t, - cmd.Run(), - "cre workflow simulation failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - out := StripANSI(stdout.String() + stderr.String()) + res := requireCLI(t, "cre workflow simulation failed", args) + out := StripANSI(res.Combined()) require.Contains(t, out, "Workflow compiled", "expected workflow to compile.\nCLI OUTPUT:\n%s", out) require.Contains(t, out, "[SIMULATION] Simulator Initialized", "expected workflow to initialize.\nCLI OUTPUT:\n%s", out) diff --git a/test/multi_command_test.go b/test/multi_command_test.go index c316d3b6..f5fe7e5f 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -2,8 +2,8 @@ package test import ( "fmt" + "os" "path/filepath" - "sync" "testing" "github.com/spf13/viper" @@ -17,231 +17,166 @@ import ( "github.com/smartcontractkit/cre-cli/test/multi_command_flows" ) -// Mutex to ensure all multi-command tests run sequentially to avoid context conflicts -var multiCommandTestMutex sync.Mutex +func setupAnvilWorkflowRegistry(t *testing.T) (*os.Process, string) { + t.Helper() + anvilProc, testEthURL := initTestEnv(t, "anvil-state.json") + t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") + t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) + return anvilProc, testEthURL +} -// TestMultiCommandHappyPaths runs all multi-command happy path tests sequentially -// to ensure they don't conflict with each other's context changes -func TestMultiCommandHappyPaths(t *testing.T) { - // Ensure sequential execution to avoid context conflicts - multiCommandTestMutex.Lock() - defer multiCommandTestMutex.Unlock() +func TestWorkflow_HappyPath1_DeployPauseActivateDelete(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := setupAnvilWorkflowRegistry(t) + defer StopAnvil(anvilProc) - // Run Happy Path 1: Deploy -> Pause -> Activate -> Delete - t.Run("HappyPath1_DeployPauseActivateDelete", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) + t.Setenv(credentials.CreApiKeyVar, "test-api") - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3)) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "happy-path-1-workflow", "", "blank_workflow")) + t.Cleanup(tc.Cleanup(t)) - // Setup environment variables for pre-baked registries from Anvil state dump - t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") - t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) + multi_command_flows.RunHappyPath1Workflow(t, tc) +} - tc := NewTestConfig(t) +func TestWorkflow_HappyPath2_DeployUpdateWithConfig(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := setupAnvilWorkflowRegistry(t) + defer StopAnvil(anvilProc) - // Use linked Address3 + its key - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "happy-path-1-workflow", "", "blank_workflow"), "failed to create workflow directory") - t.Cleanup(tc.Cleanup(t)) + t.Setenv(credentials.CreApiKeyVar, "test-api") - // Run happy path 1 workflow - multi_command_flows.RunHappyPath1Workflow(t, tc) - }) + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3)) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "happy-path-2-workflow", "", "blank_workflow")) + t.Cleanup(tc.Cleanup(t)) - // Run Happy Path 2: Deploy -> Deploy update with config - t.Run("HappyPath2_DeployUpdateWithConfig", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) + multi_command_flows.RunHappyPath2Workflow(t, tc) +} - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") +func TestWorkflow_HappyPath3a_InitDeployAutoLink(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := setupAnvilWorkflowRegistry(t) + defer StopAnvil(anvilProc) - // Setup environment variables for pre-baked registries from Anvil state dump - t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") - t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) + t.Setenv(credentials.CreApiKeyVar, "test-api") + t.Setenv("ETH_URL", testEthURL) - tc := NewTestConfig(t) + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey4)) + t.Cleanup(tc.Cleanup(t)) - // Use linked Address3 + its key - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "happy-path-2-workflow", "", "blank_workflow"), "failed to create workflow directory") - t.Cleanup(tc.Cleanup(t)) + multi_command_flows.RunHappyPath3aWorkflow(t, tc, "happy-path-3a-project", constants.TestAddress4, testEthURL) +} - // Run happy path 2 workflow - multi_command_flows.RunHappyPath2Workflow(t, tc) - }) +func TestWorkflow_HappyPath3b_DeployWithConfig(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := setupAnvilWorkflowRegistry(t) + defer StopAnvil(anvilProc) - // Run Happy Path 3a: Init -> Deploy with unlinked key (tests auto-link initiation) - t.Run("HappyPath3a_InitDeployAutoLink", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) + t.Setenv(credentials.CreApiKeyVar, "test-api") - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3)) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "happy-path-3b-workflow", "./config.json", "blank_workflow")) + t.Cleanup(tc.Cleanup(t)) - // Setup environment variables for pre-baked registries from Anvil state dump - t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") - t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) - // Set the ETH RPC URL for the init command to use - t.Setenv("ETH_URL", testEthUrl) + multi_command_flows.RunHappyPath3bWorkflow(t, tc) +} - tc := NewTestConfig(t) +func TestWorkflow_PrivateRegistry_E2E(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := setupAnvilWorkflowRegistry(t) + defer StopAnvil(anvilProc) - // Use UNlinked Address4 + its key to test auto-link feature - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey4), "failed to create env file") - t.Cleanup(tc.Cleanup(t)) + t.Setenv(environments.EnvVarEnv, "STAGING") + t.Setenv(environments.EnvVarDonFamily, "test-don") - // Run happy path 3a - init + deploy with auto-link initiation (uses --unsigned) - multi_command_flows.RunHappyPath3aWorkflow(t, tc, "happy-path-3a-project", constants.TestAddress4, testEthUrl) - }) + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, "")) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "private-registry-happy-path-workflow", "", "blank_workflow")) - // Run Happy Path 3b: Deploy with linked key + config - t.Run("HappyPath3b_DeployWithConfig", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) + v := viper.New() + v.SetConfigFile(filepath.Join(tc.ProjectDirectory, "blank_workflow", constants.DefaultWorkflowSettingsFileName)) + require.NoError(t, v.ReadInConfig()) + v.Set(fmt.Sprintf("%s.user-workflow.deployment-registry", SettingsTarget), "reg-test") + require.NoError(t, v.WriteConfig()) - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") + t.Cleanup(tc.Cleanup(t)) - // Setup environment variables for pre-baked registries from Anvil state dump - t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") - t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) + multi_command_flows.RunPrivateRegistryE2E(t, tc, tc.EnvFile, filepath.Join(tc.ProjectDirectory, "blank_workflow")) +} - tc := NewTestConfig(t) +func TestAccount_HappyPath_LinkListUnlinkList(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := setupAnvilWorkflowRegistry(t) + defer StopAnvil(anvilProc) - // Use linked Address3 + its key - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "happy-path-3b-workflow", "./config.json", "blank_workflow"), "failed to create workflow directory with config") - t.Cleanup(tc.Cleanup(t)) + t.Setenv(credentials.CreApiKeyVar, "test-api") - // Run happy path 3b - deploy with linked key + config - multi_command_flows.RunHappyPath3bWorkflow(t, tc) - }) + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey4)) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + t.Cleanup(tc.Cleanup(t)) - // Private registry (off-chain): no CRE_ETH_PRIVATE_KEY; org-derived owner from mock GQL, - // settings load + finalize, then full CLI lifecycle (see multi_command_flows.RunPrivateRegistryE2E). - t.Run("WorkflowPrivateRegistry_E2E", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) - - t.Setenv(environments.EnvVarEnv, "STAGING") + multi_command_flows.RunAccountHappyPath(t, tc, testEthURL, chainselectors.ANVIL_DEVNET.Name) +} - t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") - t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) - t.Setenv(environments.EnvVarDonFamily, "test-don") +func TestSecrets_HappyPath_CreateUpdateListDelete(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := initTestEnv(t, "anvil-state.json") + defer StopAnvil(anvilProc) - tc := NewTestConfig(t) + t.Setenv(credentials.CreApiKeyVar, "test-api") + t.Setenv("TESTID_ENV", "testval") + t.Setenv("TESTID_ENV_UPDATED", "testval2") - require.NoError(t, createCliEnvFile(tc.EnvFile, ""), "failed to create env file without private key") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "private-registry-happy-path-workflow", "", "blank_workflow"), "failed to create workflow directory") + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3)) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + t.Cleanup(tc.Cleanup(t)) - v := viper.New() - v.SetConfigFile(filepath.Join(tc.ProjectDirectory, "blank_workflow", constants.DefaultWorkflowSettingsFileName)) - require.NoError(t, v.ReadInConfig()) - v.Set(fmt.Sprintf("%s.user-workflow.deployment-registry", SettingsTarget), "reg-test") - require.NoError(t, v.WriteConfig()) + multi_command_flows.RunSecretsHappyPath(t, tc, chainselectors.ANVIL_DEVNET.Name) +} - t.Cleanup(tc.Cleanup(t)) - - multi_command_flows.RunPrivateRegistryE2E(t, tc, tc.EnvFile, filepath.Join(tc.ProjectDirectory, "blank_workflow")) - }) - - // Run Account Happy Path: Link -> List -> Unlink -> List (verify unlinked) - t.Run("AccountHappyPath_LinkListUnlinkList", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) - - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") - - // Setup environment variables for pre-baked registries from Anvil state dump - t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") - t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) - - tc := NewTestConfig(t) - - // Use test address for this test - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey4), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - t.Cleanup(tc.Cleanup(t)) +func TestSecrets_ListMsig(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := initTestEnv(t, "anvil-state.json") + defer StopAnvil(anvilProc) - // Run account happy path workflow - multi_command_flows.RunAccountHappyPath(t, tc, testEthUrl, chainselectors.ANVIL_DEVNET.Name) - }) + t.Setenv(credentials.CreApiKeyVar, "test-api") + t.Setenv("TESTID_ENV", "testval") + t.Setenv("TESTID_ENV_UPDATED", "testval2") - // Run Secrets Happy Path: Create -> Update -> List -> Delete - t.Run("SecretsHappyPath_CreateUpdateListDelete", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, "")) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", constants.TestAddress3, testEthURL)) + t.Cleanup(tc.Cleanup(t)) - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") - t.Setenv("TESTID_ENV", "testval") - t.Setenv("TESTID_ENV_UPDATED", "testval2") - - tc := NewTestConfig(t) + multi_command_flows.RunSecretsListMsig(t, tc, chainselectors.ANVIL_DEVNET.Name) +} - // Use linked Address3 + its key - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - t.Cleanup(tc.Cleanup(t)) - - // Run secrets happy path workflow - multi_command_flows.RunSecretsHappyPath(t, tc, chainselectors.ANVIL_DEVNET.Name) - }) - - // Run Secrets List with Unsigned - t.Run("SecretsListMsig", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") - defer StopAnvil(anvilProc) - - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") - t.Setenv("TESTID_ENV", "testval") - t.Setenv("TESTID_ENV_UPDATED", "testval2") +func TestWorkflow_SimulationHappyPath(t *testing.T) { + isolatedEnv(t) + anvilProc, testEthURL := initTestEnv(t, "anvil-state-simulator.json") + defer StopAnvil(anvilProc) - tc := NewTestConfig(t) - - // Use linked Address3 as owner, but no private key - require.NoError(t, createCliEnvFile(tc.EnvFile, ""), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", constants.TestAddress3, testEthUrl), "failed to create project.yaml") - t.Cleanup(tc.Cleanup(t)) - - // Run secrets list unsigned - multi_command_flows.RunSecretsListMsig(t, tc, chainselectors.ANVIL_DEVNET.Name) - }) - - // Run simulation - t.Run("SimulationHappyPath", func(t *testing.T) { - anvilProc, testEthUrl := initTestEnv(t, "anvil-state-simulator.json") - defer StopAnvil(anvilProc) + const sepoliaForwarder = "0x15fC6ae953E024d975e77382eEeC56A9101f9F88" + code := readDeployedBytecodeHex(t, "MockKeystoneForwarder.json") + anvilSetCode(t, testEthURL, sepoliaForwarder, code) - // Etch the MockKeystoneForwarder runtime at the supported Sepolia forwarder addr - const sepoliaForwarder = "0x15fC6ae953E024d975e77382eEeC56A9101f9F88" - code := readDeployedBytecodeHex( - t, - "MockKeystoneForwarder.json", - ) - anvilSetCode(t, testEthUrl, sepoliaForwarder, code) + t.Setenv(credentials.CreApiKeyVar, "test-api") - // Set dummy API key for authentication - t.Setenv(credentials.CreApiKeyVar, "test-api") - - tc := NewTestConfig(t) + tc := NewTestConfig(t) + require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3)) + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthURL)) + require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "workflow-simulate", "config.json", "por_workflow")) + t.Cleanup(tc.Cleanup(t)) - // Use linked Address3 + its key - require.NoError(t, createCliEnvFile(tc.EnvFile, constants.TestPrivateKey3), "failed to create env file") - require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") - require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "workflow-simulate", "config.json", "por_workflow"), "failed to create workflow directory") - t.Cleanup(tc.Cleanup(t)) - - // Run simulation happy path workflow - multi_command_flows.RunSimulationHappyPath(t, tc, tc.ProjectDirectory) - }) + multi_command_flows.RunSimulationHappyPath(t, tc, tc.ProjectDirectory) }