diff --git a/test/cli_test.go b/test/cli_test.go index c4ed363f..1622dcfc 100644 --- a/test/cli_test.go +++ b/test/cli_test.go @@ -2,9 +2,11 @@ package test import ( "bufio" + "bytes" "errors" "fmt" "os" + "os/exec" "path/filepath" "runtime" "strconv" @@ -115,8 +117,13 @@ func createWorkflowDirectory( return fmt.Errorf("failed to create workflow directory: %w", err) } - // Copy workflow files - items := []string{"main.go", "config.json", "go.mod", "go.sum", "contracts", "secrets.yaml"} + // Copy workflow files based on the workflowDirectoryName + var items []string + if strings.HasSuffix(workflowDirectoryName, "_ts") { + items = []string{"main.ts", "config.json", "package.json", "tsconfig.json", "contracts", "secrets.yaml"} + } else { + items = []string{"main.go", "config.json", "go.mod", "go.sum", "contracts", "secrets.yaml"} + } for _, item := range items { src := filepath.Join(sourceWorkflowDir, item) dst := filepath.Join(workflowDir, item) @@ -142,9 +149,13 @@ func createWorkflowDirectory( // user-workflow fields v.Set(fmt.Sprintf("%s.user-workflow.workflow-name", SettingsTarget), trimmedName) + workflowArtifacts := make(map[string]string) // workflow-artifacts - initially create without config-path for first deployment - workflowArtifacts := map[string]string{ - "workflow-path": "./main.go", + // if workflowDirectoryName has _ts suffix, set workflow-path to ./main.ts + if strings.HasSuffix(workflowDirectoryName, "_ts") { + workflowArtifacts["workflow-path"] = "./main.ts" + } else { + workflowArtifacts["workflow-path"] = "./main.go" } // Add secrets-path if secrets.yaml exists @@ -164,6 +175,25 @@ func createWorkflowDirectory( return fmt.Errorf("error writing workflow.yaml: %w", err) } + // if TS then run `bun install` + if strings.HasSuffix(workflowDirectoryName, "_ts") { + bunCmd := exec.Command("bun", "install") + bunCmd.Dir = workflowDir + var stdout, stderr bytes.Buffer + bunCmd.Stdout, bunCmd.Stderr = &stdout, &stderr + + err := bunCmd.Run() + + output := stdout.String() + stderr.String() + L.Debug(). + Str("BunInstallOutput", output). + Msg("Bun install output") + + if err != nil { + return fmt.Errorf("failed to run bun install: %w", err) + } + } + L.Debug(). Str("WorkflowSettingsFile", workflowSettingsPath). Interface("Config", v.AllSettings()). diff --git a/test/multi_command_flows/workflow_happy_path_4.go b/test/multi_command_flows/workflow_happy_path_4.go new file mode 100644 index 00000000..e898f7f9 --- /dev/null +++ b/test/multi_command_flows/workflow_happy_path_4.go @@ -0,0 +1,166 @@ +package multi_command_flows + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// workflowDeployEoaWithMockStorage deploys a workflow via CLI, mocking GraphQL + Origin. +func setupMock(t *testing.T, tc TestConfig) (output string, gqlURL string) { + t.Helper() + + var srv *httptest.Server + // One server that handles both GraphQL and "origin" uploads. + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + // Handle authentication validation query + if strings.Contains(req.Query, "getOrganization") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOrganization": map[string]any{ + "organizationId": "test-org-id", + }, + }, + }) + return + } + + // Respond based on the mutation in the query + if strings.Contains(req.Query, "GeneratePresignedPostUrlForArtifact") { + // Return presigned POST URL + fields (pointing back to this server) + resp := map[string]any{ + "data": map[string]any{ + "generatePresignedPostUrlForArtifact": map[string]any{ + "presignedPostUrl": srv.URL + "/upload", + "presignedPostFields": []map[string]string{{"key": "k1", "value": "v1"}}, + }, + }, + } + _ = json.NewEncoder(w).Encode(resp) + return + } + if strings.Contains(req.Query, "GenerateUnsignedGetUrlForArtifact") { + resp := map[string]any{ + "data": map[string]any{ + "generateUnsignedGetUrlForArtifact": map[string]any{ + "unsignedGetUrl": srv.URL + "/get", + }, + }, + } + _ = json.NewEncoder(w).Encode(resp) + return + } + if strings.Contains(req.Query, "listWorkflowOwners") { + // Mock response for link verification check + resp := map[string]any{ + "data": map[string]any{ + "listWorkflowOwners": map[string]any{ + "linkedOwners": []map[string]string{ + { + "workflowOwnerAddress": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "verificationStatus": "VERIFICATION_STATUS_SUCCESSFULL", //nolint:misspell // Intentional misspelling to match external API + }, + }, + }, + }, + } + _ = json.NewEncoder(w).Encode(resp) + return + } + // Fallback error + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + case r.URL.Path == "/upload" && r.Method == http.MethodPost: + // Accept origin "upload" (presigned POST target) + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("OK")) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + // Note: Server is NOT closed here - caller is responsible for keeping it alive + // across multiple commands. The server should be closed at the end of the test. + + // Point the CLI at our mock GraphQL endpoint + gqlURL = srv.URL + "/graphql" + t.Setenv(environments.EnvVarGraphQLURL, gqlURL) + + return +} + +func deployWorkflow(t *testing.T, tc TestConfig, workflowName string) (output string, err error) { + // Build CLI args - CLI will automatically resolve workflow path using new context system + args := []string{ + "workflow", "deploy", + workflowName, + tc.GetCliEnvFlag(), + 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 + + err = cmd.Run() + + output = StripANSI(stdout.String() + stderr.String()) + return +} + +// DuplicateDeployRejected deploys a workflow and then attempts to deploy it again, confirming that it fails. +func DuplicateDeployRejected(t *testing.T, tc TestConfig, workflowName string) { + t.Helper() + + // Set dummy API key + t.Setenv(credentials.CreApiKeyVar, "test-api") + + setupMock(t, tc) + + // Deploy with mocked storage - this creates the server and returns the GraphQL URL + out, err := deployWorkflow(t, tc, workflowName) + if err != nil { + t.Fatalf("failed to deploy workflow: %v", err) + } + require.Contains(t, out, "Workflow compiled", "expected workflow to compile.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "linked=true", "expected link-status true.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Uploaded binary", "expected binary upload to succeed.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow deployed successfully", "expected deployment success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Preparing transaction for workflowID:", "expected transaction preparation.\nCLI OUTPUT:\n%s", out) + // extract the workflowID from the output (only the ID, not following lines) + afterPrefix := strings.Split(out, "Preparing transaction for workflowID:")[1] + workflowID := strings.TrimSpace(strings.Split(afterPrefix, "\n")[0]) + t.Logf("workflowID: %s", workflowID) + require.NotEmpty(t, workflowID, "expected workflowID to be not empty.\nCLI OUTPUT:\n%s", out) + + // deploy workflow again and confirm it fails + out2, _ := deployWorkflow(t, tc, workflowName) // ignore error, we expect it to fail + require.Contains(t, out2, "workflow with id "+workflowID+" already exists", "expected workflow to be already deployed.\nCLI OUTPUT:\n%s", out2) +} diff --git a/test/multi_command_test.go b/test/multi_command_test.go index f03f6fbb..4bf55e2e 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -212,4 +212,51 @@ func TestMultiCommandHappyPaths(t *testing.T) { // Run simulation happy path workflow multi_command_flows.RunSimulationHappyPath(t, tc, tc.ProjectDirectory) }) + + // Run Happy Path 4: Deploy -> Deploy again and confirm failure + t.Run("HappyPath4_DuplicateDeployRejectedGoLang", 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 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-4-workflow", "", "blank_workflow"), "failed to create workflow directory") + t.Cleanup(tc.Cleanup(t)) + + // Run happy path 4 workflow + multi_command_flows.DuplicateDeployRejected(t, tc, "blank_workflow") + }) + + t.Run("HappyPath4_DuplicateDeployRejectedTypescript", 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 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-4-workflow", "", "blank_workflow_ts"), "failed to create workflow directory") + t.Cleanup(tc.Cleanup(t)) + + // Run happy path 4 workflow + multi_command_flows.DuplicateDeployRejected(t, tc, "blank_workflow_ts") + }) } diff --git a/test/test_project/blank_workflow_ts/config.json b/test/test_project/blank_workflow_ts/config.json new file mode 100644 index 00000000..1a360cb3 --- /dev/null +++ b/test/test_project/blank_workflow_ts/config.json @@ -0,0 +1,3 @@ +{ + "schedule": "*/30 * * * * *" +} diff --git a/test/test_project/blank_workflow_ts/main.ts b/test/test_project/blank_workflow_ts/main.ts new file mode 100644 index 00000000..aada0405 --- /dev/null +++ b/test/test_project/blank_workflow_ts/main.ts @@ -0,0 +1,28 @@ +import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"; + +type Config = { + schedule: string; +}; + +const onCronTrigger = (runtime: Runtime): string => { + runtime.log("Hello world! Workflow triggered."); + return "Hello world!"; +}; + +const initWorkflow = (config: Config) => { + const cron = new CronCapability(); + + return [ + handler( + cron.trigger( + { schedule: config.schedule } + ), + onCronTrigger + ), + ]; +}; + +export async function main() { + const runner = await Runner.newRunner(); + await runner.run(initWorkflow); +} diff --git a/test/test_project/blank_workflow_ts/package.json b/test/test_project/blank_workflow_ts/package.json new file mode 100644 index 00000000..cddfabf3 --- /dev/null +++ b/test/test_project/blank_workflow_ts/package.json @@ -0,0 +1,16 @@ +{ + "name": "typescript-simple-template", + "version": "1.0.0", + "main": "dist/main.js", + "private": true, + "scripts": { + "postinstall": "bun x cre-setup" + }, + "license": "UNLICENSED", + "dependencies": { + "@chainlink/cre-sdk": "^1.0.9" + }, + "devDependencies": { + "@types/bun": "1.2.21" + } +} diff --git a/test/test_project/blank_workflow_ts/tsconfig.json b/test/test_project/blank_workflow_ts/tsconfig.json new file mode 100644 index 00000000..840fdc79 --- /dev/null +++ b/test/test_project/blank_workflow_ts/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "main.ts" + ] +}