public async Task Posting_command_JSON_applies_a_command_with_the_specified_name_to_an_aggregate_with_the_specified_id() { var order = new Order(Guid.NewGuid(), new Order.CustomerInfoChanged { CustomerName = "Joe" }); await order.SaveToEventStore(); var json = new AddItem { Quantity = 5, Price = 19.99m, ProductName = "Bag o' Treats" }.ToJson(); var request = new HttpRequestMessage(HttpMethod.Post, string.Format("http://contoso.com/orders/{0}/additem", order.Id)) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; var testApi = new TestApi<Order>(); var client = testApi.GetClient(); var response = client.SendAsync(request).Result; response.StatusCode.Should().Be(HttpStatusCode.OK); var updatedOrder = await new SqlEventSourcedRepository<Order>().GetLatest(order.Id); updatedOrder.Items.Single().Quantity.Should().Be(5); }
public void Command_properties_can_be_validated() { var order = new Order(Guid.NewGuid()) .Apply(new ChangeCustomerInfo { CustomerName = "Joe" }) .Apply(new Deliver()) .SavedToEventStore(); var httpClient = new TestApi<Order>().GetClient(); var result = httpClient.PostAsync( $"http://contoso.com/orders/{order.Id}/additem/validate", new JsonContent(new AddItem { Price = 1m, Quantity = 1, ProductName = "Widget" })).Result; result.ShouldSucceed(); var content = result.Content.ReadAsStringAsync().Result; Console.WriteLine(content); content.Should().Contain("\"Failures\":[{\"Message\":\"The order has already been fulfilled.\""); }
public async Task Command_properties_can_be_validated() { var order = new Order( Guid.NewGuid(), new Order.Fulfilled()); await order.SaveToEventStore(); Console.WriteLine(order.Id); var httpClient = new TestApi<Order>().GetClient(); var result = httpClient.PostAsync( string.Format("http://contoso.com/orders/{0}/additem/validate", order.Id), new JsonContent(new AddItem { Price = 1m, Quantity = 1, ProductName = "Widget" })).Result; result.ShouldSucceed(); var content = result.Content.ReadAsStringAsync().Result; Console.WriteLine(content); content.Should().Contain("\"Failures\":[{\"Message\":\"The order has already been fulfilled.\""); }
public async Task Posting_an_invalid_command_does_not_affect_the_aggregate_state() { var order = new Order(Guid.NewGuid()) .Apply(new ChangeCustomerInfo { CustomerName = "Joe" }) .Apply(new Deliver()) .SavedToEventStore(); var json = new AddItem { Quantity = 5, Price = 19.99m, ProductName = "Bag o' Treats" }.ToJson(); var request = new HttpRequestMessage(HttpMethod.Post, $"http://contoso.com/orders/{order.Id}/additem") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; var testApi = new TestApi<Order>(); var client = testApi.GetClient(); var response = client.SendAsync(request).Result; response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var updatedOrder = await Configuration.Current.Repository<Order>().GetLatest(order.Id); updatedOrder.Items.Count.Should().Be(0); }
public void The_clock_set_in_the_CommandContext_is_used_by_resulting_events() { var created = DateTimeOffset.Parse("2014-05-15 00:00:00"); var addItem = new AddItem { ProductName = "Widget", Price = 3.99m }; Order order; using (CommandContext.Establish(addItem, Clock.Create(() => created))) { order = new Order(new CreateOrder(Any.FullName())); order.Apply(addItem); } order.Events() .Count() .Should() .Be(3); order.Events() .Should() .OnlyContain(e => e.Timestamp == created); }
public void A_constructor_command_can_be_used_to_create_a_new_aggregate_instance() { var customerName = Any.FullName(); var order = new Order(new CreateOrder(customerName)); order.CustomerName.Should().Be(customerName); order.Version.Should().Be(2); }
public void SetUp() { Command<Order>.AuthorizeDefault = delegate { return true; }; customerName = Any.FullName(); var order = new Order(new CreateOrder(customerName)); repository.Save(order).Wait(); aggregateId = order.Id; }
public void ConstructorCommand_AggregateId_is_used_to_specify_the_new_instances_Id() { var id = Any.Guid(); var createOrder = new CreateOrder(Any.Paragraph(2)) { AggregateId = id }; var order = new Order(createOrder); order.Id.Should().Be(id); }
public void Command_applicability_can_be_validated_by_the_command_class() { var cancel = new Cancel(); var order = new Order(); order.IsValidTo(cancel).Should().Be(true); order.Apply(new Deliver()); order.IsValidTo(cancel).Should().Be(false); }
public void AuthorizationPolicy_For_returns_an_instance_based_on_the_actual_rather_than_declared_type_of_the_principal() { AuthorizationFor<Customer>.ToApply<Cancel>.ToA<Order> .Requires((a, b, c) => true); IPrincipal iprincipal = new Customer(); var customer = new Customer(); var cancel = new Cancel(); var order = new Order(); iprincipal.IsAuthorizedTo(cancel, order).Should().Be(true); customer.IsAuthorizedTo(cancel, order).Should().Be(true); }
public void Constructor_commands_cannot_be_used_on_aggregates_that_have_prior_events() { var order = new Order(); order.Apply(new AddItem { Price = 1, ProductName = Any.CamelCaseName() }); Action apply = () => order.Apply(new CreateOrder(Any.CamelCaseName())); apply.ShouldThrow<ConcurrencyException>(); }
public async Task ApplyBatch_can_accept_an_array_of_commands() { var order = new Order().SavedToEventStore(); var json = new[] { new { AddItem = new { Quantity = 1, Price = 1, ProductName = "Sprocket" } }, new { AddItem = new { Quantity = 1, Price = 2, ProductName = "Cog" } } }.ToJson(); var testApi = new TestApi<Order>(); var client = testApi.GetClient(); var request = new HttpRequestMessage(HttpMethod.Post, $"http://contoso.com/orders/{order.Id}") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; var response = await client.SendAsync(request); response.ShouldSucceed(); order = await Configuration.Current.Repository<Order>().GetLatest(order.Id); order.Items.Count.Should().Be(2); order.Balance.Should().Be(3); }
public void The_event_Actor_is_set_from_the_Command_Principal() { // arrange var customer = new Customer(Any.FullName()); var serviceRepresentative = new CustomerServiceRepresentative { Name = Any.FullName() }; var command = new SpecifyShippingInfo { Principal = serviceRepresentative }; var order = new Order(new CreateOrder(customer.Name) { Principal = customer }); // act order.Apply(command); // assert order.Events() .OfType<Order.Created>() .Single() .Actor() .Should() .Be(customer.Name); order.Events() .OfType<Order.ShippingMethodSelected>() .Single() .Actor() .Should() .Be(serviceRepresentative.Name); }
public async Task When_a_command_is_delivered_a_second_time_with_the_same_ETag_it_is_not_retried_afterward() { // arrange var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = Any.Guid() }); await Save(order); var command = new AddItem { ProductName = Any.Word(), Price = 10m, ETag = Any.Guid().ToString() }; // act var scheduledCommand = await Schedule(order.Id, command, Clock.Now().AddDays(1)); var deliverer = Configuration .Current .CommandDeliverer<Order>(); await deliverer.Deliver(scheduledCommand); await deliverer.Deliver(scheduledCommand); // assert order = await Get<Order>(order.Id); order.Balance.Should().Be(10); using (var db = CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == order.Id) .Should() .OnlyContain(c => c.AppliedTime != null); db.Errors .Where(c => c.ScheduledCommand.AggregateId == order.Id) .Should() .BeEmpty(); } }
public void When_one_command_triggers_another_command_within_EnactCommand_then_the_second_command_uses_the_CommandContext_clock() { var clockTime = DateTimeOffset.Parse("2014-05-13 09:28:42 AM"); var shipOn = new ShipOn(DateTimeOffset.Parse("2014-06-01 00:00:00")); Order order; using (CommandContext.Establish(shipOn, Clock.Create(() => clockTime))) { order = new Order().Apply(shipOn); } order.Events() .OfType<CommandScheduled<Order>>() .Single() .Timestamp .Should() .Be(clockTime); }
public override async Task A_command_handler_can_cancel_a_scheduled_command_after_it_fails() { var order = new Order( new CreateOrder(Any.FullName())) .Apply(new AddItem { Price = 499.99m, ProductName = Any.Words(1, true).Single() }) .Apply(new SpecifyShippingInfo { Address = Any.Words(1, true).Single() + " St.", City = "Seattle", StateOrProvince = "WA", Country = "USA" }) .Apply(new ShipOn(Clock.Now().AddDays(10))) .Apply(new Ship()); await Save(order); var result = await AdvanceClock(11.Days()); // the command should fail validation result.FailedCommands.Count().Should().Be(1); result = await AdvanceClock(1.Hours()); result.FailedCommands.Count().Should().Be(0); }
public async Task A_command_can_be_scheduled_against_another_aggregate() { var order = new Order( new CreateOrder(Any.FullName()) { CustomerId = customerAccountId }) .Apply(new AddItem { ProductName = Any.Word(), Price = 12.99m }) .Apply(new Cancel()); await orderRepository.Save(order); var customerAccount = await customerRepository.GetLatest(customerAccountId); customerAccount.Events() .Last() .Should() .BeOfType<CustomerAccount.OrderCancelationConfirmationEmailSent>(); }
public void UseDependencies_can_be_used_to_set_dependencies_using_an_application_owned_container() { var applicationsContainer = new PocketContainer() .Register<IPaymentService>(_ => new CreditCardPaymentGateway(chargeLimit: 1)); var configuration = new Configuration() .UseDependencies(type => { if (applicationsContainer.Any(reg => reg.Key == type)) { return () => applicationsContainer.Resolve(type); } return null; }); using (ConfigurationContext.Establish(configuration)) { var order = new Order(new CreateOrder(Any.FullName())) .Apply(new AddItem { Price = 5m, ProductName = Any.Word() }) .Apply(new Ship()) .Apply(new ChargeAccount { AccountNumber = Any.PositiveInt().ToString() }); order.Events() .Last() .Should() .BeOfType<Order.PaymentConfirmed>(); } }
public override async Task When_a_command_is_scheduled_but_an_exception_is_thrown_in_a_handler_then_an_error_is_recorded() { Configuration.Current.UseDependency(_ => new CustomerAccount.OrderEmailConfirmer { SendOrderConfirmationEmail = x => { throw new Exception("drat!"); } }); // create a customer account var customer = new CustomerAccount(Any.Guid()) .Apply(new ChangeEmailAddress(Any.Email())); await accountRepository.Save(customer); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customer.Id }); await orderRepository.Save(order); // act order.Apply(new Cancel()); await orderRepository.Save(order); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromMinutes(1.2)); using (var db = new CommandSchedulerDbContext()) { db.Errors .Where(e => e.ScheduledCommand.AggregateId == customer.Id) .Should() .Contain(e => e.Error.Contains("drat!")); } }
public async Task When_a_command_is_delivered_a_second_time_with_the_same_ETag_it_is_not_retried_afterward() { // arrange var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = Any.Guid() }); await orderRepository.Save(order); var command = new AddItem { ProductName = Any.Word(), Price = 10m, ETag = Any.Guid().ToString() }; var commandScheduler = Configuration.Current.CommandScheduler<Order>(); // act await commandScheduler.Schedule(order.Id, command, Clock.Now().AddDays(1)); Thread.Sleep(1); // the sequence number is set from the current tick count, which every now and then produces a duplicate here await commandScheduler.Schedule(order.Id, command, Clock.Now().AddDays(1)); await clockTrigger.Trigger(cmd => cmd.Where(c => c.AggregateId == order.Id)); // assert order = await orderRepository.GetLatest(order.Id); order.Balance.Should().Be(10); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == order.Id) .Should() .OnlyContain(c => c.AppliedTime != null); db.Errors .Where(c => c.ScheduledCommand.AggregateId == order.Id) .Should() .BeEmpty(); } }
public override async Task A_command_handler_can_cancel_a_scheduled_command_after_it_fails() { var order = new Order( new CreateOrder(Any.FullName())) .Apply(new AddItem { Price = 499.99m, ProductName = Any.Words(1, true).Single() }) .Apply(new SpecifyShippingInfo { Address = Any.Words(1, true).Single() + " St.", City = "Seattle", StateOrProvince = "WA", Country = "USA" }) .Apply(new ShipOn(Clock.Now().AddDays(10))) .Apply(new Ship()); await orderRepository.Save(order); Func<IQueryable<ScheduledCommand>, IQueryable<ScheduledCommand>> query = cmds => cmds.Where(cmd => cmd.AggregateId == order.Id) .Where(cmd => cmd.AppliedTime == null && cmd.FinalAttemptTime == null); var result = await clockTrigger.Trigger(query); // the command should fail validation result.FailedCommands.Count().Should().Be(1); result = await clockTrigger.Trigger(query); result.FailedCommands.Count().Should().Be(0); }
public override async Task When_a_command_is_scheduled_but_the_target_it_applies_to_is_not_found_then_the_command_is_retried() { // create and cancel an order for a nonexistent customer account var customerId = Any.Guid(); Console.WriteLine(new { customerId }); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customerId }); order.Apply(new Cancel()); await orderRepository.Save(order); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customerId) .Should() .ContainSingle(c => c.AppliedTime == null); } // act // now save the customer and advance the clock await accountRepository.Save(new CustomerAccount(customerId).Apply(new ChangeEmailAddress(Any.Email()))); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromMinutes(2)); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customerId) .Should() .ContainSingle(c => c.AppliedTime != null); var customer = await accountRepository.GetLatest(customerId); customer.Events() .Last() .Should().BeOfType<CustomerAccount.OrderCancelationConfirmationEmailSent>(); } }
public async Task When_a_command_is_scheduled_but_the_event_that_triggered_it_was_not_successfully_written_then_the_command_is_not_applied() { VirtualClock.Start(); // create a customer account var customer = new CustomerAccount(Any.Guid()) .Apply(new ChangeEmailAddress(Any.Email())); await accountRepository.Save(customer); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customer.Id }); // act // cancel the order but don't save it order.Apply(new Cancel()); // assert // verify that the customer did not receive the scheduled NotifyOrderCanceled command customer = await accountRepository.GetLatest(customer.Id); customer.Events().Last().Should().BeOfType<CustomerAccount.EmailAddressChanged>(); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customer.Id) .Should() .ContainSingle(c => c.AppliedTime == null); } }
public void ToApplyAnyCommand_allows_an_authorization_rule_to_be_declared_for_all_commands_for_a_given_resource_type() { var principals = new List<IPrincipal>(); var resources = new List<EventSourcedAggregate>(); AuthorizationFor<Customer>.ToApplyAnyCommand.ToA<Order> .Requires((principal, resource) => { principals.Add(principal); resources.Add(resource); return true; }); var customer = new Customer(); var order1 = new Order(); var order2 = new Order(); customer.IsAuthorizedTo(new Cancel(), order1) .Should().BeTrue(); customer.IsAuthorizedTo(new Place(), order2) .Should().BeTrue(); principals.Should().Contain(customer); resources.Should().Contain(order1); resources.Should().Contain(order2); }
public async Task Posting_a_command_that_causes_a_concurrency_error_returns_409_Conflict() { var order = new Order(Guid.NewGuid()) .Apply(new ChangeCustomerInfo { CustomerName = "Joe" }) .SavedToEventStore(); var testApi = new TestApi<Order>(); var repository = new Mock<IEventSourcedRepository<Order>>(); repository.Setup(r => r.GetLatest(It.IsAny<Guid>())) .Returns(Task.FromResult(order)); repository.Setup(r => r.Save(It.IsAny<Order>())) .Throws(new ConcurrencyException("oops!", new IEvent[0], new Exception("inner oops"))); Configuration.Current.UseDependency(c => repository.Object); var client = testApi.GetClient(); var response = await client.PostAsJsonAsync($"http://contoso.com/orders/{order.Id}/additem", new { Price = 3m, ProductName = Any.Word() }); response.ShouldFailWith(HttpStatusCode.Conflict); }
public async Task Posting_unauthorized_command_JSON_returns_403_Forbidden() { var order = new Order(Guid.NewGuid()) .Apply(new ChangeCustomerInfo { CustomerName = "Joe" }) .Apply(new Deliver()) .SavedToEventStore(); Command<Order>.AuthorizeDefault = (o, command) => false; var request = new HttpRequestMessage(HttpMethod.Post, $"http://contoso.com/orders/{order.Id}/cancel") { Content = new StringContent(new Cancel().ToJson(), Encoding.UTF8, "application/json") }; var client = new TestApi<Order>().GetClient(); var response = await client.SendAsync(request); response.StatusCode.Should().Be(HttpStatusCode.Forbidden); }
public async Task An_ETag_header_is_applied_to_the_command() { var order = new Order(Guid.NewGuid()) .Apply(new ChangeCustomerInfo { CustomerName = "Joe" }) .SavedToEventStore(); var json = new AddItem { Quantity = 5, Price = 19.99m, ProductName = "Bag o' Treats" }.ToJson(); var etag = new EntityTagHeaderValue("\"" + Any.Guid() + "\""); Func<HttpRequestMessage> createRequest = () => { var request = new HttpRequestMessage(HttpMethod.Post, $"http://contoso.com/orders/{order.Id}/additem") { Content = new StringContent(json, Encoding.UTF8, "application/json") }; request.Headers.IfNoneMatch.Add(etag); return request; }; var testApi = new TestApi<Order>(); var client = testApi.GetClient(); // act: send the request twice var response1 = await client.SendAsync(createRequest()); var response2 = await client.SendAsync(createRequest()); // assert response1.ShouldSucceed(HttpStatusCode.OK); response2.ShouldFailWith(HttpStatusCode.NotModified); var updatedOrder = await Configuration.Current.Repository<Order>().GetLatest(order.Id); updatedOrder.Items.Single().Quantity.Should().Be(5); }