Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
40 changes: 40 additions & 0 deletions bindings/zeebe/command/activate_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ 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 accept either a Go duration string
// (e.g. "30s", "5m", "1h30m") or a plain integer representing nanoseconds.
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 %s for field 'timeout' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\", or a plain integer nanoseconds value): %w", string(*shadow.Timeout), err)
Comment thread
cicoyle 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 %s for field 'requestTimeout' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\", or a plain integer nanoseconds value): %w", string(*shadow.RequestTimeout), err)
}
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 accepts either a Go duration string
// (e.g. "30s", "5m", "1h30m") or a plain integer representing nanoseconds.
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 %s for field 'requestTimeout' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\", or a plain integer nanoseconds value): %w", string(*shadow.RequestTimeout), err)
}
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 accepts either a Go duration string
// (e.g. "30s", "5m", "1h30m") or a plain integer representing nanoseconds.
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 %s for field 'retryBackOff' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\", or a plain integer nanoseconds value): %w", string(*shadow.RetryBackOff), err)
}
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
32 changes: 32 additions & 0 deletions bindings/zeebe/command/publish_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,38 @@ type publishMessagePayload struct {
Variables interface{} `json:"variables"`
}

// UnmarshalJSON implements json.Unmarshaler to provide field-specific error messages for
// duration fields. The timeToLive field accepts either a Go duration string (e.g. "30s", "5m", "1h30m")
// or a plain integer representing nanoseconds.
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 %s for field 'timeToLive' (expected a Go duration string, e.g. \"30s\", \"5m\", \"1h30m\", or a plain integer nanoseconds value): %w", string(*shadow.TimeToLive), err)
}
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