Skip to content

Commit 0421ecc

Browse files
committed
#73 Server side create/update bank implementation.
1 parent 74bde6e commit 0421ecc

File tree

9 files changed

+377
-0
lines changed

9 files changed

+377
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using AdminAssistant.DomainModel.Modules.AccountsModule;
2+
using AdminAssistant.Framework.TypeMapping;
3+
using Swashbuckle.AspNetCore.Annotations;
4+
5+
namespace AdminAssistant.WebAPI.v1.AccountsModule
6+
{
7+
[SwaggerSchema(Required = new[] { "BankName" })]
8+
public class BankCreateRequestDto : IMapTo<Bank>
9+
{
10+
[SwaggerSchema("The Bank identifier.", ReadOnly = true)]
11+
public int BankID { get; set; }
12+
public string BankName { get; set; } = string.Empty;
13+
14+
public void MapTo(AutoMapper.Profile profile)
15+
=> profile.CreateMap<BankCreateRequestDto, Bank>()
16+
.ForMember(x => x.BankID, opt => opt.Ignore());
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using AdminAssistant.DomainModel.Modules.AccountsModule;
2+
using AdminAssistant.Framework.TypeMapping;
3+
using Swashbuckle.AspNetCore.Annotations;
4+
5+
namespace AdminAssistant.WebAPI.v1.AccountsModule
6+
{
7+
[SwaggerSchema(Required = new[] { "BankID", "BankName" })]
8+
public class BankUpdateRequestDto : IMapTo<Bank>
9+
{
10+
[SwaggerSchema("The Bank identifier.", ReadOnly = true)]
11+
public int BankID { get; set; }
12+
public string BankName { get; set; } = string.Empty;
13+
14+
public void MapTo(AutoMapper.Profile profile)
15+
=> profile.CreateMap<BankUpdateRequestDto, Bank>()
16+
.ForMember(x => x.BankID, opt => opt.Ignore());
17+
}
18+
}

src/AdminAssistant.Blazor/Server/WebAPI/v1/AccountsModule/BankController.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
using System;
12
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Threading.Tasks;
5+
using AdminAssistant.DomainModel.Modules.AccountsModule;
36
using AdminAssistant.DomainModel.Modules.AccountsModule.CQRS;
47
using AdminAssistant.Infra.Providers;
58
using Ardalis.Result;
@@ -21,6 +24,55 @@ public BankController(IMapper mapper, IMediator mediator, ILoggingProvider loggi
2124
{
2225
}
2326

27+
[HttpPut]
28+
[SwaggerOperation("Update an existing Bank.", OperationId = "PutBank")]
29+
[SwaggerResponse(StatusCodes.Status200OK, "Ok - returns the updated BankResponseDto", type: typeof(BankResponseDto))]
30+
[SwaggerResponse(StatusCodes.Status404NotFound, "NotFound - When the BankID of the given bankUpdateRequest does not exist.")]
31+
[SwaggerResponse(StatusCodes.Status422UnprocessableEntity, "UnprocessableEntity - When the given bankUpdateRequest is invalid.")]
32+
public async Task<ActionResult<BankResponseDto>> BankPut([FromBody, SwaggerParameter("The Bank for which updates are to be persisted.", Required = true)] BankUpdateRequestDto bankUpdateRequest)
33+
{
34+
Log.Start();
35+
36+
var bank = Mapper.Map<Bank>(bankUpdateRequest);
37+
var result = await Mediator.Send(new BankUpdateCommand(bank)).ConfigureAwait(false);
38+
39+
if (result.Status == ResultStatus.NotFound)
40+
{
41+
result.ValidationErrors.ToList().ForEach((err) => ModelState.AddModelError(err.Identifier, err.ErrorMessage));
42+
return Log.Finish(NotFound(ModelState));
43+
}
44+
45+
if (result.Status == ResultStatus.Invalid)
46+
{
47+
result.ValidationErrors.ToList().ForEach((err) => ModelState.AddModelError(err.Identifier, err.ErrorMessage));
48+
return Log.Finish(UnprocessableEntity(ModelState));
49+
}
50+
51+
var response = Mapper.Map<BankResponseDto>(result.Value);
52+
return Log.Finish(Ok(response));
53+
}
54+
55+
[HttpPost]
56+
[SwaggerOperation("Creates a new Bank.", OperationId = "PostBank")]
57+
[SwaggerResponse(StatusCodes.Status201Created, "Created - returns the created bank with its assigned newly ID.", type: typeof(BankResponseDto))]
58+
[SwaggerResponse(StatusCodes.Status422UnprocessableEntity, "UnprocessableEntity - When the given bankCreateRequest is invalid.")]
59+
public async Task<ActionResult<BankResponseDto>> BankPost([FromBody, SwaggerParameter("The details of the Bank to be created.", Required = true)] BankCreateRequestDto bankCreateRequest)
60+
{
61+
Log.Start();
62+
63+
var bank = Mapper.Map<Bank>(bankCreateRequest);
64+
var result = await Mediator.Send(new BankCreateCommand(bank)).ConfigureAwait(false);
65+
66+
if (result.Status == ResultStatus.Invalid)
67+
{
68+
result.ValidationErrors.ToList().ForEach((err) => ModelState.AddModelError(err.Identifier, err.ErrorMessage));
69+
return Log.Finish(UnprocessableEntity(ModelState)); // https://stackoverflow.com/questions/47269601/what-http-response-code-to-use-for-failed-post-request
70+
}
71+
72+
var response = Mapper.Map<BankResponseDto>(result.Value);
73+
return Log.Finish(CreatedAtRoute(nameof(BankGetById), new { bankID = response.BankID }, response));
74+
}
75+
2476
[HttpGet("{bankID}")]
2577
[SwaggerOperation("Gets the Bank with the given ID.", OperationId = "GetBankById")]
2678
[SwaggerResponse(StatusCodes.Status200OK, "OK - returns the Bank requested.", type: typeof(BankResponseDto))]

src/AdminAssistant.Test/AutoMapper_WebAPIMappingProfile_UnitTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public void HaveValidConfiguration()
5252
[Theory]
5353
[Trait("Category", "Unit")]
5454
[InlineData(typeof(Bank), typeof(BankResponseDto))]
55+
[InlineData(typeof(BankCreateRequestDto), typeof(Bank))]
56+
[InlineData(typeof(BankUpdateRequestDto), typeof(Bank))]
5557
[InlineData(typeof(BankAccount), typeof(BankAccountResponseDto))]
5658
[InlineData(typeof(BankAccountInfo), typeof(BankAccountInfoResponseDto))]
5759
[InlineData(typeof(BankAccountType), typeof(BankAccountTypeResponseDto))]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#pragma warning disable CA1707 // Identifiers should not contain underscores
2+
using System.Threading.Tasks;
3+
using AdminAssistant.Infra.DAL.Modules.AccountsModule;
4+
using Ardalis.Result;
5+
using FluentAssertions;
6+
using MediatR;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Moq;
9+
using Xunit;
10+
using ObjectCloner.Extensions; // https://github.com/marcelltoth/ObjectCloner
11+
12+
namespace AdminAssistant.DomainModel.Modules.AccountsModule.CQRS
13+
{
14+
public class BankCreateCommand_Should
15+
{
16+
[Fact]
17+
[Trait("Category", "Unit")]
18+
public async Task Return_APersistedBank_GivenAValidBank()
19+
{
20+
// Arrange
21+
var bank = Factory.Bank.WithTestData().Build();
22+
23+
var services = new ServiceCollection();
24+
services.AddMockServerSideLogging();
25+
services.AddAdminAssistantServerSideDomainModel();
26+
27+
var mockBankRepository = new Mock<IBankRepository>();
28+
mockBankRepository.Setup(x => x.SaveAsync(bank))
29+
.Returns(() =>
30+
{
31+
var result = bank.DeepClone();
32+
result = result with { BankID = 30 };
33+
return Task.FromResult(result);
34+
});
35+
36+
services.AddTransient((sp) => mockBankRepository.Object);
37+
38+
// Act
39+
var result = await services.BuildServiceProvider().GetRequiredService<IMediator>().Send(new BankCreateCommand(bank)).ConfigureAwait(false);
40+
41+
// Assert
42+
result.Should().NotBeNull();
43+
result.Status.Should().Be(ResultStatus.Ok);
44+
result.ValidationErrors.Should().BeEmpty();
45+
result.Value.Should().NotBeNull();
46+
result.Value.BankID.Should().BeGreaterThan(Constants.NewRecordID);
47+
}
48+
49+
[Fact]
50+
[Trait("Category", "Unit")]
51+
public async Task Return_ValidationError_GivenAnInvalidBank()
52+
{
53+
// Arrange
54+
var services = new ServiceCollection();
55+
services.AddMockServerSideLogging();
56+
services.AddAdminAssistantServerSideDomainModel();
57+
services.AddTransient((sp) => new Mock<IBankRepository>().Object);
58+
59+
var bank = Factory.Bank.WithTestData()
60+
.WithBankName(string.Empty)
61+
.Build();
62+
// Act
63+
var result = await services.BuildServiceProvider().GetRequiredService<IMediator>().Send(new BankCreateCommand(bank)).ConfigureAwait(false);
64+
65+
// Assert
66+
result.Should().NotBeNull();
67+
result.Status.Should().Be(ResultStatus.Invalid);
68+
result.ValidationErrors.Should().NotBeEmpty();
69+
}
70+
}
71+
}
72+
#pragma warning restore CA1707 // Identifiers should not contain underscores
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#pragma warning disable CA1707 // Identifiers should not contain underscores
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using AdminAssistant.Infra.DAL.Modules.AccountsModule;
5+
using Ardalis.Result;
6+
using FluentAssertions;
7+
using MediatR;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Moq;
10+
using ObjectCloner.Extensions;
11+
using Xunit;
12+
13+
namespace AdminAssistant.DomainModel.Modules.AccountsModule.CQRS
14+
{
15+
public class BankUpdateCommand_Should
16+
{
17+
[Fact]
18+
[Trait("Category", "Unit")]
19+
public async Task SaveAndReturn_APersistedBank_GivenAValidBank()
20+
{
21+
// Arrange
22+
var bank = Factory.Bank.WithTestData(10).Build();
23+
24+
var services = new ServiceCollection();
25+
services.AddMockServerSideLogging();
26+
services.AddAdminAssistantServerSideDomainModel();
27+
28+
var mockBankRepository = new Mock<IBankRepository>();
29+
mockBankRepository.Setup(x => x.SaveAsync(bank))
30+
.Returns(Task.FromResult(bank));
31+
32+
services.AddTransient((sp) => mockBankRepository.Object);
33+
34+
// Act
35+
var result = await services.BuildServiceProvider().GetRequiredService<IMediator>().Send(new BankUpdateCommand(bank)).ConfigureAwait(false);
36+
37+
// Assert
38+
result.Should().NotBeNull();
39+
result.Status.Should().Be(ResultStatus.Ok);
40+
result.ValidationErrors.Should().BeEmpty();
41+
result.Value.Should().NotBeNull();
42+
result.Value.BankID.Should().BeGreaterThan(Constants.NewRecordID);
43+
}
44+
45+
[Fact]
46+
[Trait("Category", "Unit")]
47+
public async Task Return_ValidationError_GivenAnInvalidBank()
48+
{
49+
// Arrange
50+
var services = new ServiceCollection();
51+
services.AddMockServerSideLogging();
52+
services.AddAdminAssistantServerSideDomainModel();
53+
services.AddTransient((sp) => new Mock<IBankRepository>().Object);
54+
55+
var bank = Factory.Bank.WithTestData()
56+
.WithBankName(string.Empty)
57+
.Build();
58+
// Act
59+
var result = await services.BuildServiceProvider().GetRequiredService<IMediator>().Send(new BankUpdateCommand(bank)).ConfigureAwait(false);
60+
61+
// Assert
62+
result.Should().NotBeNull();
63+
result.Status.Should().Be(ResultStatus.Invalid);
64+
result.ValidationErrors.Should().NotBeEmpty();
65+
}
66+
// TODO: Add test for BankUpdateCommand where BankID not in IBankRepository
67+
}
68+
}
69+
#pragma warning restore CA1707 // Identifiers should not contain underscores

src/AdminAssistant.Test/WebAPI/v1/AccountsModule/BankController_UnitTest.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,58 @@
1717

1818
namespace AdminAssistant.WebAPI.v1.AccountsModule
1919
{
20+
public class BankController_Put_Should
21+
{
22+
// TODO: BankController_Put UnitTests
23+
}
24+
25+
public class BankController_BankPost_Should
26+
{
27+
[Fact]
28+
[Trait("Category", "Unit")]
29+
public async Task Return_Status422UnprocessableEntity_Given_AnInvalidBank()
30+
{
31+
// Arrange
32+
var validationErrors = new List<ValidationError>()
33+
{
34+
new ValidationError() { Identifier="ExampleErrorCode", ErrorMessage="ExampleErrorMessage", Severity=ValidationSeverity.Error },
35+
new ValidationError() { Identifier="ExampleErrorCode2", ErrorMessage="ExampleErrorMessage2", Severity=ValidationSeverity.Error }
36+
};
37+
var bank = Factory.Bank.WithTestData(10).Build();
38+
39+
var services = new ServiceCollection();
40+
services.AddMocksOfExternalServerSideDependencies();
41+
42+
var mockMediator = new Mock<IMediator>();
43+
mockMediator.Setup(x => x.Send(It.IsAny<BankCreateCommand>(), It.IsAny<CancellationToken>()))
44+
.Returns(Task.FromResult(Result<Bank>.Invalid(validationErrors)));
45+
46+
services.AddTransient((sp) => mockMediator.Object);
47+
services.AddTransient<BankController>();
48+
49+
var container = services.BuildServiceProvider();
50+
51+
var mapper = container.GetRequiredService<IMapper>();
52+
var bankRequest = mapper.Map<BankCreateRequestDto>(bank);
53+
54+
// Act
55+
var response = await container.GetRequiredService<BankController>().BankPost(bankRequest).ConfigureAwait(false);
56+
57+
// Assert
58+
response.Result.Should().BeOfType<UnprocessableEntityObjectResult>();
59+
response.Value.Should().BeNull();
60+
61+
var result = (UnprocessableEntityObjectResult)response.Result;
62+
var errors = (SerializableError)result.Value;
63+
64+
foreach (var expectedErrorDetails in validationErrors)
65+
{
66+
var messages = (string[])errors[expectedErrorDetails.Identifier];
67+
messages.Should().Contain(expectedErrorDetails.ErrorMessage);
68+
}
69+
}
70+
}
71+
2072
public class BankController_BankGetById_Should
2173
{
2274
[Fact]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using AdminAssistant.Infra.DAL.Modules.AccountsModule;
6+
using AdminAssistant.DomainModel.Modules.AccountsModule.Validation;
7+
using AdminAssistant.Infra.Providers;
8+
using Ardalis.Result;
9+
using MediatR;
10+
using Ardalis.Result.FluentValidation;
11+
12+
namespace AdminAssistant.DomainModel.Modules.AccountsModule.CQRS
13+
{
14+
public class BankCreateCommand : IRequest<Result<Bank>>
15+
{
16+
public BankCreateCommand(Bank bank) => Bank = bank;
17+
18+
public Bank Bank { get; private set; }
19+
20+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Build", "CA1812", Justification = "Compiler dosen't understand dependency injection")]
21+
internal class BankCreateHandler : RequestHandlerBase<BankCreateCommand, Result<Bank>>
22+
{
23+
private readonly IBankRepository bankRepository;
24+
private readonly IBankValidator bankValidator;
25+
26+
public BankCreateHandler(ILoggingProvider loggingProvider, IBankRepository bankRepository, IBankValidator bankValidator)
27+
: base(loggingProvider)
28+
{
29+
this.bankRepository = bankRepository;
30+
this.bankValidator = bankValidator;
31+
}
32+
33+
public override async Task<Result<Bank>> Handle(BankCreateCommand command, CancellationToken cancellationToken)
34+
{
35+
var validationResult = await bankValidator.ValidateAsync(command.Bank, cancellationToken).ConfigureAwait(false);
36+
37+
if (validationResult.IsValid == false)
38+
{
39+
return Result<Bank>.Invalid(validationResult.AsErrors());
40+
}
41+
42+
var result = await bankRepository.SaveAsync(command.Bank).ConfigureAwait(false);
43+
return Result<Bank>.Success(result);
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)