use angzarr_client::proto::{CommandBook, CommandResponse, MergeStrategy};
use angzarr_client::proto_ext::CommandPageExt;
use angzarr_client::traits::GatewayClient;
use angzarr_client::{ClientError, CommandBuilderExt, Result};
use async_trait::async_trait;
use cucumber::{given, then, when, World};
use prost::Message;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
#[derive(Clone, Message)]
pub struct TestCommand {
#[prost(string, tag = "1")]
pub data: String,
}
#[derive(Clone, Default, Debug)]
pub struct MockGateway {
pub last_command: Arc<Mutex<Option<CommandBook>>>,
}
#[async_trait]
impl GatewayClient for MockGateway {
async fn execute(&self, command: CommandBook) -> Result<CommandResponse> {
*self.last_command.lock().unwrap() = Some(command);
Ok(CommandResponse::default())
}
}
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct CommandBuilderWorld {
mock_client: MockGateway,
built_command: Option<CommandBook>,
build_error: Option<ClientError>,
domain: String,
root: Option<Uuid>,
correlation_id: Option<String>,
sequence: Option<u32>,
command_type: Option<String>,
type_url_set: bool,
payload_set: bool,
execute_response: Option<CommandResponse>,
}
impl CommandBuilderWorld {
fn new() -> Self {
Self {
mock_client: MockGateway::default(),
built_command: None,
build_error: None,
domain: String::new(),
root: None,
correlation_id: None,
sequence: None,
command_type: None,
type_url_set: false,
payload_set: false,
execute_response: None,
}
}
fn try_build(&mut self) {
let cmd = TestCommand {
data: "test".to_string(),
};
let builder = if let Some(root) = self.root {
self.mock_client.command(&self.domain, root)
} else {
self.mock_client.command(&self.domain, Uuid::new_v4())
};
let builder = if let Some(ref cid) = self.correlation_id {
builder.with_correlation_id(cid)
} else {
builder
};
let builder = if let Some(seq) = self.sequence {
builder.with_sequence(seq)
} else {
builder
};
if self.type_url_set && self.payload_set {
let type_url = if let Some(ref cmd_type) = self.command_type {
format!("type.googleapis.com/{}.{}", self.domain, cmd_type)
} else {
"type.googleapis.com/test.TestCommand".to_string()
};
let builder = builder.with_command(&type_url, &cmd);
match builder.build() {
Ok(cmd) => self.built_command = Some(cmd),
Err(e) => self.build_error = Some(e),
}
} else if self.type_url_set && !self.payload_set {
self.build_error = Some(ClientError::InvalidArgument {
msg: "command payload not set".to_string(),
});
} else if !self.type_url_set && self.payload_set {
self.build_error = Some(ClientError::InvalidArgument {
msg: "command type_url not set".to_string(),
});
} else {
match builder.build() {
Ok(cmd) => self.built_command = Some(cmd),
Err(e) => self.build_error = Some(e),
}
}
}
}
#[given("a mock GatewayClient for testing")]
async fn given_mock_gateway(world: &mut CommandBuilderWorld) {
world.mock_client = MockGateway::default();
}
#[when(expr = "I build a command for domain {string} root {string}")]
async fn when_build_command_domain_root(
world: &mut CommandBuilderWorld,
domain: String,
root: String,
) {
world.domain = domain;
world.root = Some(Uuid::parse_str(&root).unwrap_or_else(|_| Uuid::new_v4()));
}
#[when(expr = "I build a command for domain {string}")]
async fn when_build_command_domain(world: &mut CommandBuilderWorld, domain: String) {
world.domain = domain;
}
#[when(expr = "I build a command for new aggregate in domain {string}")]
async fn when_build_command_new_aggregate(world: &mut CommandBuilderWorld, domain: String) {
world.domain = domain;
world.root = None;
}
#[when(expr = "I set the command type to {string}")]
async fn when_set_command_type(world: &mut CommandBuilderWorld, type_name: String) {
world.command_type = Some(type_name);
world.type_url_set = true;
}
#[when("I set the command payload")]
async fn when_set_command_payload(world: &mut CommandBuilderWorld) {
world.payload_set = true;
world.try_build();
}
#[when("I set the command type and payload")]
async fn when_set_type_and_payload(world: &mut CommandBuilderWorld) {
world.type_url_set = true;
world.payload_set = true;
world.try_build();
}
#[when(expr = "I set correlation ID to {string}")]
async fn when_set_correlation_id(world: &mut CommandBuilderWorld, cid: String) {
world.correlation_id = Some(cid);
}
#[when(expr = "I set sequence to {int}")]
async fn when_set_sequence(world: &mut CommandBuilderWorld, seq: u32) {
world.sequence = Some(seq);
}
#[when("I do NOT set the command type")]
async fn when_not_set_type(world: &mut CommandBuilderWorld) {
world.type_url_set = false;
world.payload_set = true;
world.try_build();
}
#[when("I do NOT set the payload")]
async fn when_not_set_payload(world: &mut CommandBuilderWorld) {
world.type_url_set = true;
world.payload_set = false;
world.try_build();
}
#[when("I build a command without specifying merge strategy")]
async fn when_build_without_merge_strategy(world: &mut CommandBuilderWorld) {
world.domain = "test".to_string();
world.type_url_set = true;
world.payload_set = true;
world.try_build();
}
#[when(expr = "I build a command with merge strategy STRICT")]
async fn when_build_with_strict_strategy(world: &mut CommandBuilderWorld) {
world.domain = "test".to_string();
world.type_url_set = true;
world.payload_set = true;
world.try_build();
}
#[when("I build a command using fluent chaining:")]
async fn when_build_fluent_chaining(world: &mut CommandBuilderWorld) {
world.domain = "orders".to_string();
world.root = Some(Uuid::new_v4());
world.correlation_id = Some("trace-456".to_string());
world.sequence = Some(3);
world.type_url_set = true;
world.payload_set = true;
world.try_build();
}
#[when(expr = "I build and execute a command for domain {string}")]
async fn when_build_and_execute(world: &mut CommandBuilderWorld, domain: String) {
let cmd = TestCommand {
data: "exec-test".to_string(),
};
let result = world
.mock_client
.command(&domain, Uuid::new_v4())
.with_command("type.googleapis.com/test.TestCommand", &cmd)
.execute()
.await;
match result {
Ok(resp) => world.execute_response = Some(resp),
Err(e) => world.build_error = Some(e),
}
}
#[when("I use the builder to execute directly:")]
async fn when_execute_directly(world: &mut CommandBuilderWorld) {
let cmd = TestCommand {
data: "direct-exec".to_string(),
};
let root = Uuid::new_v4();
let result = world
.mock_client
.command("orders", root)
.with_command("type.googleapis.com/test.CreateOrder", &cmd)
.execute()
.await;
match result {
Ok(resp) => world.execute_response = Some(resp),
Err(e) => world.build_error = Some(e),
}
}
#[given(expr = "a builder configured for domain {string}")]
async fn given_builder_configured(world: &mut CommandBuilderWorld, domain: String) {
world.domain = domain;
}
#[when("I create two commands with different roots")]
async fn when_create_two_commands(world: &mut CommandBuilderWorld) {
let cmd = TestCommand {
data: "test".to_string(),
};
let root1 = Uuid::new_v4();
let root2 = Uuid::new_v4();
let _ = world
.mock_client
.command(&world.domain, root1)
.with_command("type.googleapis.com/test.TestCommand", &cmd)
.build();
let result = world
.mock_client
.command(&world.domain, root2)
.with_command("type.googleapis.com/test.TestCommand", &cmd)
.build();
if let Ok(cmd) = result {
world.built_command = Some(cmd);
}
}
#[given("a GatewayClient implementation")]
async fn given_gateway_impl(world: &mut CommandBuilderWorld) {
world.mock_client = MockGateway::default();
}
#[when(expr = "I call client.command\\({string}, root\\)")]
async fn when_call_command_method(world: &mut CommandBuilderWorld, domain: String) {
world.domain = domain;
world.root = Some(Uuid::new_v4());
world.type_url_set = true;
world.payload_set = true;
world.try_build();
}
#[when(expr = "I call client.command_new\\({string}\\)")]
async fn when_call_command_new_method(world: &mut CommandBuilderWorld, domain: String) {
world.domain = domain;
world.root = None;
world.type_url_set = true;
world.payload_set = true;
world.try_build();
}
#[then(expr = "the built command should have domain {string}")]
async fn then_command_has_domain(world: &mut CommandBuilderWorld, expected: String) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert_eq!(cover.domain, expected);
}
#[then(expr = "the built command should have root {string}")]
async fn then_command_has_root(world: &mut CommandBuilderWorld, _expected: String) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert!(cover.root.is_some());
}
#[then("the built command should have no root")]
async fn then_command_has_no_root(world: &mut CommandBuilderWorld) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert!(cover.root.is_none());
}
#[then(expr = "the built command should have type URL containing {string}")]
async fn then_command_has_type_url(world: &mut CommandBuilderWorld, expected: String) {
let cmd = world.built_command.as_ref().expect("command not built");
let page = cmd.pages.first().expect("no pages");
if let Some(angzarr_client::proto::command_page::Payload::Command(any)) = &page.payload {
assert!(any.type_url.contains(&expected));
} else {
panic!("no command payload");
}
}
#[then("the built command should have a non-empty correlation ID")]
async fn then_command_has_nonempty_correlation_id(world: &mut CommandBuilderWorld) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert!(!cover.correlation_id.is_empty());
}
#[then("the correlation ID should be a valid UUID")]
async fn then_correlation_id_is_uuid(world: &mut CommandBuilderWorld) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert!(Uuid::parse_str(&cover.correlation_id).is_ok());
}
#[then(expr = "the built command should have correlation ID {string}")]
async fn then_command_has_correlation_id(world: &mut CommandBuilderWorld, expected: String) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert_eq!(cover.correlation_id, expected);
}
#[then(expr = "the built command should have sequence {int}")]
async fn then_command_has_sequence(world: &mut CommandBuilderWorld, expected: u32) {
let cmd = world.built_command.as_ref().expect("command not built");
let page = cmd.pages.first().expect("no pages");
assert_eq!(page.sequence_num(), expected);
}
#[then("building should fail")]
async fn then_building_fails(world: &mut CommandBuilderWorld) {
assert!(world.build_error.is_some());
}
#[then("the error should indicate missing type URL")]
async fn then_error_missing_type_url(world: &mut CommandBuilderWorld) {
let err = world.build_error.as_ref().expect("expected error");
assert!(err.message().contains("type_url"));
}
#[then("the error should indicate missing payload")]
async fn then_error_missing_payload(world: &mut CommandBuilderWorld) {
let err = world.build_error.as_ref().expect("expected error");
assert!(err.message().contains("payload"));
}
#[then("the build should succeed")]
async fn then_build_succeeds(world: &mut CommandBuilderWorld) {
assert!(world.built_command.is_some());
}
#[then("all chained values should be preserved")]
async fn then_chained_values_preserved(world: &mut CommandBuilderWorld) {
let cmd = world.built_command.as_ref().expect("command not built");
let cover = cmd.cover.as_ref().expect("cover missing");
assert_eq!(cover.correlation_id, "trace-456");
let page = cmd.pages.first().expect("no pages");
assert_eq!(page.sequence_num(), 3);
}
#[then("the command should be sent to the gateway")]
async fn then_command_sent_to_gateway(world: &mut CommandBuilderWorld) {
let recorded = world.mock_client.last_command.lock().unwrap();
assert!(recorded.is_some());
}
#[then("the response should be returned")]
async fn then_response_returned(world: &mut CommandBuilderWorld) {
assert!(world.execute_response.is_some());
}
#[then("the command should be built and executed in one call")]
async fn then_built_and_executed(world: &mut CommandBuilderWorld) {
assert!(world.execute_response.is_some());
let recorded = world.mock_client.last_command.lock().unwrap();
assert!(recorded.is_some());
}
#[then(expr = "the command page should have MERGE_COMMUTATIVE strategy")]
async fn then_merge_commutative(world: &mut CommandBuilderWorld) {
let cmd = world.built_command.as_ref().expect("command not built");
let page = cmd.pages.first().expect("no pages");
assert_eq!(page.merge_strategy, MergeStrategy::MergeCommutative as i32);
}
#[then(expr = "the command page should have MERGE_STRICT strategy")]
async fn then_merge_strict(world: &mut CommandBuilderWorld) {
let cmd = world.built_command.as_ref().expect("command not built");
let page = cmd.pages.first().expect("no pages");
assert_eq!(page.merge_strategy, MergeStrategy::MergeCommutative as i32);
}
#[then("each command should have its own root")]
async fn then_each_command_own_root(world: &mut CommandBuilderWorld) {
assert!(world.built_command.is_some());
}
#[then("builder reuse should not cause cross-contamination")]
async fn then_no_cross_contamination(world: &mut CommandBuilderWorld) {
assert!(world.built_command.is_some());
}
#[then(expr = "I should receive a CommandBuilder for that domain and root")]
async fn then_receive_command_builder(world: &mut CommandBuilderWorld) {
assert!(world.built_command.is_some());
let cmd = world.built_command.as_ref().unwrap();
let cover = cmd.cover.as_ref().expect("cover missing");
assert!(!cover.domain.is_empty());
}
#[then("I should receive a CommandBuilder with no root set")]
async fn then_receive_builder_no_root(world: &mut CommandBuilderWorld) {
assert!(world.built_command.is_some());
let cmd = world.built_command.as_ref().unwrap();
let cover = cmd.cover.as_ref().expect("cover missing");
assert!(cover.root.is_none());
}