Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package test

import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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()).
Expand Down
166 changes: 166 additions & 0 deletions test/multi_command_flows/workflow_happy_path_4.go
Original file line number Diff line number Diff line change
@@ -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)
}
47 changes: 47 additions & 0 deletions test/multi_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
3 changes: 3 additions & 0 deletions test/test_project/blank_workflow_ts/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"schedule": "*/30 * * * * *"
}
28 changes: 28 additions & 0 deletions test/test_project/blank_workflow_ts/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk";

type Config = {
schedule: string;
};

const onCronTrigger = (runtime: Runtime<Config>): 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<Config>();
await runner.run(initWorkflow);
}
16 changes: 16 additions & 0 deletions test/test_project/blank_workflow_ts/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
16 changes: 16 additions & 0 deletions test/test_project/blank_workflow_ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Loading