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/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/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/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..dcdc95af 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) @@ -94,6 +98,7 @@ func New(ctx *runtime.Context) *cobra.Command { } settings.AddTxnTypeFlags(cmd) + settings.AddSkipConfirmation(cmd) return cmd } 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/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/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/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..17aba9c4 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" @@ -13,6 +12,7 @@ import ( "sigs.k8s.io/yaml" "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). @@ -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 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 71974060..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" @@ -315,16 +313,10 @@ 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 - - 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) } @@ -359,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") @@ -405,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") @@ -434,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") @@ -474,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) }