Skip to content

Commit 325d4f4

Browse files
committed
tests: add comprehensive edge case coverage
Add ~39 new tests covering edge cases across workflow features: - branch: integer/boolean/nil conditions, exception handling - foreach: non-list items, raising exceptions, nested items - parallel: conflicting keys, 15+ steps, merge strategies - wait: duplicate events, special chars, unicode event names - compensation: single-step, error returns, parallel steps - scheduler: empty/complex input, cron edge cases New test files: - context_test.exs: key handling, large context, serialization - validation_test.exs: step returns, workflow input validation - resume_edge_cases_test.exs: corrupted state, context restoration
1 parent 86f1cee commit 325d4f4

10 files changed

Lines changed: 2015 additions & 1 deletion

test/durable/branch_test.exs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,74 @@ defmodule Durable.BranchTest do
187187
end
188188
end
189189

190+
describe "branch with string conditions" do
191+
test "branch matches string condition correctly" do
192+
{:ok, execution} =
193+
create_and_execute_workflow(StringConditionBranchWorkflow, %{"category" => "electronics"})
194+
195+
assert execution.status == :completed
196+
assert execution.context["processed_as"] == "electronics"
197+
end
198+
199+
test "branch uses default when string does not match" do
200+
{:ok, execution} =
201+
create_and_execute_workflow(StringConditionBranchWorkflow, %{"category" => "unknown"})
202+
203+
assert execution.status == :completed
204+
assert execution.context["processed_as"] == "other"
205+
end
206+
end
207+
208+
describe "branch execution - edge cases" do
209+
test "branch with integer condition matches correctly" do
210+
{:ok, execution} =
211+
create_and_execute_workflow(IntegerConditionBranchWorkflow, %{"priority" => 1})
212+
213+
assert execution.status == :completed
214+
assert execution.context["processed_as"] == "high"
215+
end
216+
217+
test "branch with integer condition falls to default when no match" do
218+
{:ok, execution} =
219+
create_and_execute_workflow(IntegerConditionBranchWorkflow, %{"priority" => 99})
220+
221+
assert execution.status == :completed
222+
assert execution.context["processed_as"] == "unknown"
223+
end
224+
225+
test "branch with boolean false condition (not just falsy)" do
226+
{:ok, execution} =
227+
create_and_execute_workflow(BooleanConditionBranchWorkflow, %{"enabled" => false})
228+
229+
assert execution.status == :completed
230+
assert execution.context["processed_as"] == "disabled"
231+
end
232+
233+
test "branch with boolean true condition" do
234+
{:ok, execution} =
235+
create_and_execute_workflow(BooleanConditionBranchWorkflow, %{"enabled" => true})
236+
237+
assert execution.status == :completed
238+
assert execution.context["processed_as"] == "enabled"
239+
end
240+
241+
test "branch with nil condition value uses default if present" do
242+
{:ok, execution} =
243+
create_and_execute_workflow(NilConditionBranchWorkflow, %{"value" => nil})
244+
245+
assert execution.status == :completed
246+
assert execution.context["processed_as"] == "nil_handled"
247+
end
248+
249+
test "branch condition function raising exception fails workflow" do
250+
{:ok, execution} =
251+
create_and_execute_workflow(ExceptionConditionBranchWorkflow, %{})
252+
253+
assert execution.status == :failed
254+
assert execution.error != nil
255+
end
256+
end
257+
190258
# Helper functions
191259
defp create_and_execute_workflow(module, input) do
192260
config = Config.get(Durable)
@@ -342,3 +410,156 @@ defmodule ContextBranchWorkflow do
342410
end)
343411
end
344412
end
413+
414+
defmodule StringConditionBranchWorkflow do
415+
use Durable
416+
use Durable.Helpers
417+
418+
workflow "string_condition_branch" do
419+
step(:setup, fn data ->
420+
# Keep the string type, don't convert to atom
421+
{:ok, assign(data, :category, data["category"])}
422+
end)
423+
424+
branch on: fn data -> data.category end do
425+
"electronics" ->
426+
step(:process_electronics, fn data ->
427+
{:ok, assign(data, :processed_as, "electronics")}
428+
end)
429+
430+
"clothing" ->
431+
step(:process_clothing, fn data ->
432+
{:ok, assign(data, :processed_as, "clothing")}
433+
end)
434+
435+
_ ->
436+
step(:process_other, fn data ->
437+
{:ok, assign(data, :processed_as, "other")}
438+
end)
439+
end
440+
441+
step(:done, fn data ->
442+
{:ok, data}
443+
end)
444+
end
445+
end
446+
447+
# Edge case test workflows
448+
449+
defmodule IntegerConditionBranchWorkflow do
450+
use Durable
451+
use Durable.Helpers
452+
453+
workflow "integer_condition_branch" do
454+
step(:setup, fn data ->
455+
{:ok, assign(data, :priority, data["priority"])}
456+
end)
457+
458+
branch on: fn data -> data.priority end do
459+
1 ->
460+
step(:high_priority, fn data ->
461+
{:ok, assign(data, :processed_as, "high")}
462+
end)
463+
464+
2 ->
465+
step(:medium_priority, fn data ->
466+
{:ok, assign(data, :processed_as, "medium")}
467+
end)
468+
469+
3 ->
470+
step(:low_priority, fn data ->
471+
{:ok, assign(data, :processed_as, "low")}
472+
end)
473+
474+
_ ->
475+
step(:unknown_priority, fn data ->
476+
{:ok, assign(data, :processed_as, "unknown")}
477+
end)
478+
end
479+
480+
step(:done, fn data ->
481+
{:ok, data}
482+
end)
483+
end
484+
end
485+
486+
defmodule BooleanConditionBranchWorkflow do
487+
use Durable
488+
use Durable.Helpers
489+
490+
workflow "boolean_condition_branch" do
491+
step(:setup, fn data ->
492+
{:ok, assign(data, :enabled, data["enabled"])}
493+
end)
494+
495+
branch on: fn data -> data.enabled end do
496+
true ->
497+
step(:process_enabled, fn data ->
498+
{:ok, assign(data, :processed_as, "enabled")}
499+
end)
500+
501+
false ->
502+
step(:process_disabled, fn data ->
503+
{:ok, assign(data, :processed_as, "disabled")}
504+
end)
505+
end
506+
507+
step(:done, fn data ->
508+
{:ok, data}
509+
end)
510+
end
511+
end
512+
513+
defmodule NilConditionBranchWorkflow do
514+
use Durable
515+
use Durable.Helpers
516+
517+
workflow "nil_condition_branch" do
518+
step(:setup, fn data ->
519+
{:ok, assign(data, :value, data["value"])}
520+
end)
521+
522+
branch on: fn data -> data.value end do
523+
nil ->
524+
step(:handle_nil, fn data ->
525+
{:ok, assign(data, :processed_as, "nil_handled")}
526+
end)
527+
528+
_ ->
529+
step(:handle_other, fn data ->
530+
{:ok, assign(data, :processed_as, "other")}
531+
end)
532+
end
533+
534+
step(:done, fn data ->
535+
{:ok, data}
536+
end)
537+
end
538+
end
539+
540+
defmodule ExceptionConditionBranchWorkflow do
541+
use Durable
542+
use Durable.Helpers
543+
544+
workflow "exception_condition_branch" do
545+
step(:setup, fn data ->
546+
{:ok, data}
547+
end)
548+
549+
branch on: fn _data -> raise "Condition evaluation failed" end do
550+
:a ->
551+
step(:process_a, fn data ->
552+
{:ok, assign(data, :processed_as, "a")}
553+
end)
554+
555+
_ ->
556+
step(:process_default, fn data ->
557+
{:ok, assign(data, :processed_as, "default")}
558+
end)
559+
end
560+
561+
step(:done, fn data ->
562+
{:ok, data}
563+
end)
564+
end
565+
end

test/durable/compensation_test.exs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,39 @@ defmodule Durable.CompensationTest do
246246
end
247247
end
248248

249+
describe "compensation execution - edge cases" do
250+
test "single-step workflow with compensation where step raises" do
251+
{:ok, execution} = create_and_execute_workflow(SingleStepRaisingCompensationWorkflow, %{})
252+
253+
# When a step raises and has a compensation, it should be compensated
254+
# The step needs to actually complete first before the raise for compensation to apply
255+
assert execution.status in [:failed, :compensated]
256+
end
257+
258+
test "compensation with step that returns {:error, map}" do
259+
{:ok, execution} = create_and_execute_workflow(ErrorMapCompensationWorkflow, %{})
260+
261+
# Workflow enters compensation when a step fails
262+
# Compensation returning {:error, map} should mark as compensation_failed
263+
assert execution.status in [:compensated, :compensation_failed]
264+
end
265+
266+
test "parallel steps trigger compensations when step after parallel fails" do
267+
{:ok, execution} = create_and_execute_workflow(ParallelWithCompensationWorkflow, %{})
268+
269+
# Workflow should be in compensated state
270+
assert execution.status == :compensated
271+
272+
# Should have compensation results for each completed parallel step
273+
results = execution.compensation_results
274+
# At least 2 compensations should run (from the parallel steps that completed)
275+
assert length(results) >= 2
276+
277+
# All compensations should be completed
278+
assert Enum.all?(results, fn r -> r["result"]["status"] == "completed" end)
279+
end
280+
end
281+
249282
# Helper function to create and execute workflow
250283
defp create_and_execute_workflow(module, input) do
251284
config = Config.get(Durable)
@@ -271,3 +304,72 @@ defmodule Durable.CompensationTest do
271304
{:ok, repo.get!(WorkflowExecution, execution.id)}
272305
end
273306
end
307+
308+
# Edge case test workflows
309+
310+
defmodule SingleStepRaisingCompensationWorkflow do
311+
use Durable
312+
use Durable.Helpers
313+
314+
workflow "single_step_raising_compensation" do
315+
step(:setup_step, [compensate: :undo_setup], fn data ->
316+
{:ok, assign(data, :setup_done, true)}
317+
end)
318+
319+
step(:failing_step, fn _data ->
320+
raise "Intentional failure"
321+
end)
322+
323+
compensate(:undo_setup, fn data ->
324+
{:ok, assign(data, :undone, true)}
325+
end)
326+
end
327+
end
328+
329+
defmodule ErrorMapCompensationWorkflow do
330+
use Durable
331+
use Durable.Helpers
332+
333+
workflow "error_map_compensation" do
334+
step(:do_work, [compensate: :undo_work], fn data ->
335+
{:ok, assign(data, :work_done, true)}
336+
end)
337+
338+
step(:fail_step, fn _data ->
339+
raise "Step failed"
340+
end)
341+
342+
compensate(:undo_work, fn _data ->
343+
{:error, %{reason: "Compensation returned error"}}
344+
end)
345+
end
346+
end
347+
348+
defmodule ParallelWithCompensationWorkflow do
349+
use Durable
350+
use Durable.Helpers
351+
352+
workflow "parallel_with_compensation" do
353+
parallel do
354+
step(:task_a, [compensate: :undo_task_a], fn data ->
355+
{:ok, assign(data, :a_done, true)}
356+
end)
357+
358+
step(:task_b, [compensate: :undo_task_b], fn data ->
359+
{:ok, assign(data, :b_done, true)}
360+
end)
361+
end
362+
363+
step(:after_parallel, fn _data ->
364+
raise "Failure after parallel"
365+
end)
366+
367+
compensate(:undo_task_a, fn data ->
368+
{:ok, assign(data, :a_undone, true)}
369+
end)
370+
371+
compensate(:undo_task_b, fn data ->
372+
{:ok, assign(data, :b_undone, true)}
373+
end)
374+
end
375+
end

0 commit comments

Comments
 (0)