From 73c3657731e2eaeab38becfcb0875434f9b7f4f7 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 14 Apr 2026 18:51:13 -0400 Subject: [PATCH] fix(dispatch): reject invalid command UUID overrides Signed-off-by: Yordis Prieto --- lib/application.ex | 3 +++ lib/commanded/commands/router.ex | 18 ++++++++++++++++-- test/commands/command_identity_test.exs | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/application.ex b/lib/application.ex index 76a64110..3fce8ab1 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -310,6 +310,9 @@ defmodule Commanded.Application do - `causation_id` - an optional UUID used to identify the cause of the command being dispatched. + - `command_uuid` - an optional UUID used to identify the command being + dispatched. When omitted, one will be generated automatically. + - `correlation_id` - an optional UUID used to correlate related commands/events together. diff --git a/lib/commanded/commands/router.ex b/lib/commanded/commands/router.ex index 961c474f..edbc1658 100644 --- a/lib/commanded/commands/router.ex +++ b/lib/commanded/commands/router.ex @@ -445,7 +445,7 @@ defmodule Commanded.Commands.Router do command being dispatched. - `command_uuid` - an optional UUID used to identify the command being - dispatched. + dispatched. When omitted, one will be generated automatically. - `correlation_id` - an optional UUID used to correlate related commands/events together. @@ -546,7 +546,7 @@ defmodule Commanded.Commands.Router do application = Keyword.fetch!(opts, :application) causation_id = Keyword.get(opts, :causation_id) - command_uuid = Keyword.get_lazy(opts, :command_uuid, &UUID.uuid7/0) + command_uuid = Router.fetch_command_uuid(opts) consistency = Keyword.fetch!(opts, :consistency) correlation_id = Keyword.get_lazy(opts, :correlation_id, &UUID.uuid7/0) metadata = Keyword.fetch!(opts, :metadata) |> validate_metadata() @@ -648,6 +648,20 @@ defmodule Commanded.Commands.Router do end end + @doc false + def fetch_command_uuid(opts) do + case Keyword.fetch(opts, :command_uuid) do + {:ok, value} when is_binary(value) -> + value + + :error -> + UUID.uuid7() + + _ -> + raise ArgumentError, message: "command_uuid must be a binary" + end + end + @doc false def get_opt(opts, name, default \\ nil) do Keyword.get(opts, name) || Application.get_env(:commanded, name) || default diff --git a/test/commands/command_identity_test.exs b/test/commands/command_identity_test.exs index 495721ea..1b508f86 100644 --- a/test/commands/command_identity_test.exs +++ b/test/commands/command_identity_test.exs @@ -27,5 +27,21 @@ defmodule Commanded.Commands.CommandIdentityTest do :ok = BankApp.dispatch(command, command_uuid: command_uuid) assert [^command_uuid] = CommandAuditMiddleware.dispatched_commands(& &1.command_uuid) end + + test "should reject nil command_uuid" do + command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000} + + assert_raise ArgumentError, "command_uuid must be a binary", fn -> + BankApp.dispatch(command, command_uuid: nil) + end + end + + test "should reject non-binary command_uuid" do + command = %OpenAccount{account_number: "ACC123", initial_balance: 1_000} + + assert_raise ArgumentError, "command_uuid must be a binary", fn -> + BankApp.dispatch(command, command_uuid: 123) + end + end end end