Skip to content

Commit

Permalink
Added example of basic CQRS using Endpoints, Nullable Reference Types…
Browse files Browse the repository at this point in the history
…, Records and other C# 8-9 goodies

Added tests for Registering the Product
Added tests for the Warehouse example's queries
Updated project and test configuration to run migrations automatically
  • Loading branch information
oskardudycz committed May 17, 2021
1 parent 8881f95 commit 9c348dc
Show file tree
Hide file tree
Showing 40 changed files with 1,602 additions and 9 deletions.
7 changes: 5 additions & 2 deletions Core.Marten/Config.cs
Expand Up @@ -54,8 +54,11 @@ public static class MartenConfigExtensions
{
options.Connection(config.ConnectionString);
options.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;
options.Events.DatabaseSchemaName = config.WriteModelSchema;
options.DatabaseSchemaName = config.ReadModelSchema;

var schemaName = Environment.GetEnvironmentVariable("SchemaName");
options.Events.DatabaseSchemaName = schemaName ?? config.WriteModelSchema;
options.DatabaseSchemaName = schemaName ?? config.ReadModelSchema;

options.UseDefaultSerialization(nonPublicMembersStorage: NonPublicMembersStorage.NonPublicSetters,
enumStorage: EnumStorage.AsString);
options.Events.Daemon.Mode = config.DaemonMode;
Expand Down
2 changes: 2 additions & 0 deletions Core.Testing/ApiFixture.cs
Expand Up @@ -31,6 +31,8 @@ public abstract class ApiFixture: IAsyncLifetime

protected ApiFixture()
{
Environment.SetEnvironmentVariable("SchemaName", GetType().Name.ToLower());

Sut = CreateTestContext();
}

Expand Down
2 changes: 1 addition & 1 deletion Core.Testing/TestWebHostBuilder.cs
Expand Up @@ -15,7 +15,7 @@ public static IWebHostBuilder Create(Dictionary<string, string> configuration, A
configureServices ??= _ => { };

return new WebHostBuilder()
.UseEnvironment("Tests")
.UseEnvironment("Development")
.UseContentRoot(projectDir)
.UseConfiguration(new ConfigurationBuilder()
.SetBasePath(projectDir)
Expand Down
24 changes: 24 additions & 0 deletions EventSourcing.NetCore.sln
Expand Up @@ -177,6 +177,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools", "Workshops\BuildYou
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solved", "Solved", "{C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Warehouse", "Warehouse", "{4AC3138B-6FD1-4620-A75A-3FCACE995162}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse", "Sample\Warehouse\Warehouse\Warehouse.csproj", "{C45ACE62-41BA-49D9-956A-39B479D7A50A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Sample\Warehouse\Warehouse.Api\Warehouse.Api.csproj", "{76C04CB6-32C7-47EA-884A-6343BDD39644}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "Sample\Warehouse\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{69B22937-CA8B-478D-97F8-4D33558B5BC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -419,6 +427,18 @@ Global
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C54B28C-7746-4DD0-865E-1AC0D8E8D46B}.Release|Any CPU.Build.0 = Release|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C45ACE62-41BA-49D9-956A-39B479D7A50A}.Release|Any CPU.Build.0 = Release|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76C04CB6-32C7-47EA-884A-6343BDD39644}.Release|Any CPU.Build.0 = Release|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69B22937-CA8B-478D-97F8-4D33558B5BC9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -496,6 +516,10 @@ Global
{C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0} = {94524EA9-A4BA-4684-99B8-BBE9EE85E791}
{7ACC398F-87BF-4B3E-AD61-DFB5F56D4B25} = {C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}
{03D0848C-7B19-4685-BA1F-59FFAF1DCEA6} = {C9011E7F-42EA-4FEE-A5BB-EFC11BBA0DB0}
{4AC3138B-6FD1-4620-A75A-3FCACE995162} = {A7186B6B-D56D-4AEF-B6B7-FAA827764C34}
{C45ACE62-41BA-49D9-956A-39B479D7A50A} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
{76C04CB6-32C7-47EA-884A-6343BDD39644} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
{69B22937-CA8B-478D-97F8-4D33558B5BC9} = {4AC3138B-6FD1-4620-A75A-3FCACE995162}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
Expand Down
Expand Up @@ -45,7 +45,6 @@ public CreateMeetingTests(CreateMeetingFixture fixture)
}

[Fact]
[Trait("Category", "Exercise")]
public async Task CreateCommand_ShouldReturn_CreatedStatus_With_MeetingId()
{
var commandResponse = fixture.CommandResponse;
Expand All @@ -58,7 +57,6 @@ public async Task CreateCommand_ShouldReturn_CreatedStatus_With_MeetingId()
}

[Fact]
[Trait("Category", "Exercise")]
public void CreateCommand_ShouldPublish_MeetingCreateEvent()
{
// assert MeetingCreated event was produced to external bus
Expand All @@ -70,7 +68,6 @@ public void CreateCommand_ShouldPublish_MeetingCreateEvent()
}

[Fact]
[Trait("Category", "Exercise")]
public async Task CreateCommand_ShouldUpdateReadModel()
{
// prepare query
Expand Down
Expand Up @@ -53,7 +53,6 @@ public ScheduleMeetingTests(ScheduleMeetingFixture fixture)
}

[Fact]
[Trait("Category", "Exercise")]
public async Task CreateMeeting_ShouldReturn_CreatedStatus_With_MeetingId()
{
var commandResponse = fixture.CreateMeetingCommandResponse.EnsureSuccessStatusCode();
Expand All @@ -64,7 +63,6 @@ public async Task CreateMeeting_ShouldReturn_CreatedStatus_With_MeetingId()
}

[Fact]
[Trait("Category", "Exercise")]
public async Task ScheduleMeeting_ShouldSucceed()
{
var commandResponse = fixture.ScheduleMeetingCommandResponse.EnsureSuccessStatusCode();
Expand All @@ -75,7 +73,6 @@ public async Task ScheduleMeeting_ShouldSucceed()
}

[Fact]
[Trait("Category", "Exercise")]
public async Task ScheduleMeeting_ShouldUpdateReadModel()
{
//send query
Expand Down
@@ -0,0 +1,92 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Core.Testing;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Warehouse.Products.GettingProductDetails;
using Warehouse.Products.RegisteringProduct;
using Xunit;

namespace Warehouse.Api.Tests.Products.GettingProductDetails
{
public class GetProductDetailsFixture: ApiFixture
{
protected override string ApiUrl => "/api/products";

protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder =>
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(GetProductDetailsFixture));

public ProductDetails ExistingProduct = default!;

public Guid ProductId = default!;

public override async Task InitializeAsync()
{
var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription");
var registerResponse = await Post(registerProduct);

registerResponse.EnsureSuccessStatusCode()
.StatusCode.Should().Be(HttpStatusCode.Created);

ProductId = await registerResponse.GetResultFromJson<Guid>();

var (sku, name, description) = registerProduct;
ExistingProduct = new ProductDetails(ProductId, sku!, name!, description);
}
}

public class GetProductDetailsTests: IClassFixture<GetProductDetailsFixture>
{
private readonly GetProductDetailsFixture fixture;

public GetProductDetailsTests(GetProductDetailsFixture fixture)
{
this.fixture = fixture;
}

[Fact]
public async Task ValidRequest_With_NoParams_ShouldReturn_200()
{
// Given

// When
var response = await fixture.Get(fixture.ProductId.ToString());

// Then
response.EnsureSuccessStatusCode()
.StatusCode.Should().Be(HttpStatusCode.OK);

var product = await response.GetResultFromJson<ProductDetails>();
product.Should().NotBeNull();
product.Should().BeEquivalentTo(fixture.ExistingProduct);
}

[Theory]
[InlineData(12)]
[InlineData("not-a-guid")]
public async Task InvalidGuidId_ShouldReturn_400(object invalidId)
{
// Given

// When
var response = await fixture.Get($"{invalidId}");

// Then
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}

[Fact]
public async Task NotExistingId_ShouldReturn_404()
{
// Given
var notExistingId = Guid.NewGuid();

// When
var response = await fixture.Get($"{notExistingId}");

// Then
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
}
}
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Core.Testing;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Warehouse.Products.GettingProducts;
using Warehouse.Products.RegisteringProduct;
using Xunit;

namespace Warehouse.Api.Tests.Products.GettingProducts
{
public class GetProductsFixture: ApiFixture
{
protected override string ApiUrl => "/api/products";

protected override Func<IWebHostBuilder, IWebHostBuilder> SetupWebHostBuilder =>
whb => WarehouseTestWebHostBuilder.Configure(whb, nameof(GetProductsFixture));

public IList<ProductListItem> RegisteredProducts = new List<ProductListItem>();

public override async Task InitializeAsync()
{
var productsToRegister = new[]
{
new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"),
new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"),
new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription")
};

foreach (var registerProduct in productsToRegister)
{
var registerResponse = await Post(registerProduct);
registerResponse.EnsureSuccessStatusCode()
.StatusCode.Should().Be(HttpStatusCode.Created);

var createdId = await registerResponse.GetResultFromJson<Guid>();

var (sku, name, _) = registerProduct;
RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!));
}
}
}

public class GetProductsTests: IClassFixture<GetProductsFixture>
{
private readonly GetProductsFixture fixture;

public GetProductsTests(GetProductsFixture fixture)
{
this.fixture = fixture;
}

[Fact]
public async Task ValidRequest_With_NoParams_ShouldReturn_200()
{
// Given

// When
var response = await fixture.Get();

// Then
response.EnsureSuccessStatusCode()
.StatusCode.Should().Be(HttpStatusCode.OK);

var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>();
products.Should().NotBeEmpty();
products.Should().BeEquivalentTo(fixture.RegisteredProducts);
}

[Fact]
public async Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords()
{
// Given
var filteredRecord = fixture.RegisteredProducts.First();
var filter = fixture.RegisteredProducts.First().Sku.Substring(1);

// When
var response = await fixture.Get($"?filter={filter}");

// Then
response.EnsureSuccessStatusCode()
.StatusCode.Should().Be(HttpStatusCode.OK);

var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>();
products.Should().NotBeEmpty();
products.Should().BeEquivalentTo(new List<ProductListItem>{filteredRecord});
}



[Fact]
public async Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords()
{
// Given
const int page = 2;
const int pageSize = 1;
var filteredRecords = fixture.RegisteredProducts
.Skip(page - 1)
.Take(pageSize)
.ToList();

// When
var response = await fixture.Get($"?page={page}&pageSize={pageSize}");

// Then
response.EnsureSuccessStatusCode()
.StatusCode.Should().Be(HttpStatusCode.OK);

var products = await response.GetResultFromJson<IReadOnlyList<ProductListItem>>();
products.Should().NotBeEmpty();
products.Should().BeEquivalentTo(filteredRecords);
}

[Fact]
public async Task NegativePage_ShouldReturn_400()
{
// Given
var pageSize = -20;

// When
var response = await fixture.Get($"?page={pageSize}");

// Then
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}

[Theory]
[InlineData(0)]
[InlineData(-20)]
public async Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize)
{
// Given

// When
var response = await fixture.Get($"?page={pageSize}");

// Then
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
}

0 comments on commit 9c348dc

Please sign in to comment.