Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 39 additions & 0 deletions bindings/zeebe/command/activate_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,45 @@ type activateJobsPayload struct {
RequestTimeout *metadata.Duration `json:"requestTimeout,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler to provide field-specific error messages for
// duration fields. Both timeout and requestTimeout must be Go duration strings (e.g. "30s", "5m", "1h30m").
func (p *activateJobsPayload) UnmarshalJSON(data []byte) error {
var shadow struct {
Comment thread
cicoyle marked this conversation as resolved.
Outdated
JobType string `json:"jobType"`
MaxJobsToActivate *int32 `json:"maxJobsToActivate"`
Timeout *json.RawMessage `json:"timeout,omitempty"`
WorkerName string `json:"workerName"`
FetchVariables []string `json:"fetchVariables"`
RequestTimeout *json.RawMessage `json:"requestTimeout,omitempty"`
}
if err := json.Unmarshal(data, &shadow); err != nil {
return err
}

p.JobType = shadow.JobType
p.MaxJobsToActivate = shadow.MaxJobsToActivate
p.WorkerName = shadow.WorkerName
p.FetchVariables = shadow.FetchVariables

if shadow.Timeout != nil {
var d metadata.Duration
if err := d.UnmarshalJSON(*shadow.Timeout); err != nil {
return fmt.Errorf("invalid value for field 'timeout' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\"): %w", err)
}
Comment thread
nelson-parente marked this conversation as resolved.
Outdated
p.Timeout = &d
}

if shadow.RequestTimeout != nil {
var d metadata.Duration
if err := d.UnmarshalJSON(*shadow.RequestTimeout); err != nil {
return fmt.Errorf("invalid value for field 'requestTimeout' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\"): %w", err)
}
Comment thread
nelson-parente marked this conversation as resolved.
Outdated
p.RequestTimeout = &d
}

return nil
}

func (z *ZeebeCommand) activateJobs(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
var payload activateJobsPayload
err := json.Unmarshal(req.Data, &payload)
Expand Down
69 changes: 69 additions & 0 deletions bindings/zeebe/command/activate_jobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,75 @@ func (cmd3 *mockActivateJobsCommandStep3) Send(context.Context) ([]entities.Job,
return []entities.Job{}, nil
}

func TestActivateJobsPayloadUnmarshal(t *testing.T) {
t.Run("invalid timeout produces field-scoped error", func(t *testing.T) {
raw := `{"jobType":"worker","maxJobsToActivate":10,"timeout":"PT30S"}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.Error(t, err)
assert.Contains(t, err.Error(), "timeout")
assert.Contains(t, err.Error(), "PT30S")
assert.Contains(t, err.Error(), "duration string")
})

t.Run("invalid requestTimeout produces field-scoped error", func(t *testing.T) {
raw := `{"jobType":"worker","maxJobsToActivate":10,"requestTimeout":"2minutes"}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.Error(t, err)
assert.Contains(t, err.Error(), "requestTimeout")
assert.Contains(t, err.Error(), "2minutes")
assert.Contains(t, err.Error(), "duration string")
})

t.Run("valid Go duration strings succeed", func(t *testing.T) {
raw := `{"jobType":"worker","maxJobsToActivate":10,"timeout":"30s","requestTimeout":"5m"}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.Timeout)
assert.Equal(t, 30*time.Second, p.Timeout.Duration)
require.NotNil(t, p.RequestTimeout)
assert.Equal(t, 5*time.Minute, p.RequestTimeout.Duration)
})

t.Run("numeric nanosecond timeout still parses correctly", func(t *testing.T) {
// Regression guard: numeric (nanosecond) values were accepted before and must remain valid.
raw := `{"jobType":"worker","maxJobsToActivate":10,"timeout":30000000000}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.Timeout)
assert.Equal(t, 30*time.Second, p.Timeout.Duration)
})

t.Run("numeric nanosecond requestTimeout still parses correctly", func(t *testing.T) {
// Regression guard: numeric (nanosecond) values were accepted before and must remain valid.
raw := `{"jobType":"worker","maxJobsToActivate":10,"requestTimeout":300000000000}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.RequestTimeout)
assert.Equal(t, 5*time.Minute, p.RequestTimeout.Duration)
})

t.Run("null timeout leaves pointer nil", func(t *testing.T) {
raw := `{"jobType":"worker","maxJobsToActivate":10,"timeout":null}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
assert.Nil(t, p.Timeout)
})

t.Run("null requestTimeout leaves pointer nil", func(t *testing.T) {
raw := `{"jobType":"worker","maxJobsToActivate":10,"requestTimeout":null}`
var p activateJobsPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
assert.Nil(t, p.RequestTimeout)
})
}

func TestActivateJobs(t *testing.T) {
testLogger := logger.NewLogger("test")

Expand Down
35 changes: 35 additions & 0 deletions bindings/zeebe/command/create_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,41 @@ type createInstancePayload struct {
RequestTimeout *metadata.Duration `json:"requestTimeout,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler to provide a field-specific error message for
// the requestTimeout duration field. The requestTimeout field must be a Go duration string
// (e.g. "30s", "5m", "1h30m").
func (p *createInstancePayload) UnmarshalJSON(data []byte) error {
var shadow struct {
BpmnProcessID string `json:"bpmnProcessId"`
ProcessDefinitionKey *int64 `json:"processDefinitionKey"`
Version *int32 `json:"version"`
Variables interface{} `json:"variables"`
WithResult bool `json:"withResult"`
FetchVariables []string `json:"fetchVariables"`
RequestTimeout *json.RawMessage `json:"requestTimeout,omitempty"`
}
if err := json.Unmarshal(data, &shadow); err != nil {
return err
}

p.BpmnProcessID = shadow.BpmnProcessID
p.ProcessDefinitionKey = shadow.ProcessDefinitionKey
p.Version = shadow.Version
p.Variables = shadow.Variables
p.WithResult = shadow.WithResult
p.FetchVariables = shadow.FetchVariables

if shadow.RequestTimeout != nil {
var d metadata.Duration
if err := d.UnmarshalJSON(*shadow.RequestTimeout); err != nil {
return fmt.Errorf("invalid value for field 'requestTimeout' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\"): %w", err)
}
Comment thread
nelson-parente marked this conversation as resolved.
Outdated
p.RequestTimeout = &d
}

return nil
}

func (z *ZeebeCommand) createInstance(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
var payload createInstancePayload
err := json.Unmarshal(req.Data, &payload)
Expand Down
40 changes: 40 additions & 0 deletions bindings/zeebe/command/create_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"testing"
"time"

"github.com/camunda/zeebe/clients/go/v8/pkg/commands"
"github.com/camunda/zeebe/clients/go/v8/pkg/pb"
Expand Down Expand Up @@ -98,6 +99,45 @@ func (cmd3 *mockCreateInstanceCommandStep3) Send(context.Context) (*pb.CreatePro
return &pb.CreateProcessInstanceResponse{}, nil
}

func TestCreateInstancePayloadUnmarshal(t *testing.T) {
t.Run("invalid requestTimeout produces field-scoped error", func(t *testing.T) {
raw := `{"bpmnProcessId":"order-process","requestTimeout":"PT5M"}`
var p createInstancePayload
err := p.UnmarshalJSON([]byte(raw))
require.Error(t, err)
assert.Contains(t, err.Error(), "requestTimeout")
assert.Contains(t, err.Error(), "PT5M")
assert.Contains(t, err.Error(), "duration string")
})

t.Run("valid Go duration string succeeds", func(t *testing.T) {
raw := `{"bpmnProcessId":"order-process","requestTimeout":"5m"}`
var p createInstancePayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.RequestTimeout)
assert.Equal(t, 5*time.Minute, p.RequestTimeout.Duration)
})

t.Run("numeric nanosecond requestTimeout still parses correctly", func(t *testing.T) {
// Regression guard: numeric (nanosecond) values were accepted before and must remain valid.
raw := `{"bpmnProcessId":"order-process","requestTimeout":300000000000}`
var p createInstancePayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.RequestTimeout)
assert.Equal(t, 5*time.Minute, p.RequestTimeout.Duration)
})

t.Run("null requestTimeout leaves pointer nil", func(t *testing.T) {
raw := `{"bpmnProcessId":"order-process","requestTimeout":null}`
var p createInstancePayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
assert.Nil(t, p.RequestTimeout)
})
}

func TestCreateInstance(t *testing.T) {
testLogger := logger.NewLogger("test")

Expand Down
31 changes: 31 additions & 0 deletions bindings/zeebe/command/fail_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ type failJobPayload struct {
Variables interface{} `json:"variables"`
}

// UnmarshalJSON implements json.Unmarshaler to provide a field-specific error message for
// the retryBackOff duration field. The retryBackOff field must be a Go duration string
// (e.g. "30s", "5m", "1h30m").
func (p *failJobPayload) UnmarshalJSON(data []byte) error {
var shadow struct {
JobKey *int64 `json:"jobKey"`
Retries *int32 `json:"retries"`
ErrorMessage string `json:"errorMessage"`
RetryBackOff *json.RawMessage `json:"retryBackOff,omitempty"`
Variables interface{} `json:"variables"`
}
if err := json.Unmarshal(data, &shadow); err != nil {
return err
}

p.JobKey = shadow.JobKey
p.Retries = shadow.Retries
p.ErrorMessage = shadow.ErrorMessage
p.Variables = shadow.Variables

if shadow.RetryBackOff != nil {
var d metadata.Duration
if err := d.UnmarshalJSON(*shadow.RetryBackOff); err != nil {
return fmt.Errorf("invalid value for field 'retryBackOff' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\"): %w", err)
}
Comment thread
nelson-parente marked this conversation as resolved.
Outdated
p.RetryBackOff = &d
}

return nil
}

func (z *ZeebeCommand) failJob(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
var payload failJobPayload
err := json.Unmarshal(req.Data, &payload)
Expand Down
40 changes: 40 additions & 0 deletions bindings/zeebe/command/fail_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"testing"
"time"

"github.com/camunda/zeebe/clients/go/v8/pkg/commands"
"github.com/camunda/zeebe/clients/go/v8/pkg/pb"
Expand Down Expand Up @@ -82,6 +83,45 @@ func (cmd3 *mockFailJobCommandStep3) Send(context.Context) (*pb.FailJobResponse,
return &pb.FailJobResponse{}, nil
}

func TestFailJobPayloadUnmarshal(t *testing.T) {
t.Run("invalid retryBackOff produces field-scoped error", func(t *testing.T) {
raw := `{"jobKey":1,"retries":3,"retryBackOff":"PT10S"}`
var p failJobPayload
err := p.UnmarshalJSON([]byte(raw))
require.Error(t, err)
assert.Contains(t, err.Error(), "retryBackOff")
assert.Contains(t, err.Error(), "PT10S")
assert.Contains(t, err.Error(), "duration string")
})

t.Run("valid Go duration string succeeds", func(t *testing.T) {
raw := `{"jobKey":1,"retries":3,"retryBackOff":"10s"}`
var p failJobPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.RetryBackOff)
assert.Equal(t, 10*time.Second, p.RetryBackOff.Duration)
})

t.Run("numeric nanosecond retryBackOff still parses correctly", func(t *testing.T) {
// Regression guard: numeric (nanosecond) values were accepted before and must remain valid.
raw := `{"jobKey":1,"retries":3,"retryBackOff":30000000000}`
var p failJobPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
require.NotNil(t, p.RetryBackOff)
assert.Equal(t, 30*time.Second, p.RetryBackOff.Duration)
})

t.Run("null retryBackOff leaves pointer nil", func(t *testing.T) {
raw := `{"jobKey":1,"retries":3,"retryBackOff":null}`
var p failJobPayload
err := p.UnmarshalJSON([]byte(raw))
require.NoError(t, err)
assert.Nil(t, p.RetryBackOff)
})
}

func TestFailJob(t *testing.T) {
testLogger := logger.NewLogger("test")

Expand Down
31 changes: 31 additions & 0 deletions bindings/zeebe/command/publish_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,37 @@ type publishMessagePayload struct {
Variables interface{} `json:"variables"`
}

// UnmarshalJSON implements json.Unmarshaler to provide field-specific error messages for
// duration fields. The timeToLive field must be a Go duration string (e.g. "30s", "5m", "1h30m").
func (p *publishMessagePayload) UnmarshalJSON(data []byte) error {
// Use a shadow struct with raw JSON for duration fields so we can provide better errors.
var shadow struct {
MessageName string `json:"messageName"`
CorrelationKey string `json:"correlationKey"`
MessageID string `json:"messageId"`
TimeToLive *json.RawMessage `json:"timeToLive,omitempty"`
Variables interface{} `json:"variables"`
}
if err := json.Unmarshal(data, &shadow); err != nil {
return err
}

p.MessageName = shadow.MessageName
p.CorrelationKey = shadow.CorrelationKey
p.MessageID = shadow.MessageID
p.Variables = shadow.Variables

if shadow.TimeToLive != nil {
var d metadata.Duration
if err := d.UnmarshalJSON(*shadow.TimeToLive); err != nil {
return fmt.Errorf("invalid value for field 'timeToLive' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\"): %w", err)
}
Comment thread
nelson-parente marked this conversation as resolved.
Outdated
p.TimeToLive = &d
}

return nil
}

func (z *ZeebeCommand) publishMessage(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
var payload publishMessagePayload
err := json.Unmarshal(req.Data, &payload)
Expand Down
Loading
Loading