Skip to content
Merged
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
8 changes: 8 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,14 @@ func (e *StdEngine) configurePipelines(pipelineCfg map[string]any) error {
Compensation: compSteps,
}

// Set RoutePattern from inline HTTP trigger path so that step.request_parse
// can extract path parameters via _route_pattern in the pipeline context.
if pipeCfg.Trigger.Type == "http" {
if path, _ := pipeCfg.Trigger.Config["path"].(string); path != "" {
pipeline.RoutePattern = path
}
}

adder.AddPipeline(pipelineName, pipeline)
// Register in the engine's pipeline registry so step.workflow_call can
// look up this pipeline at execution time.
Expand Down
90 changes: 90 additions & 0 deletions engine_pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,96 @@ func TestPipeline_ConfigurePipelines_InlineHTTPTrigger(t *testing.T) {
}
}

func TestPipeline_ConfigurePipelines_InlineHTTPTrigger_SetsRoutePattern(t *testing.T) {
// Verify that RoutePattern is populated from the inline HTTP trigger path
// so that step.request_parse can extract path parameters.
const wantPattern = "/api/resources/{id}"

app := newMockApplication()
engine := NewStdEngine(app, app.Logger())
engine.AddStepType("step.set", module.NewSetStepFactory())
pipelineHandler := handlers.NewPipelineWorkflowHandler()
engine.RegisterWorkflowHandler(pipelineHandler)

mt := &mockTrigger{name: module.HTTPTriggerName, configType: "http"}
engine.RegisterTrigger(mt)

pipelineCfg := map[string]any{
"resource-pipeline": map[string]any{
"trigger": map[string]any{
"type": "http",
"config": map[string]any{
"method": "GET",
"path": wantPattern,
},
},
"steps": []any{
map[string]any{
"name": "set-ok",
"type": "step.set",
"config": map[string]any{
"values": map[string]any{"status": "ok"},
},
},
},
},
}

if err := engine.configurePipelines(pipelineCfg); err != nil {
t.Fatalf("configurePipelines failed: %v", err)
}

pipeline, ok := engine.pipelineRegistry["resource-pipeline"]
if !ok {
t.Fatal("expected pipeline to be registered in pipelineRegistry")
}
if pipeline.RoutePattern != wantPattern {
t.Errorf("expected RoutePattern %q, got %q", wantPattern, pipeline.RoutePattern)
}
}

func TestPipeline_ConfigurePipelines_InlineHTTPTrigger_NoPathNoRoutePattern(t *testing.T) {
// When no path is provided in the trigger config, RoutePattern should remain empty.
app := newMockApplication()
engine := NewStdEngine(app, app.Logger())
engine.AddStepType("step.set", module.NewSetStepFactory())
pipelineHandler := handlers.NewPipelineWorkflowHandler()
engine.RegisterWorkflowHandler(pipelineHandler)

mt := &mockTrigger{name: module.HTTPTriggerName, configType: "http"}
engine.RegisterTrigger(mt)

pipelineCfg := map[string]any{
"no-path-pipeline": map[string]any{
"trigger": map[string]any{
"type": "http",
"config": map[string]any{},
},
"steps": []any{
map[string]any{
"name": "set-ok",
"type": "step.set",
"config": map[string]any{
"values": map[string]any{"status": "ok"},
},
},
},
},
}

if err := engine.configurePipelines(pipelineCfg); err != nil {
t.Fatalf("configurePipelines failed: %v", err)
}

pipeline, ok := engine.pipelineRegistry["no-path-pipeline"]
if !ok {
t.Fatal("expected pipeline to be registered in pipelineRegistry")
}
if pipeline.RoutePattern != "" {
t.Errorf("expected empty RoutePattern, got %q", pipeline.RoutePattern)
}
}

func TestPipeline_ConfigurePipelines_InlineEventTrigger(t *testing.T) {
// Use a minimal engine without plugins to avoid trigger collisions.
app := newMockApplication()
Expand Down
8 changes: 8 additions & 0 deletions module/pipeline_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ func (p *Pipeline) Execute(ctx context.Context, triggerData map[string]any) (*Pi
if req := ctx.Value(HTTPRequestContextKey); req != nil {
md["_http_request"] = req
}
// Seed _route_pattern from RoutePattern if not already provided via Metadata
// (e.g. by CQRS handlers). This ensures step.request_parse can extract path
// parameters when a pipeline is executed via an inline HTTP trigger.
if p.RoutePattern != "" {
if _, exists := md["_route_pattern"]; !exists {
md["_route_pattern"] = p.RoutePattern
}
}
pc := NewPipelineContext(triggerData, md)

logger := p.Logger
Expand Down
84 changes: 84 additions & 0 deletions module/pipeline_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,87 @@ func TestPipeline_CompensateWithNoCompensationSteps(t *testing.T) {
t.Errorf("expected 'compensation executed' in error, got: %v", err)
}
}

func TestPipeline_Execute_SeedsRoutePatternFromField(t *testing.T) {
// RoutePattern should be seeded into _route_pattern in pipeline metadata
// when Execute is called, enabling step.request_parse path param extraction
// for pipelines executed via inline HTTP triggers.
var capturedRoutePattern any
step1 := &mockStep{
name: "capture",
execFn: func(_ context.Context, pc *PipelineContext) (*StepResult, error) {
capturedRoutePattern = pc.Metadata["_route_pattern"]
return &StepResult{}, nil
},
}

p := &Pipeline{
Name: "route-pipeline",
Steps: []PipelineStep{step1},
RoutePattern: "/api/items/{id}",
}

_, err := p.Execute(context.Background(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedRoutePattern != "/api/items/{id}" {
t.Errorf("expected _route_pattern %q in metadata, got %v", "/api/items/{id}", capturedRoutePattern)
}
}

func TestPipeline_Execute_MetadataRoutePatternTakesPrecedence(t *testing.T) {
// When _route_pattern is already set in p.Metadata (e.g. by a CQRS handler),
// it should not be overwritten by p.RoutePattern.
var capturedRoutePattern any
step1 := &mockStep{
name: "capture",
execFn: func(_ context.Context, pc *PipelineContext) (*StepResult, error) {
capturedRoutePattern = pc.Metadata["_route_pattern"]
return &StepResult{}, nil
},
}

p := &Pipeline{
Name: "route-pipeline",
Steps: []PipelineStep{step1},
RoutePattern: "/api/items/{id}",
Metadata: map[string]any{
"_route_pattern": "/overridden/pattern/{id}",
},
}

_, err := p.Execute(context.Background(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedRoutePattern != "/overridden/pattern/{id}" {
t.Errorf("expected _route_pattern %q from Metadata, got %v", "/overridden/pattern/{id}", capturedRoutePattern)
}
}

func TestPipeline_Execute_NoRoutePatternNoMetadata(t *testing.T) {
// When RoutePattern is empty and Metadata has no _route_pattern,
// _route_pattern should not appear in the pipeline context.
var routePatternPresent bool
step1 := &mockStep{
name: "capture",
execFn: func(_ context.Context, pc *PipelineContext) (*StepResult, error) {
_, routePatternPresent = pc.Metadata["_route_pattern"]
return &StepResult{}, nil
},
}

p := &Pipeline{
Name: "plain-pipeline",
Steps: []PipelineStep{step1},
}

_, err := p.Execute(context.Background(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if routePatternPresent {
t.Error("expected _route_pattern key to be absent from metadata")
}
}
64 changes: 32 additions & 32 deletions module/pipeline_step_base64_decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,32 @@ const (

// mimeToExtension maps common MIME types to their canonical file extensions.
var mimeToExtension = map[string]string{
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/bmp": ".bmp",
"image/tiff": ".tiff",
"image/svg+xml": ".svg",
"image/x-icon": ".ico",
"application/pdf": ".pdf",
"application/zip": ".zip",
"text/plain": ".txt",
"text/html": ".html",
"text/css": ".css",
"text/javascript": ".js",
"application/json": ".json",
"application/xml": ".xml",
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
"audio/wav": ".wav",
"video/mp4": ".mp4",
"video/webm": ".webm",
"video/ogg": ".ogv",
"application/octet-stream": ".bin",
"application/gzip": ".gz",
"application/x-tar": ".tar",
"application/vnd.ms-excel": ".xls",
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/bmp": ".bmp",
"image/tiff": ".tiff",
"image/svg+xml": ".svg",
"image/x-icon": ".ico",
"application/pdf": ".pdf",
"application/zip": ".zip",
"text/plain": ".txt",
"text/html": ".html",
"text/css": ".css",
"text/javascript": ".js",
"application/json": ".json",
"application/xml": ".xml",
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
"audio/wav": ".wav",
"video/mp4": ".mp4",
"video/webm": ".webm",
"video/ogg": ".ogv",
"application/octet-stream": ".bin",
"application/gzip": ".gz",
"application/x-tar": ".tar",
"application/vnd.ms-excel": ".xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"application/msword": ".doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
Expand All @@ -52,12 +52,12 @@ var mimeToExtension = map[string]string{
// Base64DecodeStep decodes base64-encoded content (raw or data-URI), optionally
// validating the MIME type and decoded size.
type Base64DecodeStep struct {
name string
inputFrom string
format string
allowedTypes []string
maxSizeBytes int
validateMagic bool
name string
inputFrom string
format string
allowedTypes []string
maxSizeBytes int
validateMagic bool
}

// NewBase64DecodeStepFactory returns a StepFactory that creates Base64DecodeStep instances.
Expand Down
2 changes: 1 addition & 1 deletion module/pipeline_step_webhook_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package module
import (
"context"
"crypto/hmac"
"crypto/sha1" //nolint:gosec // Required for Twilio HMAC-SHA1 webhook signature verification
"crypto/sha1" //nolint:gosec // Required for Twilio HMAC-SHA1 webhook signature verification
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
Expand Down
Loading