Skip to content
This repository was archived by the owner on Feb 24, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6f4edce
Scaffold C18 solution
Carl-Hugo Aug 8, 2023
ed7dcf8
Move the REPR project to a subfolder
Carl-Hugo Aug 12, 2023
d01675f
Add the SimpleEndpoint project
Carl-Hugo Aug 12, 2023
8d06afa
Create the REPR diagram
Carl-Hugo Aug 12, 2023
414c9ab
Scaffold features and implement the Products feature
Carl-Hugo Aug 12, 2023
9387428
Create the basket feature
Carl-Hugo Aug 13, 2023
2c53db3
Write tests
Carl-Hugo Aug 13, 2023
fca2d3a
Cleanup
Carl-Hugo Aug 13, 2023
d9e779a
Move FluentValidation registration to Features
Carl-Hugo Aug 13, 2023
69597ad
Update version to the NuGet one
Carl-Hugo Aug 13, 2023
c933f56
Split the GET in two
Carl-Hugo Aug 14, 2023
1a0578f
Add the UnitPrice property to products
Carl-Hugo Aug 14, 2023
ddeea98
Extract exceptions and remove ProductSeeder
Carl-Hugo Aug 14, 2023
a730908
Add folders structure diagram
Carl-Hugo Aug 14, 2023
8f5b9a9
Fix typo in Baskets
Carl-Hugo Aug 14, 2023
8922dec
Add call hierarchy diagrams
Carl-Hugo Aug 15, 2023
c4192b3
Use expression bodies to shorten the code
Carl-Hugo Aug 15, 2023
23b1404
Adding an item to the cart now returns 201
Carl-Hugo Aug 15, 2023
4baba94
Cleanup and fine-tuning
Carl-Hugo Aug 15, 2023
685e411
Add sequence diagram
Carl-Hugo Aug 17, 2023
5595162
Add MyExceptionMiddleware
Carl-Hugo Aug 17, 2023
0c53fee
Move MyExceptionMiddleware to Program.cs
Carl-Hugo Aug 18, 2023
99d7c7f
Remove SeederDelegateAsync method
Carl-Hugo Aug 18, 2023
99a11c0
Use the synchronous Add method
Carl-Hugo Aug 18, 2023
f6ad3df
Cleanup
Carl-Hugo Aug 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions C18/REPR/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coveragereport
22 changes: 22 additions & 0 deletions C18/REPR/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Test coverage

The initial test coverage report indicates 97.2% Line Coverage and 63.1% Branch coverage for the Web assembly.
Most of the not tested branches are related to the constructor injection guards that we have no unit tests for.

## How to collect code coverage

```bash
# 1. Generage the coverage.cobertura.xml file
dotnet test --collect:"XPlat Code Coverage"

# 2. Generate the repport based on the previous file (change the GUID by the GUID generated by the collector)
reportgenerator -reports:"Web.Tests\TestResults\b74d6e70-a4f3-49ff-bf3c-00e4abba742c\coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html
```

## Prerequisites

Run once to install the `reportgenerator` global tool:

```bash
dotnet tool install -g dotnet-reportgenerator-globaltool
```
28 changes: 28 additions & 0 deletions C18/REPR/REPR.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web.Tests", "Web.Tests\Web.Tests.csproj", "{50D34992-16B0-4421-948F-9C7D6A0EEEB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "Web\Web.csproj", "{0653C456-D190-43D1-98BC-74000960AE85}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{50D34992-16B0-4421-948F-9C7D6A0EEEB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50D34992-16B0-4421-948F-9C7D6A0EEEB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50D34992-16B0-4421-948F-9C7D6A0EEEB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50D34992-16B0-4421-948F-9C7D6A0EEEB3}.Release|Any CPU.Build.0 = Release|Any CPU
{0653C456-D190-43D1-98BC-74000960AE85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0653C456-D190-43D1-98BC-74000960AE85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0653C456-D190-43D1-98BC-74000960AE85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0653C456-D190-43D1-98BC-74000960AE85}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
115 changes: 115 additions & 0 deletions C18/REPR/Web.Tests/Features/Baskets/BasketsTest.AddItemTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using static Web.Features.Baskets;

namespace Web.Features;
public partial class BasketsTest
{
public class AddItemTest
{
[Fact]
public async Task Should_add_the_new_item_to_the_basket()
{
// Arrange
await using var application = new C18WebApplication();
var client = application.CreateClient();

// Act
var response = await client.PostAsJsonAsync(
"/baskets",
new AddItem.Command(4, 1, 22)
);

// Assert the response
Assert.NotNull(response);
Assert.True(response.IsSuccessStatusCode);
var result = await response.Content.ReadFromJsonAsync<AddItem.Response>();
Assert.NotNull(result);
Assert.Equal(1, result.ProductId);
Assert.Equal(22, result.Quantity);

// Assert the database state
using var seedScope = application.Services.CreateScope();
var db = seedScope.ServiceProvider.GetRequiredService<BasketContext>();
var dbItem = db.Items.FirstOrDefault(x => x.CustomerId == 4 && x.ProductId == 1);
Assert.NotNull(dbItem);
Assert.Equal(22, dbItem.Quantity);
}

[Fact]
public async Task Should_return_a_valid_product_url()
{
// Arrange
await using var application = new C18WebApplication();
await application.SeedAsync<Products.ProductContext>(async db =>
{
db.Products.RemoveRange(db.Products);
db.Products.Add(new("A test product", 15.22m, 1));
await db.SaveChangesAsync();
});
var client = application.CreateClient();

// Act
var response = await client.PostAsJsonAsync(
"/baskets",
new AddItem.Command(4, 1, 22)
);

// Assert
Assert.NotNull(response);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.NotNull(response.Headers.Location);

var productResponse = await client.GetAsync(response.Headers.Location);
Assert.NotNull(productResponse);
Assert.True(productResponse.IsSuccessStatusCode);
}

[Fact]
public async Task Should_return_a_ProblemDetails_with_a_Conflict_status_code()
{
// Arrange
await using var application = new C18WebApplication();
await application.SeedAsync<BasketContext>(async db =>
{
db.Items.RemoveRange(db.Items);
db.Items.Add(new(
CustomerId: 1,
ProductId: 1,
Quantity: 10
));
await db.SaveChangesAsync();
});
var client = application.CreateClient();

// Act
var response = await client.PostAsJsonAsync(
"/baskets",
new AddItem.Command(
CustomerId: 1,
ProductId: 1,
Quantity: 20
)
);

// Assert the response
Assert.NotNull(response);
Assert.False(response.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
var problem = await response.Content
.ReadFromJsonAsync<ProblemDetails>();
Assert.NotNull(problem);
Assert.Equal("The product \u00271\u0027 is already in your shopping cart.", problem.Title);

// Assert the database state
using var seedScope = application.Services.CreateScope();
var db = seedScope.ServiceProvider
.GetRequiredService<BasketContext>();
var dbItem = db.Items.FirstOrDefault(x => x.CustomerId == 1 && x.ProductId == 1);
Assert.NotNull(dbItem);
Assert.Equal(10, dbItem.Quantity);
}
}
}
62 changes: 62 additions & 0 deletions C18/REPR/Web.Tests/Features/Baskets/BasketsTest.FetchItemsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Net.Http.Json;
using static Web.Features.Baskets;

namespace Web.Features;
public partial class BasketsTest
{
public class FetchItemsTest
{
[Fact]
public async Task Should_return_the_specified_customer_items()
{
// Arrange
await using var application = new C18WebApplication();
await application.SeedAsync<BasketContext>(async (db) =>
{
db.Items.RemoveRange(db.Items.ToArray());
db.Items.Add(new BasketItem(2, 1, 5));
db.Items.Add(new BasketItem(2, 3, 15));
await db.SaveChangesAsync();
});
var client = application.CreateClient();

// Act
var response = await client
.GetFromJsonAsync<IEnumerable<FetchItems.Item>>("/baskets/2");

// Assert
Assert.NotNull(response);
Assert.Collection(response,
i => {
Assert.Equal(1, i.ProductId);
Assert.Equal(5, i.Quantity);
},
i => {
Assert.Equal(3, i.ProductId);
Assert.Equal(15, i.Quantity);
}
);
}

[Fact]
public async Task Should_return_an_empty_list_when_the_customer_have_no_item_in_its_cart()
{
// Arrange
await using var application = new C18WebApplication();
await application.SeedAsync<BasketContext>(async (db) =>
{
db.Items.RemoveRange(db.Items.ToArray());
await db.SaveChangesAsync();
});
var client = application.CreateClient();

// Act
var response = await client
.GetFromJsonAsync<IEnumerable<FetchItems.Item>>("/baskets/5");

// Assert
Assert.NotNull(response);
Assert.Empty(response);
}
}
}
72 changes: 72 additions & 0 deletions C18/REPR/Web.Tests/Features/Baskets/BasketsTest.RemoveItemTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using System.Net.Http.Json;
using static Web.Features.Baskets;

namespace Web.Features;
public partial class BasketsTest
{
public class RemoveItemTest
{
[Fact]
public async Task Should_remove_the_specified_item_from_the_customer_cart()
{
// Arrange
await using var application = new C18WebApplication();
await application.SeedAsync<BasketContext>(async db =>
{
db.Items.RemoveRange(db.Items.ToArray());
db.Items.Add(new BasketItem(1, 3, 30));
db.Items.Add(new BasketItem(2, 1, 5));
db.Items.Add(new BasketItem(2, 3, 15));
db.Items.Add(new BasketItem(3, 2, 18));
await db.SaveChangesAsync();
});
var client = application.CreateClient();

// Act
var response = await client.DeleteAsync("/baskets/2/1");

// Assert the response
Assert.NotNull(response);
Assert.True(response.IsSuccessStatusCode);
var result = await response.Content.ReadFromJsonAsync<RemoveItem.Response>();
Assert.NotNull(result);
Assert.Equal(5, result.Quantity);

// Assert the database state
using var seedScope = application.Services.CreateScope();
var db = seedScope.ServiceProvider.GetRequiredService<BasketContext>();
var dbItem = db.Items.FirstOrDefault(x => x.CustomerId == 2 && x.ProductId == 1);
Assert.Null(dbItem);
var remainingItems = db.Items.Count();
Assert.Equal(3, remainingItems);
}

[Fact]
public async Task Should_return_a_ProblemDetails_with_a_NotFound_status_code()
{
// Arrange
await using var application = new C18WebApplication();
var client = application.CreateClient();

// Act
var response = await client.DeleteAsync("/baskets/99/99");

// Assert the response
Assert.NotNull(response);
Assert.False(response.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
Assert.NotNull(problem);
Assert.Equal("The product \u002799\u0027 is not in your shopping cart.", problem.Title);

// Assert the database state
using var seedScope = application.Services.CreateScope();
var db = seedScope.ServiceProvider.GetRequiredService<BasketContext>();
var dbItem = db.Items.FirstOrDefault(x => x.CustomerId == 99);
Assert.Null(dbItem);
}
}
}
Loading