diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj
index 66b0cf6..3ffe10b 100644
--- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj
+++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj
@@ -1,21 +1,28 @@
-
+
net7.0
enable
+ 64377c40-44d6-4989-9662-5d778f8b3b92
-
-
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/CleanArchitecture.Api/Controllers/ApiController.cs b/CleanArchitecture.Api/Controllers/ApiController.cs
index ddb8cd0..c092d04 100644
--- a/CleanArchitecture.Api/Controllers/ApiController.cs
+++ b/CleanArchitecture.Api/Controllers/ApiController.cs
@@ -63,6 +63,11 @@ public class ApiController : ControllerBase
return HttpStatusCode.NotFound;
}
+ if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.InsufficientPermissions))
+ {
+ return HttpStatusCode.Forbidden;
+ }
+
return HttpStatusCode.BadRequest;
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs
index 5d6cfc6..d3986dd 100644
--- a/CleanArchitecture.Api/Controllers/UserController.cs
+++ b/CleanArchitecture.Api/Controllers/UserController.cs
@@ -1,15 +1,19 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
+using CleanArchitecture.Api.Models;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Notifications;
using MediatR;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Annotations;
namespace CleanArchitecture.Api.Controllers;
[ApiController]
-[Route("[controller]")]
+[Route("/api/v1/[controller]")]
public class UserController : ApiController
{
private readonly IUserService _userService;
@@ -21,14 +25,20 @@ public class UserController : ApiController
_userService = userService;
}
+ [Authorize]
[HttpGet]
+ [SwaggerOperation("Get a list of all users")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))]
public async Task GetAllUsersAsync()
{
var users = await _userService.GetAllUsersAsync();
return Response(users);
}
-
- [HttpGet("{id}")]
+
+ [Authorize]
+ [HttpGet("{id:guid}")]
+ [SwaggerOperation("Get a user by id")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
public async Task GetUserByIdAsync(
[FromRoute] Guid id,
[FromQuery] bool isDeleted = false)
@@ -36,25 +46,62 @@ public class UserController : ApiController
var user = await _userService.GetUserByUserIdAsync(id, isDeleted);
return Response(user);
}
-
+
+ [Authorize]
+ [HttpGet("me")]
+ [SwaggerOperation("Get the current active user")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
+ public async Task GetCurrentUserAsync()
+ {
+ var user = await _userService.GetCurrentUserAsync();
+ return Response(user);
+ }
+
[HttpPost]
+ [SwaggerOperation("Create a new user")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel)
{
var userId = await _userService.CreateUserAsync(viewModel);
return Response(userId);
}
-
- [HttpDelete("{id}")]
+
+ [Authorize]
+ [HttpDelete("{id:guid}")]
+ [SwaggerOperation("Delete a user")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
public async Task DeleteUserAsync([FromRoute] Guid id)
{
await _userService.DeleteUserAsync(id);
return Response(id);
}
-
+
+ [Authorize]
[HttpPut]
+ [SwaggerOperation("Update a user")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
public async Task UpdateUserAsync([FromBody] UpdateUserViewModel viewModel)
{
await _userService.UpdateUserAsync(viewModel);
return Response(viewModel);
}
+
+ [Authorize]
+ [HttpPost("changePassword")]
+ [SwaggerOperation("Change a password for the current active user")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
+ public async Task ChangePasswordAsync([FromBody] ChangePasswordViewModel viewModel)
+ {
+ await _userService.ChangePasswordAsync(viewModel);
+ return Response(viewModel);
+ }
+
+ [HttpPost("login")]
+ [SwaggerOperation("Get a signed token for a user")]
+ [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))]
+ public async Task LoginUserAsync([FromBody] LoginUserViewModel viewModel)
+ {
+ var token = await _userService.LoginUserAsync(viewModel);
+ return Response(token);
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Api/Dockerfile b/CleanArchitecture.Api/Dockerfile
deleted file mode 100644
index eaa2fcf..0000000
--- a/CleanArchitecture.Api/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
-
-FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
-WORKDIR /app
-EXPOSE 80
-EXPOSE 443
-
-FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
-WORKDIR /src
-COPY ["CleanArchitecture.Api/CleanArchitecture.Api.csproj", "CleanArchitecture.Api/"]
-COPY ["CleanArchitecture.Application/CleanArchitecture.Application.csproj", "CleanArchitecture.Application/"]
-COPY ["CleanArchitecture.Domain/CleanArchitecture.Domain.csproj", "CleanArchitecture.Domain/"]
-COPY ["CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj", "CleanArchitecture.Infrastructure/"]
-RUN dotnet restore "CleanArchitecture.Api/CleanArchitecture.Api.csproj"
-COPY . .
-WORKDIR "/src/CleanArchitecture.Api"
-RUN dotnet build "CleanArchitecture.Api.csproj" -c Release -o /app/build
-
-FROM build AS publish
-RUN dotnet publish "CleanArchitecture.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "CleanArchitecture.Api.dll"]
\ No newline at end of file
diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs
index 4aee2bc..b828705 100644
--- a/CleanArchitecture.Api/Program.cs
+++ b/CleanArchitecture.Api/Program.cs
@@ -1,20 +1,66 @@
+using System.Collections.Generic;
+using System.Text;
using CleanArchitecture.Application.Extensions;
using CleanArchitecture.Domain.Extensions;
+using CleanArchitecture.Domain.Settings;
using CleanArchitecture.gRPC;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddGrpc();
builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen();
+builder.Services.AddSwaggerGen(c =>
+{
+ c.EnableAnnotations();
+ c.SwaggerDoc("v1", new OpenApiInfo
+ {
+ Title = "CleanArchitecture",
+ Version = "v1",
+ Description = "A clean architecture API"
+ });
+
+ c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
+ {
+ Description = "JWT Authorization header using the Bearer scheme. " +
+ "Use the /api/v1/user/login endpoint to generate a token",
+ Name = "Authorization",
+ In = ParameterLocation.Header,
+ Type = SecuritySchemeType.Http,
+ Scheme = "bearer"
+ });
+
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "Bearer"
+ },
+ Scheme = "oauth2",
+ Name = "Bearer",
+ In = ParameterLocation.Header
+ },
+ new List()
+ }
+ });
+});
+
+builder.Services.AddHealthChecks();
builder.Services.AddHttpContextAccessor();
builder.Services.AddDbContext(options =>
@@ -24,38 +70,72 @@ builder.Services.AddDbContext(options =>
b => b.MigrationsAssembly("CleanArchitecture.Infrastructure"));
});
+builder.Services.AddAuthentication(
+ options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; })
+ .AddJwtBearer(
+ jwtOptions => { jwtOptions.TokenValidationParameters = CreateTokenValidationParameters(); });
+
builder.Services.AddInfrastructure();
builder.Services.AddQueryHandlers();
builder.Services.AddServices();
builder.Services.AddCommandHandlers();
builder.Services.AddNotificationHandlers();
+builder.Services.AddApiUser();
-builder.Services.AddMediatR(cfg =>
-{
- cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly);
-});
+builder.Services
+ .AddOptions()
+ .Bind(builder.Configuration.GetSection("Auth"))
+ .ValidateOnStart();
+
+builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); });
var app = builder.Build();
-app.UseSwagger();
-app.UseSwaggerUI();
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
app.UseHttpsRedirection();
+app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
+app.MapHealthChecks("/health");
app.MapGrpcService();
-using (IServiceScope scope = app.Services.CreateScope())
+using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
- ApplicationDbContext appDbContext = services.GetRequiredService();
+ var appDbContext = services.GetRequiredService();
appDbContext.EnsureMigrationsApplied();
}
app.Run();
-// Needed for integration tests webapplication factory
-public partial class Program { }
\ No newline at end of file
+TokenValidationParameters CreateTokenValidationParameters()
+{
+ var result = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = builder.Configuration["Auth:Issuer"],
+ ValidAudience = builder.Configuration["Auth:Audience"],
+ IssuerSigningKey = new SymmetricSecurityKey(
+ Encoding.UTF8.GetBytes(
+ builder.Configuration["Auth:Secret"]!)),
+ RequireSignedTokens = false
+ };
+
+ return result;
+}
+
+// Needed for integration tests web application factory
+public partial class Program
+{
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Api/appsettings.Development.json b/CleanArchitecture.Api/appsettings.Development.json
index 0c208ae..c183fbb 100644
--- a/CleanArchitecture.Api/appsettings.Development.json
+++ b/CleanArchitecture.Api/appsettings.Development.json
@@ -4,5 +4,13 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
+ },
+ "Auth": {
+ "Issuer": "CleanArchitectureServer",
+ "Audience": "CleanArchitectureClient",
+ "Secret": "sD3v061gf8BxXgmxcHss"
}
}
diff --git a/CleanArchitecture.Api/appsettings.json b/CleanArchitecture.Api/appsettings.json
index 26b0156..c481317 100644
--- a/CleanArchitecture.Api/appsettings.json
+++ b/CleanArchitecture.Api/appsettings.json
@@ -8,5 +8,10 @@
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
+ },
+ "Auth": {
+ "Issuer": "CleanArchitectureServer",
+ "Audience": "CleanArchitectureClient",
+ "Secret": "sD3v061gf8BxXgmxcHss"
}
}
diff --git a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj
index 082265f..9af11da 100644
--- a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj
+++ b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj
@@ -8,11 +8,11 @@
-
-
-
-
-
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs
index 204e29c..2d995f8 100644
--- a/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs
+++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs
@@ -7,7 +7,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries;
public class QueryHandlerBaseFixture
{
public Mock Bus { get; } = new();
-
+
public QueryHandlerBaseFixture VerifyExistingNotification(string key, string errorCode, string message)
{
Bus.Verify(
diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs
index 9e22915..9dde37d 100644
--- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs
+++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Domain.Entities;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MockQueryable.Moq;
using Moq;
@@ -10,28 +11,30 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
{
+ public GetAllUsersTestFixture()
+ {
+ UserRepository = new Mock();
+
+ Handler = new GetAllUsersQueryHandler(UserRepository.Object);
+ }
+
private Mock UserRepository { get; }
public GetAllUsersQueryHandler Handler { get; }
public Guid ExistingUserId { get; } = Guid.NewGuid();
- public GetAllUsersTestFixture()
- {
- UserRepository = new();
-
- Handler = new(UserRepository.Object);
- }
-
public void SetupUserAsync()
{
var user = new Mock(() =>
- new User(
- ExistingUserId,
- "max@mustermann.com",
- "Max",
- "Mustermann"));
+ new User(
+ ExistingUserId,
+ "max@mustermann.com",
+ "Max",
+ "Mustermann",
+ "Password",
+ UserRole.User));
var query = new[] { user.Object }.AsQueryable().BuildMock();
-
+
UserRepository
.Setup(x => x.GetAllNoTracking())
.Returns(query);
@@ -40,11 +43,13 @@ public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
public void SetupDeletedUserAsync()
{
var user = new Mock(() =>
- new User(
- ExistingUserId,
- "max@mustermann.com",
- "Max",
- "Mustermann"));
+ new User(
+ ExistingUserId,
+ "max@mustermann.com",
+ "Max",
+ "Mustermann",
+ "Password",
+ UserRole.User));
user.Object.Delete();
diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs
index 404f699..e175faa 100644
--- a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs
+++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Domain.Entities;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MockQueryable.Moq;
using Moq;
@@ -10,28 +11,30 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
{
+ public GetUserByIdTestFixture()
+ {
+ UserRepository = new Mock();
+
+ Handler = new GetUserByIdQueryHandler(UserRepository.Object, Bus.Object);
+ }
+
private Mock UserRepository { get; }
public GetUserByIdQueryHandler Handler { get; }
public Guid ExistingUserId { get; } = Guid.NewGuid();
- public GetUserByIdTestFixture()
- {
- UserRepository = new();
-
- Handler = new(UserRepository.Object, Bus.Object);
- }
-
public void SetupUserAsync()
{
var user = new Mock(() =>
new User(
- ExistingUserId,
- "max@mustermann.com",
- "Max",
- "Mustermann"));
+ ExistingUserId,
+ "max@mustermann.com",
+ "Max",
+ "Mustermann",
+ "Password",
+ UserRole.User));
var query = new[] { user.Object }.AsQueryable().BuildMock();
-
+
UserRepository
.Setup(x => x.GetAllNoTracking())
.Returns(query);
@@ -44,7 +47,9 @@ public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
ExistingUserId,
"max@mustermann.com",
"Max",
- "Mustermann"));
+ "Mustermann",
+ "Password",
+ UserRole.User));
user.Object.Delete();
diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs
index f1ec70e..816c374 100644
--- a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs
+++ b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
+using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
using FluentAssertions;
using Xunit;
@@ -16,14 +17,15 @@ public sealed class GetAllUsersQueryHandlerTests
_fixture.SetupUserAsync();
var result = await _fixture.Handler.Handle(
- new(),
+ new GetAllUsersQuery(),
default);
-
+
_fixture.VerifyNoDomainNotification();
-
- result.Should().NotBeNull();
- result.Should().ContainSingle();
- result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId);
+
+ var userViewModels = result.ToArray();
+ userViewModels.Should().NotBeNull();
+ userViewModels.Should().ContainSingle();
+ userViewModels.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId);
}
[Fact]
@@ -32,7 +34,7 @@ public sealed class GetAllUsersQueryHandlerTests
_fixture.SetupDeletedUserAsync();
var result = await _fixture.Handler.Handle(
- new(),
+ new GetAllUsersQuery(),
default);
_fixture.VerifyNoDomainNotification();
diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs
index a65da4c..4b3aec0 100644
--- a/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs
+++ b/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs
@@ -18,15 +18,15 @@ public sealed class GetUserByIdQueryHandlerTests
_fixture.SetupUserAsync();
var result = await _fixture.Handler.Handle(
- new(_fixture.ExistingUserId, false),
+ new GetUserByIdQuery(_fixture.ExistingUserId, false),
default);
-
+
_fixture.VerifyNoDomainNotification();
result.Should().NotBeNull();
result!.Id.Should().Be(_fixture.ExistingUserId);
}
-
+
[Fact]
public async Task Should_Raise_Notification_For_No_User()
{
@@ -36,7 +36,7 @@ public sealed class GetUserByIdQueryHandlerTests
var result = await _fixture.Handler.Handle(
request,
default);
-
+
_fixture.VerifyExistingNotification(
nameof(GetUserByIdQuery),
ErrorCodes.ObjectNotFound,
@@ -51,7 +51,7 @@ public sealed class GetUserByIdQueryHandlerTests
_fixture.SetupDeletedUserAsync();
var result = await _fixture.Handler.Handle(
- new(_fixture.ExistingUserId, false),
+ new GetUserByIdQuery(_fixture.ExistingUserId, false),
default);
_fixture.VerifyExistingNotification(
diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj
index 2f39ab9..2b5f8ef 100644
--- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj
+++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj
@@ -6,11 +6,11 @@
-
+
-
+
diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs
index beaec9f..784380f 100644
--- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs
+++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs
@@ -14,15 +14,15 @@ public static class ServiceCollectionExtension
public static IServiceCollection AddServices(this IServiceCollection services)
{
services.AddScoped();
-
+
return services;
}
-
+
public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
{
services.AddScoped, GetUserByIdQueryHandler>();
services.AddScoped>, GetAllUsersQueryHandler>();
-
+
return services;
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs
index 5f65254..1fbca3e 100644
--- a/CleanArchitecture.Application/Interfaces/IUserService.cs
+++ b/CleanArchitecture.Application/Interfaces/IUserService.cs
@@ -1,6 +1,6 @@
-using System.Threading.Tasks;
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels.Users;
namespace CleanArchitecture.Application.Interfaces;
@@ -8,8 +8,11 @@ namespace CleanArchitecture.Application.Interfaces;
public interface IUserService
{
public Task GetUserByUserIdAsync(Guid userId, bool isDeleted);
+ public Task GetCurrentUserAsync();
public Task> GetAllUsersAsync();
public Task CreateUserAsync(CreateUserViewModel user);
public Task UpdateUserAsync(UpdateUserViewModel user);
public Task DeleteUserAsync(Guid userId);
+ public Task ChangePasswordAsync(ChangePasswordViewModel viewModel);
+ public Task LoginUserAsync(LoginUserViewModel viewModel);
}
\ No newline at end of file
diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs
index 40e8571..9e59d7f 100644
--- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs
+++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs
@@ -4,4 +4,4 @@ using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetAll;
-public sealed record GetAllUsersQuery : IRequest>;
+public sealed record GetAllUsersQuery : IRequest>;
\ No newline at end of file
diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs
index e394336..b4edf1e 100644
--- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs
+++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs
@@ -4,4 +4,4 @@ using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetUserById;
-public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest;
+public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest;
\ No newline at end of file
diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs
index 2c166a8..f057731 100644
--- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs
+++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs
@@ -13,8 +13,8 @@ namespace CleanArchitecture.Application.Queries.Users.GetUserById;
public sealed class GetUserByIdQueryHandler :
IRequestHandler
{
- private readonly IUserRepository _userRepository;
private readonly IMediatorHandler _bus;
+ private readonly IUserRepository _userRepository;
public GetUserByIdQueryHandler(IUserRepository userRepository, IMediatorHandler bus)
{
@@ -26,7 +26,7 @@ public sealed class GetUserByIdQueryHandler :
{
var user = _userRepository
.GetAllNoTracking()
- .FirstOrDefault(x =>
+ .FirstOrDefault(x =>
x.Id == request.UserId &&
x.Deleted == request.IsDeleted);
@@ -42,4 +42,4 @@ public sealed class GetUserByIdQueryHandler :
return UserViewModel.FromUser(user);
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs
index dd91d4f..da20411 100644
--- a/CleanArchitecture.Application/Services/UserService.cs
+++ b/CleanArchitecture.Application/Services/UserService.cs
@@ -5,8 +5,10 @@ using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.ViewModels.Users;
+using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
+using CleanArchitecture.Domain.Commands.Users.LoginUser;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.Interfaces;
@@ -15,10 +17,12 @@ namespace CleanArchitecture.Application.Services;
public sealed class UserService : IUserService
{
private readonly IMediatorHandler _bus;
+ private readonly IUser _user;
- public UserService(IMediatorHandler bus)
+ public UserService(IMediatorHandler bus, IUser user)
{
_bus = bus;
+ _user = user;
}
public async Task GetUserByUserIdAsync(Guid userId, bool isDeleted)
@@ -26,11 +30,16 @@ public sealed class UserService : IUserService
return await _bus.QueryAsync(new GetUserByIdQuery(userId, isDeleted));
}
+ public async Task GetCurrentUserAsync()
+ {
+ return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false));
+ }
+
public async Task> GetAllUsersAsync()
{
return await _bus.QueryAsync(new GetAllUsersQuery());
}
-
+
public async Task CreateUserAsync(CreateUserViewModel user)
{
var userId = Guid.NewGuid();
@@ -39,22 +48,34 @@ public sealed class UserService : IUserService
userId,
user.Email,
user.Surname,
- user.GivenName));
+ user.GivenName,
+ user.Password));
return userId;
}
-
+
public async Task UpdateUserAsync(UpdateUserViewModel user)
{
await _bus.SendCommandAsync(new UpdateUserCommand(
user.Id,
user.Email,
user.Surname,
- user.GivenName));
+ user.GivenName,
+ user.Role));
}
-
+
public async Task DeleteUserAsync(Guid userId)
{
await _bus.SendCommandAsync(new DeleteUserCommand(userId));
}
+
+ public async Task ChangePasswordAsync(ChangePasswordViewModel viewModel)
+ {
+ await _bus.SendCommandAsync(new ChangePasswordCommand(viewModel.Password, viewModel.NewPassword));
+ }
+
+ public async Task LoginUserAsync(LoginUserViewModel viewModel)
+ {
+ return await _bus.QueryAsync(new LoginUserCommand(viewModel.Email, viewModel.Password));
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs b/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs
new file mode 100644
index 0000000..bc35cbb
--- /dev/null
+++ b/CleanArchitecture.Application/ViewModels/Users/ChangePasswordViewModel.cs
@@ -0,0 +1,3 @@
+namespace CleanArchitecture.Application.ViewModels.Users;
+
+public sealed record ChangePasswordViewModel(string Password, string NewPassword);
\ No newline at end of file
diff --git a/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs b/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs
new file mode 100644
index 0000000..ac5e7de
--- /dev/null
+++ b/CleanArchitecture.Application/ViewModels/Users/LoginUserViewModel.cs
@@ -0,0 +1,3 @@
+namespace CleanArchitecture.Application.ViewModels.Users;
+
+public sealed record LoginUserViewModel(string Email, string Password);
\ No newline at end of file
diff --git a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs
index c1a01d4..3efbb36 100644
--- a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs
+++ b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs
@@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users;
public sealed record CreateUserViewModel(
string Email,
string Surname,
- string GivenName);
\ No newline at end of file
+ string GivenName,
+ string Password);
\ No newline at end of file
diff --git a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs
index e64fd7d..137d30f 100644
--- a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs
+++ b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Application.ViewModels.Users;
@@ -6,4 +7,5 @@ public sealed record UpdateUserViewModel(
Guid Id,
string Email,
string Surname,
- string GivenName);
\ No newline at end of file
+ string GivenName,
+ UserRole Role);
\ No newline at end of file
diff --git a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs
index 9ecc82f..53f2aa9 100644
--- a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs
+++ b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs
@@ -1,5 +1,6 @@
using System;
using CleanArchitecture.Domain.Entities;
+using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Application.ViewModels.Users;
@@ -9,6 +10,7 @@ public sealed class UserViewModel
public string Email { get; set; } = string.Empty;
public string GivenName { get; set; } = string.Empty;
public string Surname { get; set; } = string.Empty;
+ public UserRole Role { get; set; }
public static UserViewModel FromUser(User user)
{
@@ -17,7 +19,8 @@ public sealed class UserViewModel
Id = user.Id,
Email = user.Email,
GivenName = user.GivenName,
- Surname = user.Surname
+ Surname = user.Surname,
+ Role = user.Role
};
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj
index 6cc4fcb..dfa6f77 100644
--- a/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj
+++ b/CleanArchitecture.Domain.Tests/CleanArchitecture.Domain.Tests.csproj
@@ -8,10 +8,11 @@
-
-
-
-
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -23,7 +24,7 @@
-
+
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs
new file mode 100644
index 0000000..08d16ca
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs
@@ -0,0 +1,63 @@
+using System.Threading.Tasks;
+using CleanArchitecture.Domain.Commands.Users.ChangePassword;
+using CleanArchitecture.Domain.Errors;
+using CleanArchitecture.Domain.Events.User;
+using Xunit;
+
+namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
+
+public sealed class ChangePasswordCommandHandlerTests
+{
+ private readonly ChangePasswordCommandTestFixture _fixture = new();
+
+ [Fact]
+ public async Task Should_Change_Password()
+ {
+ var user = _fixture.SetupUser();
+
+ var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQm", "z8]tnayvd5FNLU9:]AQw");
+
+ await _fixture.CommandHandler.Handle(command, default);
+
+ _fixture
+ .VerifyNoDomainNotification()
+ .VerifyCommit()
+ .VerifyRaisedEvent(x => x.UserId == user.Id);
+ }
+
+ [Fact]
+ public async Task Should_Not_Change_Password_No_User()
+ {
+ var userId = _fixture.SetupMissingUser();
+
+ var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQm", "z8]tnayvd5FNLU9:]AQw");
+
+ await _fixture.CommandHandler.Handle(command, default);
+
+ _fixture
+ .VerifyNoCommit()
+ .VerifyNoRaisedEvent()
+ .VerifyAnyDomainNotification()
+ .VerifyExistingNotification(
+ ErrorCodes.ObjectNotFound,
+ $"There is no User with Id {userId}");
+ }
+
+ [Fact]
+ public async Task Should_Not_Change_Password_Incorrect_Password()
+ {
+ _fixture.SetupUser();
+
+ var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQw", "z8]tnayvd5FNLU9:]AQx");
+
+ await _fixture.CommandHandler.Handle(command, default);
+
+ _fixture
+ .VerifyNoCommit()
+ .VerifyNoRaisedEvent()
+ .VerifyAnyDomainNotification()
+ .VerifyExistingNotification(
+ DomainErrorCodes.UserPasswordIncorrect,
+ "The password is incorrect");
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs
new file mode 100644
index 0000000..74f24c7
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandTestFixture.cs
@@ -0,0 +1,52 @@
+using System;
+using CleanArchitecture.Domain.Commands.Users.ChangePassword;
+using CleanArchitecture.Domain.Enums;
+using CleanArchitecture.Domain.Interfaces.Repositories;
+using Moq;
+using BC = BCrypt.Net.BCrypt;
+
+namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
+
+public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase
+{
+ public ChangePasswordCommandTestFixture()
+ {
+ UserRepository = new Mock();
+
+ CommandHandler = new ChangePasswordCommandHandler(
+ Bus.Object,
+ UnitOfWork.Object,
+ NotificationHandler.Object,
+ UserRepository.Object,
+ User.Object);
+ }
+
+ public ChangePasswordCommandHandler CommandHandler { get; }
+ private Mock UserRepository { get; }
+
+ public Entities.User SetupUser()
+ {
+ var user = new Entities.User(
+ Guid.NewGuid(),
+ "max@mustermann.com",
+ "Max",
+ "Mustermann",
+ BC.HashPassword("z8]tnayvd5FNLU9:]AQm"),
+ UserRole.User);
+
+ User.Setup(x => x.GetUserId()).Returns(user.Id);
+
+ UserRepository
+ .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id)))
+ .ReturnsAsync(user);
+
+ return user;
+ }
+
+ public Guid SetupMissingUser()
+ {
+ var id = Guid.NewGuid();
+ User.Setup(x => x.GetUserId()).Returns(id);
+ return id;
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs
new file mode 100644
index 0000000..39c8186
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using System.Linq;
+using CleanArchitecture.Domain.Commands.Users.ChangePassword;
+using CleanArchitecture.Domain.Errors;
+using Xunit;
+
+namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
+
+public sealed class ChangePasswordCommandValidationTests :
+ ValidationTestBase
+{
+ public ChangePasswordCommandValidationTests() : base(new ChangePasswordCommandValidation())
+ {
+ }
+
+ [Fact]
+ public void Should_Be_Valid()
+ {
+ var command = CreateTestCommand();
+
+ ShouldBeValid(command);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Empty_Password()
+ {
+ var command = CreateTestCommand("");
+
+ var errors = new List
+ {
+ DomainErrorCodes.UserEmptyPassword,
+ DomainErrorCodes.UserSpecialCharPassword,
+ DomainErrorCodes.UserNumberPassword,
+ DomainErrorCodes.UserLowercaseLetterPassword,
+ DomainErrorCodes.UserUppercaseLetterPassword,
+ DomainErrorCodes.UserShortPassword
+ };
+
+ ShouldHaveExpectedErrors(command, errors.ToArray());
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Special_Character()
+ {
+ var command = CreateTestCommand("z8tnayvd5FNLU9AQm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Number()
+ {
+ var command = CreateTestCommand("z]tnayvdFNLU:]AQm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Lowercase_Character()
+ {
+ var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Uppercase_Character()
+ {
+ var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Password_Too_Short()
+ {
+ var command = CreateTestCommand("zA6{");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Password_Too_Long()
+ {
+ var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12));
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
+ }
+
+ private static ChangePasswordCommand CreateTestCommand(
+ string? password = null, string? newPassword = null)
+ {
+ return new(
+ password ?? "z8]tnayvd5FNLU9:]AQm",
+ newPassword ?? "z8]tnayvd5FNLU9:]AQw");
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs
index 7d5109f..9319274 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs
@@ -9,18 +9,19 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser;
public sealed class CreateUserCommandHandlerTests
{
private readonly CreateUserCommandTestFixture _fixture = new();
-
+
[Fact]
public void Should_Create_User()
{
_fixture.SetupUser();
-
+
var command = new CreateUserCommand(
Guid.NewGuid(),
"test@email.com",
"Test",
- "Email");
-
+ "Email",
+ "Po=PF]PC6t.?8?ks)A6W");
+
_fixture.CommandHandler.Handle(command, default).Wait();
_fixture
@@ -28,18 +29,19 @@ public sealed class CreateUserCommandHandlerTests
.VerifyCommit()
.VerifyRaisedEvent(x => x.UserId == command.UserId);
}
-
+
[Fact]
public void Should_Not_Create_Already_Existing_User()
{
var user = _fixture.SetupUser();
-
+
var command = new CreateUserCommand(
user.Id,
"test@email.com",
"Test",
- "Email");
-
+ "Email",
+ "Po=PF]PC6t.?8?ks)A6W");
+
_fixture.CommandHandler.Handle(command, default).Wait();
_fixture
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs
index 2f30515..f6879b3 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs
@@ -1,5 +1,6 @@
using System;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;
@@ -7,27 +8,29 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser;
public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase
{
- public CreateUserCommandHandler CommandHandler { get; }
- private Mock UserRepository { get; }
-
public CreateUserCommandTestFixture()
{
UserRepository = new Mock();
-
- CommandHandler = new(
+
+ CommandHandler = new CreateUserCommandHandler(
Bus.Object,
UnitOfWork.Object,
NotificationHandler.Object,
UserRepository.Object);
}
-
+
+ public CreateUserCommandHandler CommandHandler { get; }
+ private Mock UserRepository { get; }
+
public Entities.User SetupUser()
{
var user = new Entities.User(
Guid.NewGuid(),
"max@mustermann.com",
"Max",
- "Mustermann");
+ "Mustermann",
+ "Password",
+ UserRole.User);
UserRepository
.Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id)))
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs
index a6c54e4..05a8d1b 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Errors;
using Xunit;
@@ -11,111 +13,181 @@ public sealed class CreateUserCommandValidationTests :
public CreateUserCommandValidationTests() : base(new CreateUserCommandValidation())
{
}
-
+
[Fact]
public void Should_Be_Valid()
{
var command = CreateTestCommand();
-
+
ShouldBeValid(command);
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_User_Id()
{
- var command = CreateTestCommand(userId: Guid.Empty);
-
+ var command = CreateTestCommand(Guid.Empty);
+
ShouldHaveSingleError(
- command,
+ command,
DomainErrorCodes.UserEmptyId,
"User id may not be empty");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_Email()
{
var command = CreateTestCommand(email: string.Empty);
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserInvalidEmail,
"Email is not a valid email address");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Invalid_Email()
{
var command = CreateTestCommand(email: "not a email");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserInvalidEmail,
"Email is not a valid email address");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
{
var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmailExceedsMaxLength,
"Email may not be longer than 320 characters");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_Surname()
{
var command = CreateTestCommand(surName: "");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmptySurname,
"Surname may not be empty");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Surname_Exceeds_Max_Length()
{
var command = CreateTestCommand(surName: new string('a', 101));
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserSurnameExceedsMaxLength,
"Surname may not be longer than 100 characters");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_Given_Name()
{
var command = CreateTestCommand(givenName: "");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmptyGivenName,
"Given name may not be empty");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Given_Name_Exceeds_Max_Length()
{
var command = CreateTestCommand(givenName: new string('a', 101));
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserGivenNameExceedsMaxLength,
"Given name may not be longer than 100 characters");
}
-
- private CreateUserCommand CreateTestCommand(
+
+ [Fact]
+ public void Should_Be_Invalid_For_Empty_Password()
+ {
+ var command = CreateTestCommand(password: "");
+
+ var errors = new List
+ {
+ DomainErrorCodes.UserEmptyPassword,
+ DomainErrorCodes.UserSpecialCharPassword,
+ DomainErrorCodes.UserNumberPassword,
+ DomainErrorCodes.UserLowercaseLetterPassword,
+ DomainErrorCodes.UserUppercaseLetterPassword,
+ DomainErrorCodes.UserShortPassword
+ };
+
+ ShouldHaveExpectedErrors(command, errors.ToArray());
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Special_Character()
+ {
+ var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Number()
+ {
+ var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Lowercase_Character()
+ {
+ var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Uppercase_Character()
+ {
+ var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Password_Too_Short()
+ {
+ var command = CreateTestCommand(password: "zA6{");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Password_Too_Long()
+ {
+ var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
+ }
+
+ private static CreateUserCommand CreateTestCommand(
Guid? userId = null,
string? email = null,
string? surName = null,
- string? givenName = null) =>
- new (
+ string? givenName = null,
+ string? password = null)
+ {
+ return new(
userId ?? Guid.NewGuid(),
email ?? "test@email.com",
surName ?? "test",
- givenName ?? "email");
+ givenName ?? "email",
+ password ?? "Po=PF]PC6t.?8?ks)A6W");
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs
index dc23bd6..fabfd29 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs
@@ -9,14 +9,14 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser;
public sealed class DeleteUserCommandHandlerTests
{
private readonly DeleteUserCommandTestFixture _fixture = new();
-
+
[Fact]
public void Should_Delete_User()
{
var user = _fixture.SetupUser();
-
+
var command = new DeleteUserCommand(user.Id);
-
+
_fixture.CommandHandler.Handle(command, default).Wait();
_fixture
@@ -24,14 +24,14 @@ public sealed class DeleteUserCommandHandlerTests
.VerifyCommit()
.VerifyRaisedEvent(x => x.UserId == user.Id);
}
-
+
[Fact]
public void Should_Not_Delete_Non_Existing_User()
{
_fixture.SetupUser();
-
+
var command = new DeleteUserCommand(Guid.NewGuid());
-
+
_fixture.CommandHandler.Handle(command, default).Wait();
_fixture
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs
index 19f7761..aaf2e66 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs
@@ -1,5 +1,6 @@
using System;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;
@@ -7,27 +8,30 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser;
public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase
{
- public DeleteUserCommandHandler CommandHandler { get; }
- private Mock UserRepository { get; }
-
public DeleteUserCommandTestFixture()
{
UserRepository = new Mock();
-
- CommandHandler = new (
+
+ CommandHandler = new DeleteUserCommandHandler(
Bus.Object,
UnitOfWork.Object,
NotificationHandler.Object,
- UserRepository.Object);
+ UserRepository.Object,
+ User.Object);
}
+ public DeleteUserCommandHandler CommandHandler { get; }
+ private Mock UserRepository { get; }
+
public Entities.User SetupUser()
{
var user = new Entities.User(
Guid.NewGuid(),
"max@mustermann.com",
"Max",
- "Mustermann");
+ "Mustermann",
+ "Password",
+ UserRole.User);
UserRepository
.Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id)))
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs
index 50106e5..a2e6e45 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs
@@ -16,21 +16,23 @@ public sealed class DeleteUserCommandValidationTests :
public void Should_Be_Valid()
{
var command = CreateTestCommand();
-
+
ShouldBeValid(command);
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_User_Id()
{
- var command = CreateTestCommand(userId: Guid.Empty);
-
+ var command = CreateTestCommand(Guid.Empty);
+
ShouldHaveSingleError(
- command,
+ command,
DomainErrorCodes.UserEmptyId,
"User id may not be empty");
}
-
- private DeleteUserCommand CreateTestCommand(Guid? userId = null) =>
- new (userId ?? Guid.NewGuid());
+
+ private static DeleteUserCommand CreateTestCommand(Guid? userId = null)
+ {
+ return new(userId ?? Guid.NewGuid());
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs
new file mode 100644
index 0000000..6137b67
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using CleanArchitecture.Domain.Commands.Users.LoginUser;
+using CleanArchitecture.Domain.Errors;
+using FluentAssertions;
+using Xunit;
+
+namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
+
+public sealed class LoginUserCommandHandlerTests
+{
+ private readonly LoginUserCommandTestFixture _fixture = new();
+
+ [Fact]
+ public async Task Should_Login_User()
+ {
+ var user = _fixture.SetupUser();
+
+ var command = new LoginUserCommand(user.Email, "z8]tnayvd5FNLU9:]AQm");
+
+ var token = await _fixture.CommandHandler.Handle(command, default);
+
+ _fixture.VerifyNoDomainNotification();
+
+ token.Should().NotBeNullOrEmpty();
+
+ var handler = new JwtSecurityTokenHandler();
+ var decodedToken = handler.ReadToken(token) as JwtSecurityToken;
+
+ var userIdClaim = decodedToken!.Claims
+ .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier));
+
+ Guid.Parse(userIdClaim!.Value).Should().Be(user.Id);
+
+ var userEmailClaim = decodedToken!.Claims
+ .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email));
+
+ userEmailClaim!.Value.Should().Be(user.Email);
+
+ var userRoleClaim = decodedToken!.Claims
+ .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role));
+
+ userRoleClaim!.Value.Should().Be(user.Role.ToString());
+ }
+
+ [Fact]
+ public async Task Should_Not_Login_User_No_User()
+ {
+ var command = new LoginUserCommand("test@email.com", "z8]tnayvd5FNLU9:]AQm");
+
+ var token = await _fixture.CommandHandler.Handle(command, default);
+
+ _fixture
+ .VerifyAnyDomainNotification()
+ .VerifyExistingNotification(
+ ErrorCodes.ObjectNotFound,
+ $"There is no User with Email {command.Email}");
+
+ token.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task Should_Not_Login_User_Incorrect_Password()
+ {
+ var user = _fixture.SetupUser();
+
+ var command = new LoginUserCommand(user.Email, "z8]tnayvd5FNLU9:]AQw");
+
+ var token = await _fixture.CommandHandler.Handle(command, default);
+
+ _fixture
+ .VerifyAnyDomainNotification()
+ .VerifyExistingNotification(
+ DomainErrorCodes.UserPasswordIncorrect,
+ "The password is incorrect");
+
+ token.Should().BeEmpty();
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs
new file mode 100644
index 0000000..71ee339
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandTestFixture.cs
@@ -0,0 +1,55 @@
+using System;
+using CleanArchitecture.Domain.Commands.Users.LoginUser;
+using CleanArchitecture.Domain.Enums;
+using CleanArchitecture.Domain.Interfaces.Repositories;
+using CleanArchitecture.Domain.Settings;
+using Microsoft.Extensions.Options;
+using Moq;
+using BC = BCrypt.Net.BCrypt;
+
+namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
+
+public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase
+{
+ public LoginUserCommandTestFixture()
+ {
+ UserRepository = new Mock();
+
+ TokenSettings = Options.Create(new TokenSettings
+ {
+ Issuer = "TestIssuer",
+ Audience = "TestAudience",
+ Secret = "asjdlkasjd87439284)@#(*"
+ });
+
+ CommandHandler = new LoginUserCommandHandler(
+ Bus.Object,
+ UnitOfWork.Object,
+ NotificationHandler.Object,
+ UserRepository.Object,
+ TokenSettings);
+ }
+
+ public LoginUserCommandHandler CommandHandler { get; set; }
+ public Mock UserRepository { get; set; }
+ public IOptions TokenSettings { get; set; }
+
+ public Entities.User SetupUser()
+ {
+ var user = new Entities.User(
+ Guid.NewGuid(),
+ "max@mustermann.com",
+ "Max",
+ "Mustermann",
+ BC.HashPassword("z8]tnayvd5FNLU9:]AQm"),
+ UserRole.User);
+
+ User.Setup(x => x.GetUserId()).Returns(user.Id);
+
+ UserRepository
+ .Setup(x => x.GetByEmailAsync(It.Is(y => y == user.Email)))
+ .ReturnsAsync(user);
+
+ return user;
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs
new file mode 100644
index 0000000..fd85ce8
--- /dev/null
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs
@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using System.Linq;
+using CleanArchitecture.Domain.Commands.Users.LoginUser;
+using CleanArchitecture.Domain.Errors;
+using Xunit;
+
+namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
+
+public sealed class LoginUserCommandValidationTests :
+ ValidationTestBase
+{
+ public LoginUserCommandValidationTests() : base(new LoginUserCommandValidation())
+ {
+ }
+
+ [Fact]
+ public void Should_Be_Valid()
+ {
+ var command = CreateTestCommand();
+
+ ShouldBeValid(command);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Empty_Email()
+ {
+ var command = CreateTestCommand(string.Empty);
+
+ ShouldHaveSingleError(
+ command,
+ DomainErrorCodes.UserInvalidEmail,
+ "Email is not a valid email address");
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Invalid_Email()
+ {
+ var command = CreateTestCommand("not a email");
+
+ ShouldHaveSingleError(
+ command,
+ DomainErrorCodes.UserInvalidEmail,
+ "Email is not a valid email address");
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
+ {
+ var command = CreateTestCommand(new string('a', 320) + "@test.com");
+
+ ShouldHaveSingleError(
+ command,
+ DomainErrorCodes.UserEmailExceedsMaxLength,
+ "Email may not be longer than 320 characters");
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Empty_Password()
+ {
+ var command = CreateTestCommand(password: "");
+
+ var errors = new List
+ {
+ DomainErrorCodes.UserEmptyPassword,
+ DomainErrorCodes.UserSpecialCharPassword,
+ DomainErrorCodes.UserNumberPassword,
+ DomainErrorCodes.UserLowercaseLetterPassword,
+ DomainErrorCodes.UserUppercaseLetterPassword,
+ DomainErrorCodes.UserShortPassword
+ };
+
+ ShouldHaveExpectedErrors(command, errors.ToArray());
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Special_Character()
+ {
+ var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Number()
+ {
+ var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Lowercase_Character()
+ {
+ var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Missing_Uppercase_Character()
+ {
+ var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Password_Too_Short()
+ {
+ var command = CreateTestCommand(password: "zA6{");
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
+ }
+
+ [Fact]
+ public void Should_Be_Invalid_For_Password_Too_Long()
+ {
+ var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
+
+ ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
+ }
+
+ private static LoginUserCommand CreateTestCommand(
+ string? email = null,
+ string? password = null)
+ {
+ return new(
+ email ?? "test@email.com",
+ password ?? "Po=PF]PC6t.?8?ks)A6W");
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs
index bdd53e6..98a13df 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;
using Xunit;
@@ -10,18 +11,19 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser;
public sealed class UpdateUserCommandHandlerTests
{
private readonly UpdateUserCommandTestFixture _fixture = new();
-
+
[Fact]
public async Task Should_Update_User()
{
var user = _fixture.SetupUser();
-
+
var command = new UpdateUserCommand(
user.Id,
"test@email.com",
"Test",
- "Email");
-
+ "Email",
+ UserRole.User);
+
await _fixture.CommandHandler.Handle(command, default);
_fixture
@@ -29,18 +31,19 @@ public sealed class UpdateUserCommandHandlerTests
.VerifyCommit()
.VerifyRaisedEvent(x => x.UserId == command.UserId);
}
-
+
[Fact]
public async Task Should_Not_Update_Non_Existing_User()
{
_fixture.SetupUser();
-
+
var command = new UpdateUserCommand(
Guid.NewGuid(),
"test@email.com",
"Test",
- "Email");
-
+ "Email",
+ UserRole.User);
+
await _fixture.CommandHandler.Handle(command, default);
_fixture
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs
index 1b63128..69bb3ca 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs
@@ -1,5 +1,6 @@
using System;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;
@@ -7,27 +8,30 @@ namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser;
public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase
{
- public UpdateUserCommandHandler CommandHandler { get; }
- private Mock UserRepository { get; }
-
public UpdateUserCommandTestFixture()
{
UserRepository = new Mock();
-
- CommandHandler = new(
+
+ CommandHandler = new UpdateUserCommandHandler(
Bus.Object,
UnitOfWork.Object,
NotificationHandler.Object,
- UserRepository.Object);
+ UserRepository.Object,
+ User.Object);
}
-
+
+ public UpdateUserCommandHandler CommandHandler { get; }
+ private Mock UserRepository { get; }
+
public Entities.User SetupUser()
{
var user = new Entities.User(
Guid.NewGuid(),
"max@mustermann.com",
"Max",
- "Mustermann");
+ "Mustermann",
+ "Password",
+ UserRole.User);
UserRepository
.Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id)))
diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs
index e593959..2fe4e21 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs
@@ -1,5 +1,6 @@
using System;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors;
using Xunit;
@@ -11,111 +12,115 @@ public sealed class UpdateUserCommandValidationTests :
public UpdateUserCommandValidationTests() : base(new UpdateUserCommandValidation())
{
}
-
+
[Fact]
public void Should_Be_Valid()
{
var command = CreateTestCommand();
-
+
ShouldBeValid(command);
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_User_Id()
{
- var command = CreateTestCommand(userId: Guid.Empty);
-
+ var command = CreateTestCommand(Guid.Empty);
+
ShouldHaveSingleError(
- command,
+ command,
DomainErrorCodes.UserEmptyId,
"User id may not be empty");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_Email()
{
var command = CreateTestCommand(email: string.Empty);
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserInvalidEmail,
"Email is not a valid email address");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Invalid_Email()
{
var command = CreateTestCommand(email: "not a email");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserInvalidEmail,
"Email is not a valid email address");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
{
var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmailExceedsMaxLength,
"Email may not be longer than 320 characters");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_Surname()
{
var command = CreateTestCommand(surName: "");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmptySurname,
"Surname may not be empty");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Surname_Exceeds_Max_Length()
{
var command = CreateTestCommand(surName: new string('a', 101));
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserSurnameExceedsMaxLength,
"Surname may not be longer than 100 characters");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Empty_Given_Name()
{
var command = CreateTestCommand(givenName: "");
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmptyGivenName,
"Given name may not be empty");
}
-
+
[Fact]
public void Should_Be_Invalid_For_Given_Name_Exceeds_Max_Length()
{
var command = CreateTestCommand(givenName: new string('a', 101));
-
+
ShouldHaveSingleError(
command,
DomainErrorCodes.UserGivenNameExceedsMaxLength,
"Given name may not be longer than 100 characters");
}
-
- private UpdateUserCommand CreateTestCommand(
+
+ private static UpdateUserCommand CreateTestCommand(
Guid? userId = null,
string? email = null,
string? surName = null,
- string? givenName = null) =>
- new (
+ string? givenName = null,
+ UserRole? role = null)
+ {
+ return new(
userId ?? Guid.NewGuid(),
email ?? "test@email.com",
surName ?? "test",
- givenName ?? "email");
+ givenName ?? "email",
+ role ?? UserRole.User);
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs
index 01964a0..ce8a493 100644
--- a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs
+++ b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq.Expressions;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Domain.Notifications;
using Moq;
@@ -8,19 +9,24 @@ namespace CleanArchitecture.Domain.Tests;
public class CommandHandlerFixtureBase
{
- protected Mock Bus { get; }
- protected Mock UnitOfWork { get; }
- protected Mock NotificationHandler { get; }
-
protected CommandHandlerFixtureBase()
{
Bus = new Mock();
UnitOfWork = new Mock();
NotificationHandler = new Mock();
+ User = new Mock();
+
+ User.Setup(x => x.GetUserId()).Returns(Guid.NewGuid());
+ User.Setup(x => x.GetUserRole()).Returns(UserRole.Admin);
UnitOfWork.Setup(unit => unit.CommitAsync()).ReturnsAsync(true);
}
+ protected Mock Bus { get; }
+ protected Mock UnitOfWork { get; }
+ protected Mock NotificationHandler { get; }
+ protected Mock User { get; }
+
public CommandHandlerFixtureBase VerifyExistingNotification(string errorCode, string message)
{
Bus.Verify(
diff --git a/CleanArchitecture.Domain.Tests/ValidationTestBase.cs b/CleanArchitecture.Domain.Tests/ValidationTestBase.cs
index 7f985c8..37391ee 100644
--- a/CleanArchitecture.Domain.Tests/ValidationTestBase.cs
+++ b/CleanArchitecture.Domain.Tests/ValidationTestBase.cs
@@ -8,7 +8,7 @@ namespace CleanArchitecture.Domain.Tests;
public class ValidationTestBase
where TCommand : CommandBase
- where TValidation: AbstractValidator
+ where TValidation : AbstractValidator
{
private readonly TValidation _validation;
@@ -54,7 +54,7 @@ public class ValidationTestBase
}
protected void ShouldHaveExpectedErrors(
- TCommand command,
+ TCommand command,
params KeyValuePair[] expectedErrors)
{
var result = _validation.Validate(command);
@@ -70,4 +70,22 @@ public class ValidationTestBase
.Be(1);
}
}
+
+ protected void ShouldHaveExpectedErrors(
+ TCommand command,
+ params string[] expectedErrors)
+ {
+ var result = _validation.Validate(command);
+
+ result.IsValid.Should().BeFalse();
+ result.Errors.Count.Should().Be(expectedErrors.Length);
+
+ foreach (var error in expectedErrors)
+ {
+ result.Errors
+ .Count(validation => validation.ErrorCode == error)
+ .Should()
+ .Be(1);
+ }
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs
new file mode 100644
index 0000000..285bfd6
--- /dev/null
+++ b/CleanArchitecture.Domain/ApiUser.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using CleanArchitecture.Domain.Enums;
+using CleanArchitecture.Domain.Interfaces;
+using Microsoft.AspNetCore.Http;
+
+namespace CleanArchitecture.Domain;
+
+public sealed class ApiUser : IUser
+{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public ApiUser(IHttpContextAccessor httpContextAccessor)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ public Guid GetUserId()
+ {
+ var claim = _httpContextAccessor.HttpContext?.User.Claims
+ .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier));
+
+ if (Guid.TryParse(claim?.Value, out var userId))
+ {
+ return userId;
+ }
+
+ throw new ArgumentException("Could not parse user id to guid");
+ }
+
+ public UserRole GetUserRole()
+ {
+ var claim = _httpContextAccessor.HttpContext?.User.Claims
+ .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role));
+
+ if (Enum.TryParse(claim?.Value, out UserRole userRole))
+ {
+ return userRole;
+ }
+
+ throw new ArgumentException("Could not parse user role");
+ }
+
+ public string Name => _httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty;
+
+ public string GetUserEmail()
+ {
+ var claim = _httpContextAccessor.HttpContext?.User.Claims
+ .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email));
+
+ if (!string.IsNullOrWhiteSpace(claim?.Value))
+ {
+ return claim.Value;
+ }
+
+ throw new ArgumentException("Could not parse user email");
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
index 8c0626a..3774cae 100644
--- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
+++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
@@ -6,8 +6,12 @@
-
-
+
+
+
+
+
+
diff --git a/CleanArchitecture.Domain/Commands/CommandBase.cs b/CleanArchitecture.Domain/Commands/CommandBase.cs
index 25f84f6..02d78d1 100644
--- a/CleanArchitecture.Domain/Commands/CommandBase.cs
+++ b/CleanArchitecture.Domain/Commands/CommandBase.cs
@@ -6,17 +6,17 @@ namespace CleanArchitecture.Domain.Commands;
public abstract class CommandBase : IRequest
{
- public Guid AggregateId { get; }
- public string MessageType { get; }
- public DateTime Timestamp { get; }
- public ValidationResult? ValidationResult { get; protected set; }
-
protected CommandBase(Guid aggregateId)
{
MessageType = GetType().Name;
Timestamp = DateTime.Now;
AggregateId = aggregateId;
}
-
+
+ public Guid AggregateId { get; }
+ public string MessageType { get; }
+ public DateTime Timestamp { get; }
+ public ValidationResult? ValidationResult { get; protected set; }
+
public abstract bool IsValid();
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs
index fe9b36c..54a557b 100644
--- a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs
+++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs
@@ -9,21 +9,21 @@ namespace CleanArchitecture.Domain.Commands;
public abstract class CommandHandlerBase
{
- protected readonly IMediatorHandler _bus;
- private readonly IUnitOfWork _unitOfWork;
+ protected readonly IMediatorHandler Bus;
private readonly DomainNotificationHandler _notifications;
+ private readonly IUnitOfWork _unitOfWork;
protected CommandHandlerBase(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler notifications)
{
- _bus = bus;
+ Bus = bus;
_unitOfWork = unitOfWork;
_notifications = (DomainNotificationHandler)notifications;
}
-
- public async Task CommitAsync()
+
+ protected async Task CommitAsync()
{
if (_notifications.HasNotifications())
{
@@ -35,7 +35,7 @@ public abstract class CommandHandlerBase
return true;
}
- await _bus.RaiseEventAsync(
+ await Bus.RaiseEventAsync(
new DomainNotification(
"Commit",
"Problem occured while saving the data. Please try again.",
@@ -43,18 +43,18 @@ public abstract class CommandHandlerBase
return false;
}
-
+
protected async Task NotifyAsync(string key, string message, string code)
{
- await _bus.RaiseEventAsync(
+ await Bus.RaiseEventAsync(
new DomainNotification(key, message, code));
}
protected async Task NotifyAsync(DomainNotification notification)
{
- await _bus.RaiseEventAsync(notification);
+ await Bus.RaiseEventAsync(notification);
}
-
+
protected async ValueTask TestValidityAsync(CommandBase command)
{
if (command.IsValid())
@@ -71,8 +71,8 @@ public abstract class CommandHandlerBase
{
await NotifyAsync(
new DomainNotification(
- command.MessageType,
- error.ErrorMessage,
+ command.MessageType,
+ error.ErrorMessage,
error.ErrorCode,
error.FormattedMessagePlaceholderValues));
}
diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs
new file mode 100644
index 0000000..4a27534
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
+
+public sealed class ChangePasswordCommand : CommandBase
+{
+ private readonly ChangePasswordCommandValidation _validation = new();
+
+ public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid())
+ {
+ Password = password;
+ NewPassword = newPassword;
+ }
+
+ public string Password { get; }
+ public string NewPassword { get; }
+
+ public override bool IsValid()
+ {
+ ValidationResult = _validation.Validate(this);
+ return ValidationResult.IsValid;
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs
new file mode 100644
index 0000000..38a85dd
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs
@@ -0,0 +1,71 @@
+using System.Threading;
+using System.Threading.Tasks;
+using CleanArchitecture.Domain.Errors;
+using CleanArchitecture.Domain.Events.User;
+using CleanArchitecture.Domain.Interfaces;
+using CleanArchitecture.Domain.Interfaces.Repositories;
+using CleanArchitecture.Domain.Notifications;
+using MediatR;
+using BC = BCrypt.Net.BCrypt;
+
+namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
+
+public sealed class ChangePasswordCommandHandler : CommandHandlerBase,
+ IRequestHandler
+{
+ private readonly IUser _user;
+ private readonly IUserRepository _userRepository;
+
+ public ChangePasswordCommandHandler(
+ IMediatorHandler bus,
+ IUnitOfWork unitOfWork,
+ INotificationHandler notifications,
+ IUserRepository userRepository,
+ IUser user) : base(bus, unitOfWork, notifications)
+ {
+ _userRepository = userRepository;
+ _user = user;
+ }
+
+ public async Task Handle(ChangePasswordCommand request, CancellationToken cancellationToken)
+ {
+ if (!await TestValidityAsync(request))
+ {
+ return;
+ }
+
+ var user = await _userRepository.GetByIdAsync(_user.GetUserId());
+
+ if (user == null)
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"There is no User with Id {_user.GetUserId()}",
+ ErrorCodes.ObjectNotFound));
+
+ return;
+ }
+
+ if (!BC.Verify(request.Password, user.Password))
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ "The password is incorrect",
+ DomainErrorCodes.UserPasswordIncorrect));
+
+ return;
+ }
+
+ var passwordHash = BC.HashPassword(request.NewPassword);
+ user.SetPassword(passwordHash);
+
+ _userRepository.Update(user);
+
+ if (await CommitAsync())
+ {
+ await Bus.RaiseEventAsync(new PasswordChangedEvent(user.Id));
+ }
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs
new file mode 100644
index 0000000..83ea0d9
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandValidation.cs
@@ -0,0 +1,25 @@
+using CleanArchitecture.Domain.Extensions.Validation;
+using FluentValidation;
+
+namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
+
+public sealed class ChangePasswordCommandValidation : AbstractValidator
+{
+ public ChangePasswordCommandValidation()
+ {
+ AddRuleForPassword();
+ AddRuleForNewPassword();
+ }
+
+ private void AddRuleForPassword()
+ {
+ RuleFor(cmd => cmd.Password)
+ .Password();
+ }
+
+ private void AddRuleForNewPassword()
+ {
+ RuleFor(cmd => cmd.NewPassword)
+ .Password();
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
index 6106289..bafd286 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs
@@ -4,25 +4,28 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
public sealed class CreateUserCommand : CommandBase
{
- private readonly CreateUserCommandValidation _validation = new();
-
- public Guid UserId { get; }
- public string Email { get; }
- public string Surname { get; }
- public string GivenName { get; }
-
+ private readonly CreateUserCommandValidation _validation = new();
+
public CreateUserCommand(
Guid userId,
string email,
string surname,
- string givenName) : base(userId)
+ string givenName,
+ string password) : base(userId)
{
UserId = userId;
Email = email;
Surname = surname;
GivenName = givenName;
+ Password = password;
}
+ public Guid UserId { get; }
+ public string Email { get; }
+ public string Surname { get; }
+ public string GivenName { get; }
+ public string Password { get; }
+
public override bool IsValid()
{
ValidationResult = _validation.Validate(this);
diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
index 0ebbd00..a639591 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs
@@ -1,12 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Entities;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Domain.Interfaces.Repositories;
using CleanArchitecture.Domain.Notifications;
using MediatR;
+using BC = BCrypt.Net.BCrypt;
namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
@@ -14,7 +16,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
IRequestHandler
{
private readonly IUserRepository _userRepository;
-
+
public CreateUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
@@ -35,7 +37,7 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
if (existingUser != null)
{
- await _bus.RaiseEventAsync(
+ await Bus.RaiseEventAsync(
new DomainNotification(
request.MessageType,
$"There is already a User with Id {request.UserId}",
@@ -43,17 +45,33 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
return;
}
+ existingUser = await _userRepository.GetByEmailAsync(request.Email);
+
+ if (existingUser != null)
+ {
+ await Bus.RaiseEventAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"There is already a User with Email {request.Email}",
+ DomainErrorCodes.UserAlreadyExists));
+ return;
+ }
+
+ var passwordHash = BC.HashPassword(request.Password);
+
var user = new User(
- request.UserId,
+ request.UserId,
request.Email,
request.Surname,
- request.GivenName);
-
+ request.GivenName,
+ passwordHash,
+ UserRole.User);
+
_userRepository.Add(user);
-
+
if (await CommitAsync())
{
- await _bus.RaiseEventAsync(new UserCreatedEvent(user.Id));
+ await Bus.RaiseEventAsync(new UserCreatedEvent(user.Id));
}
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs
index 3c14aef..23be1aa 100644
--- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs
+++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs
@@ -1,4 +1,5 @@
using CleanArchitecture.Domain.Errors;
+using CleanArchitecture.Domain.Extensions.Validation;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
@@ -11,6 +12,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Email)
@@ -31,7 +33,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Surname)
@@ -42,7 +44,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.GivenName)
@@ -53,4 +55,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Password)
+ .Password();
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs
index a15d7d6..48c42f5 100644
--- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs
@@ -4,15 +4,15 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
public sealed class DeleteUserCommand : CommandBase
{
- private readonly DeleteUserCommandValidation _validation = new();
-
- public Guid UserId { get; }
-
+ private readonly DeleteUserCommandValidation _validation = new();
+
public DeleteUserCommand(Guid userId) : base(userId)
{
UserId = userId;
}
+ public Guid UserId { get; }
+
public override bool IsValid()
{
ValidationResult = _validation.Validate(this);
diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs
index edf9b0c..1fc4970 100644
--- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs
@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces;
@@ -12,15 +13,18 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
public sealed class DeleteUserCommandHandler : CommandHandlerBase,
IRequestHandler
{
+ private readonly IUser _user;
private readonly IUserRepository _userRepository;
-
+
public DeleteUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler notifications,
- IUserRepository userRepository) : base(bus, unitOfWork, notifications)
+ IUserRepository userRepository,
+ IUser user) : base(bus, unitOfWork, notifications)
{
_userRepository = userRepository;
+ _user = user;
}
public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken)
@@ -31,7 +35,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
}
var user = await _userRepository.GetByIdAsync(request.UserId);
-
+
if (user == null)
{
await NotifyAsync(
@@ -43,11 +47,22 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
return;
}
+ if (_user.GetUserId() != request.UserId && _user.GetUserRole() != UserRole.Admin)
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"No permission to delete user {request.UserId}",
+ ErrorCodes.InsufficientPermissions));
+
+ return;
+ }
+
_userRepository.Remove(user);
if (await CommitAsync())
{
- await _bus.RaiseEventAsync(new UserDeletedEvent(request.UserId));
+ await Bus.RaiseEventAsync(new UserDeletedEvent(request.UserId));
}
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs
new file mode 100644
index 0000000..2fcdc00
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs
@@ -0,0 +1,28 @@
+using System;
+using MediatR;
+
+namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
+
+public sealed class LoginUserCommand : CommandBase,
+ IRequest
+{
+ private readonly LoginUserCommandValidation _validation = new();
+
+
+ public LoginUserCommand(
+ string email,
+ string password) : base(Guid.NewGuid())
+ {
+ Email = email;
+ Password = password;
+ }
+
+ public string Email { get; set; }
+ public string Password { get; set; }
+
+ public override bool IsValid()
+ {
+ ValidationResult = _validation.Validate(this);
+ return ValidationResult.IsValid;
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs
new file mode 100644
index 0000000..06cb8d7
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs
@@ -0,0 +1,104 @@
+using System;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using CleanArchitecture.Domain.Enums;
+using CleanArchitecture.Domain.Errors;
+using CleanArchitecture.Domain.Interfaces;
+using CleanArchitecture.Domain.Interfaces.Repositories;
+using CleanArchitecture.Domain.Notifications;
+using CleanArchitecture.Domain.Settings;
+using MediatR;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using BC = BCrypt.Net.BCrypt;
+
+namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
+
+public sealed class LoginUserCommandHandler : CommandHandlerBase,
+ IRequestHandler
+{
+ private const double ExpiryDurationMinutes = 30;
+ private readonly TokenSettings _tokenSettings;
+
+ private readonly IUserRepository _userRepository;
+
+ public LoginUserCommandHandler(
+ IMediatorHandler bus,
+ IUnitOfWork unitOfWork,
+ INotificationHandler notifications,
+ IUserRepository userRepository,
+ IOptions tokenSettings) : base(bus, unitOfWork, notifications)
+ {
+ _userRepository = userRepository;
+ _tokenSettings = tokenSettings.Value;
+ }
+
+ public async Task Handle(LoginUserCommand request, CancellationToken cancellationToken)
+ {
+ if (!await TestValidityAsync(request))
+ {
+ return "";
+ }
+
+ var user = await _userRepository.GetByEmailAsync(request.Email);
+
+ if (user == null)
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"There is no User with Email {request.Email}",
+ ErrorCodes.ObjectNotFound));
+
+ return "";
+ }
+
+ var passwordVerified = BC.Verify(request.Password, user.Password);
+
+ if (!passwordVerified)
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ "The password is incorrect",
+ DomainErrorCodes.UserPasswordIncorrect));
+
+ return "";
+ }
+
+ return BuildToken(
+ user.Email,
+ user.Role,
+ user.Id,
+ _tokenSettings);
+ }
+
+ private static string BuildToken(string email, UserRole role, Guid id, TokenSettings tokenSettings)
+ {
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.Email, email),
+ new Claim(ClaimTypes.Role, role.ToString()),
+ new Claim(ClaimTypes.NameIdentifier, id.ToString())
+ };
+
+ var securityKey = new SymmetricSecurityKey(
+ Encoding.UTF8.GetBytes(tokenSettings.Secret));
+
+ var credentials = new SigningCredentials(
+ securityKey,
+ SecurityAlgorithms.HmacSha256Signature);
+
+ var tokenDescriptor = new JwtSecurityToken(
+ tokenSettings.Issuer,
+ tokenSettings.Audience,
+ claims,
+ expires: DateTime.Now.AddMinutes(ExpiryDurationMinutes),
+ signingCredentials: credentials);
+
+ return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs
new file mode 100644
index 0000000..9f668cd
--- /dev/null
+++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs
@@ -0,0 +1,31 @@
+using CleanArchitecture.Domain.Errors;
+using CleanArchitecture.Domain.Extensions.Validation;
+using FluentValidation;
+
+namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
+
+public sealed class LoginUserCommandValidation : AbstractValidator
+{
+ public LoginUserCommandValidation()
+ {
+ AddRuleForEmail();
+ AddRuleForPassword();
+ }
+
+ private void AddRuleForEmail()
+ {
+ RuleFor(cmd => cmd.Email)
+ .EmailAddress()
+ .WithErrorCode(DomainErrorCodes.UserInvalidEmail)
+ .WithMessage("Email is not a valid email address")
+ .MaximumLength(320)
+ .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
+ .WithMessage("Email may not be longer than 320 characters");
+ }
+
+ private void AddRuleForPassword()
+ {
+ RuleFor(cmd => cmd.Password)
+ .Password();
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
index 44e3617..918bec9 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs
@@ -1,28 +1,32 @@
using System;
+using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
public sealed class UpdateUserCommand : CommandBase
{
private readonly UpdateUserCommandValidation _validation = new();
-
- public Guid UserId { get; }
- public string Email { get; }
- public string Surname { get; }
- public string GivenName { get; }
-
+
public UpdateUserCommand(
Guid userId,
string email,
string surname,
- string givenName) : base(userId)
+ string givenName,
+ UserRole role) : base(userId)
{
UserId = userId;
Email = email;
Surname = surname;
GivenName = givenName;
+ Role = role;
}
+ public Guid UserId { get; }
+ public string Email { get; }
+ public string Surname { get; }
+ public string GivenName { get; }
+ public UserRole Role { get; }
+
public override bool IsValid()
{
ValidationResult = _validation.Validate(this);
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
index a94c5b5..6f882e3 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs
@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces;
@@ -12,15 +13,18 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
public sealed class UpdateUserCommandHandler : CommandHandlerBase,
IRequestHandler
{
+ private readonly IUser _user;
private readonly IUserRepository _userRepository;
-
+
public UpdateUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler notifications,
- IUserRepository userRepository) : base(bus, unitOfWork, notifications)
+ IUserRepository userRepository,
+ IUser user) : base(bus, unitOfWork, notifications)
{
_userRepository = userRepository;
+ _user = user;
}
public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken)
@@ -34,23 +38,39 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
if (user == null)
{
- await _bus.RaiseEventAsync(
+ await Bus.RaiseEventAsync(
new DomainNotification(
request.MessageType,
$"There is no User with Id {request.UserId}",
ErrorCodes.ObjectNotFound));
return;
}
-
+
+ if (_user.GetUserId() != request.UserId && _user.GetUserRole() != UserRole.Admin)
+ {
+ await NotifyAsync(
+ new DomainNotification(
+ request.MessageType,
+ $"No permission to update user {request.UserId}",
+ ErrorCodes.InsufficientPermissions));
+
+ return;
+ }
+
+ if (_user.GetUserRole() == UserRole.Admin)
+ {
+ user.SetRole(request.Role);
+ }
+
user.SetEmail(request.Email);
user.SetSurname(request.Surname);
user.SetGivenName(request.GivenName);
-
+
_userRepository.Update(user);
-
+
if (await CommitAsync())
{
- await _bus.RaiseEventAsync(new UserUpdatedEvent(user.Id));
+ await Bus.RaiseEventAsync(new UserUpdatedEvent(user.Id));
}
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs
index 0b7cc35..f0afc67 100644
--- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs
+++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs
@@ -11,6 +11,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Email)
@@ -31,7 +32,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Surname)
@@ -42,7 +43,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.GivenName)
@@ -53,4 +54,12 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Role)
+ .IsInEnum()
+ .WithErrorCode(DomainErrorCodes.UserInvalidRole)
+ .WithMessage("Role is not a valid role");
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Entities/Entity.cs b/CleanArchitecture.Domain/Entities/Entity.cs
index 395e70e..99a272a 100644
--- a/CleanArchitecture.Domain/Entities/Entity.cs
+++ b/CleanArchitecture.Domain/Entities/Entity.cs
@@ -31,31 +31,4 @@ public abstract class Entity
{
Deleted = false;
}
-
- public override bool Equals(object? obj)
- {
- var compareTo = obj as Entity;
-
- if (ReferenceEquals(this, compareTo))
- {
- return true;
- }
-
- if (compareTo is null)
- {
- return false;
- }
-
- return Id == compareTo.Id;
- }
-
- public override int GetHashCode()
- {
- return GetType().GetHashCode() * 907 + Id.GetHashCode();
- }
-
- public override string ToString()
- {
- return GetType().Name + " [Id=" + Id + "]";
- }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs
index e2c56e9..b92cd3e 100644
--- a/CleanArchitecture.Domain/Entities/User.cs
+++ b/CleanArchitecture.Domain/Entities/User.cs
@@ -1,27 +1,34 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Domain.Entities;
public class User : Entity
{
- public string Email { get; private set; }
- public string GivenName { get; private set; }
- public string Surname { get; private set; }
-
- public string FullName => $"{Surname}, {GivenName}";
-
public User(
Guid id,
string email,
string surname,
- string givenName) : base(id)
+ string givenName,
+ string password,
+ UserRole role) : base(id)
{
Email = email;
GivenName = givenName;
Surname = surname;
+ Password = password;
+ Role = role;
}
-
+
+ public string Email { get; private set; }
+ public string GivenName { get; private set; }
+ public string Surname { get; private set; }
+ public string Password { get; private set; }
+ public UserRole Role { get; private set; }
+
+ public string FullName => $"{Surname}, {GivenName}";
+
[MemberNotNull(nameof(Email))]
public void SetEmail(string email)
{
@@ -38,7 +45,7 @@ public class User : Entity
Email = email;
}
-
+
[MemberNotNull(nameof(GivenName))]
public void SetGivenName(string givenName)
{
@@ -55,7 +62,7 @@ public class User : Entity
GivenName = givenName;
}
-
+
[MemberNotNull(nameof(Surname))]
public void SetSurname(string surname)
{
@@ -72,4 +79,26 @@ public class User : Entity
Surname = surname;
}
+
+ [MemberNotNull(nameof(Password))]
+ public void SetPassword(string password)
+ {
+ if (password == null)
+ {
+ throw new ArgumentNullException(nameof(password));
+ }
+
+ if (password.Length > 100)
+ {
+ throw new ArgumentException(
+ "Password may not be longer than 100 characters");
+ }
+
+ Password = password;
+ }
+
+ public void SetRole(UserRole role)
+ {
+ Role = role;
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Enums/UserRole.cs b/CleanArchitecture.Domain/Enums/UserRole.cs
new file mode 100644
index 0000000..f95d8e1
--- /dev/null
+++ b/CleanArchitecture.Domain/Enums/UserRole.cs
@@ -0,0 +1,7 @@
+namespace CleanArchitecture.Domain.Enums;
+
+public enum UserRole
+{
+ Admin,
+ User
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs
index 045b455..6c486cf 100644
--- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs
+++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs
@@ -10,7 +10,18 @@ public static class DomainErrorCodes
public const string UserSurnameExceedsMaxLength = "USER_SURNAME_EXCEEDS_MAX_LENGTH";
public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH";
public const string UserInvalidEmail = "USER_INVALID_EMAIL";
-
+ public const string UserInvalidRole = "USER_INVALID_ROLE";
+
+ // User Password Validation
+ public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY";
+ public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS";
+ public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS";
+ public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER";
+ public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER";
+ public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER";
+ public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER";
+
// User
public const string UserAlreadyExists = "USER_ALREADY_EXISTS";
+ public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT";
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Errors/ErrorCodes.cs b/CleanArchitecture.Domain/Errors/ErrorCodes.cs
index 056c188..6cc6fd7 100644
--- a/CleanArchitecture.Domain/Errors/ErrorCodes.cs
+++ b/CleanArchitecture.Domain/Errors/ErrorCodes.cs
@@ -4,4 +4,5 @@ public static class ErrorCodes
{
public const string CommitFailed = "COMMIT_FAILED";
public const string ObjectNotFound = "OBJECT_NOT_FOUND";
+ public const string InsufficientPermissions = "UNAUTHORIZED";
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs
index 45428b8..3e29e28 100644
--- a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs
+++ b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs
@@ -8,9 +8,10 @@ namespace CleanArchitecture.Domain.EventHandler;
public sealed class UserEventHandler :
INotificationHandler,
INotificationHandler,
- INotificationHandler
+ INotificationHandler,
+ INotificationHandler
{
- public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken)
+ public Task Handle(PasswordChangedEvent notification, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
@@ -19,7 +20,12 @@ public sealed class UserEventHandler :
{
return Task.CompletedTask;
}
-
+
+ public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
public Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken)
{
return Task.CompletedTask;
diff --git a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs
new file mode 100644
index 0000000..7ebb021
--- /dev/null
+++ b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace CleanArchitecture.Domain.Events.User;
+
+public sealed class PasswordChangedEvent : DomainEvent
+{
+ public PasswordChangedEvent(Guid userId) : base(userId)
+ {
+ UserId = userId;
+ }
+
+ public Guid UserId { get; }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs
index 19f3658..2a67c9b 100644
--- a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs
+++ b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs
@@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User;
public sealed class UserCreatedEvent : DomainEvent
{
- public Guid UserId { get; }
-
public UserCreatedEvent(Guid userId) : base(userId)
{
UserId = userId;
}
+
+ public Guid UserId { get; }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs
index 576bcbf..89fadc1 100644
--- a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs
+++ b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs
@@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User;
public sealed class UserDeletedEvent : DomainEvent
{
- public Guid UserId { get; }
-
public UserDeletedEvent(Guid userId) : base(userId)
{
UserId = userId;
}
+
+ public Guid UserId { get; }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs
index 996c7e6..92a09cf 100644
--- a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs
+++ b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs
@@ -4,10 +4,10 @@ namespace CleanArchitecture.Domain.Events.User;
public sealed class UserUpdatedEvent : DomainEvent
{
- public Guid UserId { get; }
-
public UserUpdatedEvent(Guid userId) : base(userId)
{
UserId = userId;
}
+
+ public Guid UserId { get; }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs
index 64cd05d..4c3b2b9 100644
--- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs
+++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs
@@ -1,8 +1,11 @@
+using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
+using CleanArchitecture.Domain.Commands.Users.LoginUser;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.EventHandler;
using CleanArchitecture.Domain.Events.User;
+using CleanArchitecture.Domain.Interfaces;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
@@ -16,17 +19,29 @@ public static class ServiceCollectionExtension
services.AddScoped, CreateUserCommandHandler>();
services.AddScoped, UpdateUserCommandHandler>();
services.AddScoped, DeleteUserCommandHandler>();
-
+ services.AddScoped, ChangePasswordCommandHandler>();
+ services.AddScoped, LoginUserCommandHandler>();
+
+
return services;
}
-
+
public static IServiceCollection AddNotificationHandlers(this IServiceCollection services)
{
// User
services.AddScoped, UserEventHandler>();
services.AddScoped, UserEventHandler>();
services.AddScoped, UserEventHandler>();
-
+ services.AddScoped, UserEventHandler>();
+
+ return services;
+ }
+
+ public static IServiceCollection AddApiUser(this IServiceCollection services)
+ {
+ // User
+ services.AddScoped();
+
return services;
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs
new file mode 100644
index 0000000..c94ff0d
--- /dev/null
+++ b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs
@@ -0,0 +1,35 @@
+using System.Text.RegularExpressions;
+using CleanArchitecture.Domain.Errors;
+using FluentValidation;
+
+namespace CleanArchitecture.Domain.Extensions.Validation;
+
+public static class CustomValidator
+{
+ public static IRuleBuilderOptions StringMustBeBase64(this IRuleBuilder ruleBuilder)
+ {
+ return ruleBuilder.Must(x => IsBase64String(x));
+ }
+
+ private static bool IsBase64String(string base64)
+ {
+ base64 = base64.Trim();
+ return base64.Length % 4 == 0 && Regex.IsMatch(base64, @"^[a-zA-Z0-9\+/]*={0,3}$", RegexOptions.None);
+ }
+
+ public static IRuleBuilder Password(
+ this IRuleBuilder ruleBuilder,
+ int minLength = 8,
+ int maxLength = 50)
+ {
+ var options = ruleBuilder
+ .NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword)
+ .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.UserShortPassword)
+ .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.UserLongPassword)
+ .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.UserUppercaseLetterPassword)
+ .Matches("[a-z]").WithErrorCode(DomainErrorCodes.UserLowercaseLetterPassword)
+ .Matches("[0-9]").WithErrorCode(DomainErrorCodes.UserNumberPassword)
+ .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.UserSpecialCharPassword);
+ return options;
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Interfaces/IUser.cs b/CleanArchitecture.Domain/Interfaces/IUser.cs
new file mode 100644
index 0000000..26cbe1a
--- /dev/null
+++ b/CleanArchitecture.Domain/Interfaces/IUser.cs
@@ -0,0 +1,11 @@
+using System;
+using CleanArchitecture.Domain.Enums;
+
+namespace CleanArchitecture.Domain.Interfaces;
+
+public interface IUser
+{
+ string Name { get; }
+ Guid GetUserId();
+ UserRole GetUserRole();
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Notifications/DomainNotification.cs b/CleanArchitecture.Domain/Notifications/DomainNotification.cs
index c9b2719..e57a891 100644
--- a/CleanArchitecture.Domain/Notifications/DomainNotification.cs
+++ b/CleanArchitecture.Domain/Notifications/DomainNotification.cs
@@ -4,11 +4,6 @@ namespace CleanArchitecture.Domain.Notifications;
public sealed class DomainNotification : DomainEvent
{
- public string Key { get; private set; }
- public string Value { get; private set; }
- public string Code { get; private set; }
- public object? Data { get; set; }
-
public DomainNotification(
string key,
string value,
@@ -23,4 +18,9 @@ public sealed class DomainNotification : DomainEvent
Data = data;
}
+
+ public string Key { get; }
+ public string Value { get; }
+ public string Code { get; }
+ public object? Data { get; set; }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs b/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs
index d1e579d..59f9235 100644
--- a/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs
+++ b/CleanArchitecture.Domain/Notifications/DomainNotificationHandler.cs
@@ -15,18 +15,18 @@ public class DomainNotificationHandler : INotificationHandler();
}
- public virtual List GetNotifications()
- {
- return _notifications;
- }
-
public Task Handle(DomainNotification notification, CancellationToken cancellationToken = default)
{
_notifications.Add(notification);
return Task.CompletedTask;
}
-
+
+ public virtual List GetNotifications()
+ {
+ return _notifications;
+ }
+
public virtual bool HasNotifications()
{
return GetNotifications().Any();
diff --git a/CleanArchitecture.Domain/Settings/TokenSettings.cs b/CleanArchitecture.Domain/Settings/TokenSettings.cs
new file mode 100644
index 0000000..96cbe01
--- /dev/null
+++ b/CleanArchitecture.Domain/Settings/TokenSettings.cs
@@ -0,0 +1,8 @@
+namespace CleanArchitecture.Domain.Settings;
+
+public sealed class TokenSettings
+{
+ public string Issuer { get; set; } = null!;
+ public string Audience { get; set; } = null!;
+ public string Secret { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj
index 574a54c..45957b8 100644
--- a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj
+++ b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -23,7 +23,7 @@
-
+
diff --git a/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs b/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs
index 95148af..15a24ed 100644
--- a/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs
+++ b/CleanArchitecture.Infrastructure.Tests/DomainNotificationHandlerTests.cs
@@ -16,9 +16,9 @@ public sealed class DomainNotificationHandlerTests
[Fact]
public void Should_Handle_DomainNotification()
{
- string key = "Key";
- string value = "Value";
- string code = "Code";
+ const string key = "Key";
+ const string value = "Value";
+ const string code = "Code";
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
@@ -29,9 +29,9 @@ public sealed class DomainNotificationHandlerTests
[Fact]
public void Should_Handle_DomainNotification_Overload()
{
- string key = "Key";
- string value = "Value";
- string code = "Code";
+ const string key = "Key";
+ const string value = "Value";
+ const string code = "Code";
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
@@ -42,9 +42,9 @@ public sealed class DomainNotificationHandlerTests
[Fact]
public void DomainNotification_HasNotifications_After_Handling_One()
{
- string key = "Key";
- string value = "Value";
- string code = "Code";
+ const string key = "Key";
+ const string value = "Value";
+ const string code = "Code";
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
@@ -60,4 +60,4 @@ public sealed class DomainNotificationHandlerTests
domainNotificationHandler.HasNotifications().Should().BeFalse();
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs b/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs
index d942a92..14d90b6 100644
--- a/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs
+++ b/CleanArchitecture.Infrastructure.Tests/DomainNotificationTests.cs
@@ -10,9 +10,9 @@ public sealed class DomainNotificationTests
[Fact]
public void Should_Create_DomainNotification_Instance()
{
- string key = "Key";
- string value = "Value";
- string code = "Code";
+ const string key = "Key";
+ const string value = "Value";
+ const string code = "Code";
var domainNotification = new DomainNotification(
key, value, code);
@@ -26,9 +26,9 @@ public sealed class DomainNotificationTests
[Fact]
public void Should_Create_DomainNotification_Overload_Instance()
{
- string key = "Key";
- string value = "Value";
- string code = "Code";
+ const string key = "Key";
+ const string value = "Value";
+ const string code = "Code";
var domainNotification = new DomainNotification(
key, value, code);
@@ -38,4 +38,4 @@ public sealed class DomainNotificationTests
domainNotification.Code.Should().Be(code);
domainNotification.Should().NotBe(default(Guid));
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs
index f3ed4d8..382ccc2 100644
--- a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs
+++ b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs
@@ -19,9 +19,9 @@ public sealed class InMemoryBusTests
var inMemoryBus = new InMemoryBus(mediator.Object);
- var key = "Key";
- var value = "Value";
- var code = "Code";
+ const string key = "Key";
+ const string value = "Value";
+ const string code = "Code";
var domainEvent = new DomainNotification(key, value, code);
diff --git a/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs b/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs
index 2a0e51f..5700ae1 100644
--- a/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs
+++ b/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs
@@ -19,18 +19,18 @@ public sealed class UnitOfWorkTests
var options = new DbContextOptionsBuilder();
var dbContextMock = new Mock(options.Options);
var loggerMock = new Mock>>();
-
+
dbContextMock
.Setup(x => x.SaveChangesAsync(CancellationToken.None))
.Returns(Task.FromResult(1));
-
+
var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object);
-
+
var result = await unitOfWork.CommitAsync();
result.Should().BeTrue();
}
-
+
[Fact]
public async Task Should_Commit_Async_Returns_False()
{
@@ -45,7 +45,7 @@ public sealed class UnitOfWorkTests
var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object);
var result = await unitOfWork.CommitAsync();
-
+
result.Should().BeFalse();
}
diff --git a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj
index 6b37ff6..01399f8 100644
--- a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj
+++ b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj
@@ -6,18 +6,18 @@
-
+
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs
index 2723e12..4763c9c 100644
--- a/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs
+++ b/CleanArchitecture.Infrastructure/Configurations/UserConfiguration.cs
@@ -1,4 +1,6 @@
+using System;
using CleanArchitecture.Domain.Entities;
+using CleanArchitecture.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -22,5 +24,19 @@ public sealed class UserConfiguration : IEntityTypeConfiguration
.Property(user => user.Surname)
.IsRequired()
.HasMaxLength(100);
+
+ builder
+ .Property(user => user.Password)
+ .IsRequired()
+ .HasMaxLength(128);
+
+ builder.HasData(new User(
+ Guid.NewGuid(),
+ "admin@email.com",
+ "Admin",
+ "User",
+ // !Password123#
+ "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2",
+ UserRole.Admin));
}
}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs
index f4b4323..4280c4d 100644
--- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs
+++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs
@@ -6,12 +6,12 @@ namespace CleanArchitecture.Infrastructure.Database;
public class ApplicationDbContext : DbContext
{
- public DbSet Users { get; set; } = null!;
-
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
+ public DbSet Users { get; set; } = null!;
+
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfiguration(new UserConfiguration());
diff --git a/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs b/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs
index 9192d48..869c1ea 100644
--- a/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs
+++ b/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs
@@ -18,4 +18,4 @@ public static class DbContextExtension
context.Database.Migrate();
}
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs
index ff72bb8..a7a68a8 100644
--- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs
+++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs
@@ -22,4 +22,4 @@ public static class ServiceCollectionExtensions
return services;
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure/InMemoryBus.cs b/CleanArchitecture.Infrastructure/InMemoryBus.cs
index 5896d83..2df646c 100644
--- a/CleanArchitecture.Infrastructure/InMemoryBus.cs
+++ b/CleanArchitecture.Infrastructure/InMemoryBus.cs
@@ -14,7 +14,7 @@ public sealed class InMemoryBus : IMediatorHandler
{
_mediator = mediator;
}
-
+
public Task QueryAsync(IRequest query)
{
return _mediator.Send(query);
diff --git a/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs
new file mode 100644
index 0000000..72080c0
--- /dev/null
+++ b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs
@@ -0,0 +1,82 @@
+//
+using System;
+using CleanArchitecture.Infrastructure.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CleanArchitecture.Infrastructure.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20230320204057_AddUserRoleAndPassword")]
+ partial class AddUserRoleAndPassword
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.4")
+ .HasAnnotation("Proxies:ChangeTracking", false)
+ .HasAnnotation("Proxies:CheckEquality", false)
+ .HasAnnotation("Proxies:LazyLoading", true)
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Deleted")
+ .HasColumnType("bit");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("nvarchar(320)");
+
+ b.Property("GivenName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.Property("Surname")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"),
+ Deleted = false,
+ Email = "admin@email.com",
+ GivenName = "User",
+ Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2",
+ Role = 0,
+ Surname = "Admin"
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs
new file mode 100644
index 0000000..438e65c
--- /dev/null
+++ b/CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CleanArchitecture.Infrastructure.Migrations
+{
+ ///
+ public partial class AddUserRoleAndPassword : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Password",
+ table: "Users",
+ type: "nvarchar(128)",
+ maxLength: 128,
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn(
+ name: "Role",
+ table: "Users",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.InsertData(
+ table: "Users",
+ columns: new[] { "Id", "Deleted", "Email", "GivenName", "Password", "Role", "Surname" },
+ values: new object[] { new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"), false, "admin@email.com", "User", "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", 0, "Admin" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DeleteData(
+ table: "Users",
+ keyColumn: "Id",
+ keyValue: new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"));
+
+ migrationBuilder.DropColumn(
+ name: "Password",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "Role",
+ table: "Users");
+ }
+ }
+}
diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
index dcc40d6..249004e 100644
--- a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -17,7 +17,7 @@ namespace CleanArchitecture.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "7.0.3")
+ .HasAnnotation("ProductVersion", "7.0.4")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
@@ -44,6 +44,14 @@ namespace CleanArchitecture.Infrastructure.Migrations
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
b.Property("Surname")
.IsRequired()
.HasMaxLength(100)
@@ -52,6 +60,18 @@ namespace CleanArchitecture.Infrastructure.Migrations
b.HasKey("Id");
b.ToTable("Users");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("3fc7aacd-41cc-4ca2-b842-32edcd0782d5"),
+ Deleted = false,
+ Email = "admin@email.com",
+ GivenName = "User",
+ Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2",
+ Role = 0,
+ Surname = "Admin"
+ });
});
#pragma warning restore 612, 618
}
diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs
index 52b768d..db13d69 100644
--- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs
+++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs
@@ -50,19 +50,6 @@ public class BaseRepository : IRepository where TEntity : Enti
return await DbSet.FindAsync(id);
}
- public int SaveChanges()
- {
- return _dbContext.SaveChanges();
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- _dbContext.Dispose();
- }
- }
-
public virtual void Update(TEntity entity)
{
DbSet.Update(entity);
@@ -72,7 +59,7 @@ public class BaseRepository : IRepository where TEntity : Enti
{
return DbSet.AnyAsync(entity => entity.Id == id);
}
-
+
public void Remove(TEntity entity, bool hardDelete = false)
{
if (hardDelete)
@@ -86,4 +73,16 @@ public class BaseRepository : IRepository where TEntity : Enti
}
}
+ public int SaveChanges()
+ {
+ return _dbContext.SaveChanges();
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _dbContext.Dispose();
+ }
+ }
}
\ No newline at end of file
diff --git a/CleanArchitecture.Infrastructure/UnitOfWork.cs b/CleanArchitecture.Infrastructure/UnitOfWork.cs
index 97d88a9..735590e 100644
--- a/CleanArchitecture.Infrastructure/UnitOfWork.cs
+++ b/CleanArchitecture.Infrastructure/UnitOfWork.cs
@@ -45,4 +45,4 @@ public sealed class UnitOfWork : IUnitOfWork where TContext : DbContex
_context.Dispose();
}
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
index 5f128a0..5768a6d 100644
--- a/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
+++ b/CleanArchitecture.IntegrationTests/CleanArchitecture.IntegrationTests.csproj
@@ -8,14 +8,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -27,8 +27,8 @@
-
-
+
+
diff --git a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs
index fd8791b..a06ecfa 100644
--- a/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs
+++ b/CleanArchitecture.IntegrationTests/Controller/UserControllerTests.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels.Users;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures;
using FluentAssertions;
@@ -23,28 +24,17 @@ public sealed class UserControllerTests : IClassFixture
_fixture = fixture;
}
- [Fact, Priority(0)]
- public async Task Should_Get_No_User()
- {
- var response = await _fixture.ServerClient.GetAsync("user");
-
- response.StatusCode.Should().Be(HttpStatusCode.OK);
-
- var message = await response.Content.ReadAsJsonAsync>();
-
- message?.Data.Should().NotBeNull();
-
- var content = message!.Data!;
-
- content.Should().BeNullOrEmpty();
- }
-
- [Fact, Priority(5)]
+ [Fact]
+ [Priority(0)]
public async Task Should_Create_User()
{
- var user = new CreateUserViewModel("test@email.com", "Test", "Email");
+ var user = new CreateUserViewModel(
+ _fixture.CreatedUserEmail,
+ "Test",
+ "Email",
+ _fixture.CreatedUserPassword);
- var response = await _fixture.ServerClient.PostAsJsonAsync("user", user);
+ var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -55,10 +45,31 @@ public sealed class UserControllerTests : IClassFixture
_fixture.CreatedUserId = message!.Data;
}
- [Fact, Priority(10)]
+ [Fact]
+ [Priority(5)]
+ public async Task Should_Login_User()
+ {
+ var user = new LoginUserViewModel(
+ _fixture.CreatedUserEmail,
+ _fixture.CreatedUserPassword);
+
+ var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", user);
+
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var message = await response.Content.ReadAsJsonAsync();
+
+ message?.Data.Should().NotBeEmpty();
+
+ _fixture.CreatedUserToken = message!.Data!;
+ _fixture.EnableAuthentication();
+ }
+
+ [Fact]
+ [Priority(10)]
public async Task Should_Get_Created_Users()
{
- var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId);
+ var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -74,16 +85,38 @@ public sealed class UserControllerTests : IClassFixture
content.GivenName.Should().Be("Email");
}
- [Fact, Priority(15)]
+ [Fact]
+ [Priority(10)]
+ public async Task Should_Get_The_Current_Active_Users()
+ {
+ var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me");
+
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var message = await response.Content.ReadAsJsonAsync();
+
+ message?.Data.Should().NotBeNull();
+
+ var content = message!.Data!;
+
+ content.Id.Should().Be(_fixture.CreatedUserId);
+ content.Email.Should().Be("test@email.com");
+ content.Surname.Should().Be("Test");
+ content.GivenName.Should().Be("Email");
+ }
+
+ [Fact]
+ [Priority(15)]
public async Task Should_Update_User()
{
var user = new UpdateUserViewModel(
_fixture.CreatedUserId,
"newtest@email.com",
"NewTest",
- "NewEmail");
+ "NewEmail",
+ UserRole.User);
- var response = await _fixture.ServerClient.PutAsJsonAsync("user", user);
+ var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/user", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -96,10 +129,11 @@ public sealed class UserControllerTests : IClassFixture
content.Should().BeEquivalentTo(user);
}
- [Fact, Priority(20)]
+ [Fact]
+ [Priority(20)]
public async Task Should_Get_Updated_Users()
{
- var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId);
+ var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -113,12 +147,49 @@ public sealed class UserControllerTests : IClassFixture
content.Email.Should().Be("newtest@email.com");
content.Surname.Should().Be("NewTest");
content.GivenName.Should().Be("NewEmail");
+
+ _fixture.CreatedUserEmail = content.Email;
}
- [Fact, Priority(25)]
- public async Task Should_Get_One_User()
+ [Fact]
+ [Priority(25)]
+ public async Task Should_Change_User_Password()
{
- var response = await _fixture.ServerClient.GetAsync("user");
+ var user = new ChangePasswordViewModel(
+ _fixture.CreatedUserPassword,
+ _fixture.CreatedUserPassword + "1");
+
+ var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/changePassword", user);
+
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var message = await response.Content.ReadAsJsonAsync();
+
+ message?.Data.Should().NotBeNull();
+
+ var content = message!.Data;
+
+ content.Should().BeEquivalentTo(user);
+
+ // Verify the user can login with the new password
+ var login = new LoginUserViewModel(
+ _fixture.CreatedUserEmail,
+ _fixture.CreatedUserPassword + "1");
+
+ var loginResponse = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", login);
+
+ loginResponse.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var loginMessage = await loginResponse.Content.ReadAsJsonAsync();
+
+ loginMessage?.Data.Should().NotBeEmpty();
+ }
+
+ [Fact]
+ [Priority(30)]
+ public async Task Should_Get_All_User()
+ {
+ var response = await _fixture.ServerClient.GetAsync("/api/v1/user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -128,17 +199,28 @@ public sealed class UserControllerTests : IClassFixture
var content = message!.Data!.ToList();
- content.Should().ContainSingle();
- content.First().Id.Should().Be(_fixture.CreatedUserId);
- content.First().Email.Should().Be("newtest@email.com");
- content.First().Surname.Should().Be("NewTest");
- content.First().GivenName.Should().Be("NewEmail");
+ content.Count.Should().Be(2);
+
+ var currentUser = content.First(x => x.Id == _fixture.CreatedUserId);
+
+ currentUser.Id.Should().Be(_fixture.CreatedUserId);
+ currentUser.Role.Should().Be(UserRole.User);
+ currentUser.Email.Should().Be("newtest@email.com");
+ currentUser.Surname.Should().Be("NewTest");
+ currentUser.GivenName.Should().Be("NewEmail");
+
+ var adminUser = content.First(x => x.Role == UserRole.Admin);
+
+ adminUser.Email.Should().Be("admin@email.com");
+ adminUser.Surname.Should().Be("Admin");
+ adminUser.GivenName.Should().Be("User");
}
- [Fact, Priority(30)]
+ [Fact]
+ [Priority(35)]
public async Task Should_Delete_User()
{
- var response = await _fixture.ServerClient.DeleteAsync("user/" + _fixture.CreatedUserId);
+ var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -149,20 +231,4 @@ public sealed class UserControllerTests : IClassFixture
var content = message!.Data;
content.Should().Be(_fixture.CreatedUserId);
}
-
- [Fact, Priority(35)]
- public async Task Should_Get_No_User_Again()
- {
- var response = await _fixture.ServerClient.GetAsync("user");
-
- response.StatusCode.Should().Be(HttpStatusCode.OK);
-
- var message = await response.Content.ReadAsJsonAsync>();
-
- message?.Data.Should().NotBeNull();
-
- var content = message!.Data!;
-
- content.Should().BeNullOrEmpty();
- }
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs
index 8cb7fad..d81e2c0 100644
--- a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs
+++ b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs
@@ -2,28 +2,29 @@
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
-using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.IntegrationTests.Extensions;
public static class FunctionalTestsServiceCollectionExtensions
{
- public static IServiceCollection SetupTestDatabase(this IServiceCollection services, DbConnection connection) where TContext : DbContext
+ public static IServiceCollection SetupTestDatabase(this IServiceCollection services,
+ DbConnection connection) where TContext : DbContext
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions));
if (descriptor != null)
services.Remove(descriptor);
services.AddScoped(p =>
- DbContextOptionsFactory(
- p,
- (_, options) => options
- .ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
- .UseLazyLoadingProxies()
- .UseSqlite(connection)));
+ DbContextOptionsFactory(
+ p,
+ (_, options) => options
+ .ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
+ .UseLazyLoadingProxies()
+ .UseSqlite(connection)));
return services;
}
@@ -42,4 +43,4 @@ public static class FunctionalTestsServiceCollectionExtensions
return builder.Options;
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs b/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs
index f765a9c..3ac2378 100644
--- a/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs
+++ b/CleanArchitecture.IntegrationTests/Extensions/HttpExtensions.cs
@@ -10,7 +10,7 @@ public static class HttpExtensions
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
- PropertyNameCaseInsensitive = true,
+ PropertyNameCaseInsensitive = true
};
private static T? Deserialize(string json)
@@ -55,4 +55,4 @@ public static class HttpExtensions
return httpClient.PutAsync(url, content);
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs
index 2d4994e..cfe5a52 100644
--- a/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs
+++ b/CleanArchitecture.IntegrationTests/Fixtures/TestFixtureBase.cs
@@ -9,8 +9,6 @@ namespace CleanArchitecture.IntegrationTests.Fixtures;
public class TestFixtureBase
{
- public HttpClient ServerClient { get; }
-
public TestFixtureBase()
{
var projectDir = Directory.GetCurrentDirectory();
@@ -25,6 +23,8 @@ public class TestFixtureBase
ServerClient.Timeout = TimeSpan.FromMinutes(5);
}
+ public HttpClient ServerClient { get; }
+
protected virtual void SeedTestData(ApplicationDbContext context)
{
}
@@ -35,4 +35,4 @@ public class TestFixtureBase
IServiceProvider scopedServices)
{
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs
index 5d31bea..5769927 100644
--- a/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs
+++ b/CleanArchitecture.IntegrationTests/Fixtures/UserTestFixture.cs
@@ -5,4 +5,12 @@ namespace CleanArchitecture.IntegrationTests.Fixtures;
public sealed class UserTestFixture : TestFixtureBase
{
public Guid CreatedUserId { get; set; }
-}
+ public string CreatedUserEmail { get; set; } = "test@email.com";
+ public string CreatedUserPassword { get; set; } = "z8]tnayvd5FNLU9:]AQm";
+ public string CreatedUserToken { get; set; } = string.Empty;
+
+ public void EnableAuthentication()
+ {
+ ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}");
+ }
+}
\ No newline at end of file
diff --git a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs
index 0e9f243..b35bbdf 100644
--- a/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs
+++ b/CleanArchitecture.IntegrationTests/Infrastructure/CleanArchitectureWebApplicationFactory.cs
@@ -18,11 +18,11 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
ServiceProvider serviceProvider,
IServiceProvider scopedServices);
- private readonly SqliteConnection _connection = new($"DataSource=:memory:");
-
private readonly AddCustomSeedDataHandler? _addCustomSeedDataHandler;
- private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler;
+
+ private readonly SqliteConnection _connection = new("DataSource=:memory:");
private readonly string? _environment;
+ private readonly RegisterCustomServicesHandler? _registerCustomServicesHandler;
public CleanArchitectureWebApplicationFactory(
AddCustomSeedDataHandler? addCustomSeedDataHandler,
@@ -51,7 +51,7 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
var sp = services.BuildServiceProvider();
- using IServiceScope scope = sp.CreateScope();
+ using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var applicationDbContext = scopedServices.GetRequiredService();
@@ -62,4 +62,4 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
_registerCustomServicesHandler?.Invoke(services, sp, scopedServices);
});
}
-}
+}
\ No newline at end of file
diff --git a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj
index 1f914a1..de1ac9c 100644
--- a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj
+++ b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj
@@ -6,19 +6,19 @@
-
-
+
+
-
-
+
+
-
-
-
+
+
+
diff --git a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj
index 6d11a38..b6443c4 100644
--- a/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj
+++ b/CleanArchitecture.gRPC.Tests/CleanArchitecture.gRPC.Tests.csproj
@@ -8,11 +8,11 @@
-
-
-
-
-
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs
index 63dd2bf..5b04bef 100644
--- a/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs
+++ b/CleanArchitecture.gRPC.Tests/Fixtures/UserTestsFixture.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Domain.Entities;
+using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MockQueryable.Moq;
using Moq;
@@ -10,19 +11,31 @@ namespace CleanArchitecture.gRPC.Tests.Fixtures;
public sealed class UserTestsFixture
{
- private Mock UserRepository { get; } = new ();
-
- public UsersApiImplementation UsersApiImplementation { get; }
-
- public IEnumerable ExistingUsers { get; }
-
public UserTestsFixture()
{
- ExistingUsers = new List()
+ ExistingUsers = new List
{
- new (Guid.NewGuid(), "test@test.de", "Test First Name", "Test Last Name"),
- new (Guid.NewGuid(), "email@Email.de", "Email First Name", "Email Last Name"),
- new (Guid.NewGuid(), "user@user.de", "User First Name", "User Last Name"),
+ new(
+ Guid.NewGuid(),
+ "test@test.de",
+ "Test First Name",
+ "Test Last Name",
+ "Test Password",
+ UserRole.User),
+ new(
+ Guid.NewGuid(),
+ "email@Email.de",
+ "Email First Name",
+ "Email Last Name",
+ "Email Password",
+ UserRole.Admin),
+ new(
+ Guid.NewGuid(),
+ "user@user.de",
+ "User First Name",
+ "User Last Name",
+ "User Password",
+ UserRole.User)
};
var queryable = ExistingUsers.AsQueryable().BuildMock();
@@ -33,4 +46,10 @@ public sealed class UserTestsFixture
UsersApiImplementation = new UsersApiImplementation(UserRepository.Object);
}
+
+ private Mock UserRepository { get; } = new();
+
+ public UsersApiImplementation UsersApiImplementation { get; }
+
+ public IEnumerable ExistingUsers { get; }
}
\ No newline at end of file
diff --git a/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj b/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj
index 82b6f59..1268e7f 100644
--- a/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj
+++ b/CleanArchitecture.gRPC/CleanArchitecture.gRPC.csproj
@@ -6,12 +6,12 @@
-
-
+
+
-
+
diff --git a/CleanArchitecture.sln b/CleanArchitecture.sln
index 31560fb..1209ff0 100644
--- a/CleanArchitecture.sln
+++ b/CleanArchitecture.sln
@@ -1,26 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Api", "CleanArchitecture.Api\CleanArchitecture.Api.csproj", "{CD720672-0ED9-4FDD-AD69-A416CB394318}"
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33502.453
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Api", "CleanArchitecture.Api\CleanArchitecture.Api.csproj", "{CD720672-0ED9-4FDD-AD69-A416CB394318}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Application", "CleanArchitecture.Application\CleanArchitecture.Application.csproj", "{859B50AF-9C8D-4489-B64A-EEBDF756A012}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Application", "CleanArchitecture.Application\CleanArchitecture.Application.csproj", "{859B50AF-9C8D-4489-B64A-EEBDF756A012}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Domain", "CleanArchitecture.Domain\CleanArchitecture.Domain.csproj", "{12C5BEEF-9BFD-450A-8627-6205702CA32B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Domain", "CleanArchitecture.Domain\CleanArchitecture.Domain.csproj", "{12C5BEEF-9BFD-450A-8627-6205702CA32B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastructure", "CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj", "{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Infrastructure", "CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj", "{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Application.Tests", "CleanArchitecture.Application.Tests\CleanArchitecture.Application.Tests.csproj", "{6794B922-2AFD-4187-944D-7984B9973259}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Application.Tests", "CleanArchitecture.Application.Tests\CleanArchitecture.Application.Tests.csproj", "{6794B922-2AFD-4187-944D-7984B9973259}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Domain.Tests", "CleanArchitecture.Domain.Tests\CleanArchitecture.Domain.Tests.csproj", "{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Domain.Tests", "CleanArchitecture.Domain.Tests\CleanArchitecture.Domain.Tests.csproj", "{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastructure.Tests", "CleanArchitecture.Infrastructure.Tests\CleanArchitecture.Infrastructure.Tests.csproj", "{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Infrastructure.Tests", "CleanArchitecture.Infrastructure.Tests\CleanArchitecture.Infrastructure.Tests.csproj", "{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.gRPC", "CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj", "{7A6353A9-B60C-4B13-A849-D21B315047EE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.gRPC", "CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj", "{7A6353A9-B60C-4B13-A849-D21B315047EE}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Proto", "CleanArchitecture.Proto\CleanArchitecture.Proto.csproj", "{5F978903-7A7A-45C2-ABE0-C2906ECD326B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Proto", "CleanArchitecture.Proto\CleanArchitecture.Proto.csproj", "{5F978903-7A7A-45C2-ABE0-C2906ECD326B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.gRPC.Tests", "CleanArchitecture.gRPC.Tests\CleanArchitecture.gRPC.Tests.csproj", "{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.gRPC.Tests", "CleanArchitecture.gRPC.Tests\CleanArchitecture.gRPC.Tests.csproj", "{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -73,4 +78,14 @@ Global
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {6794B922-2AFD-4187-944D-7984B9973259} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
+ {E1F25916-EBBE-4CBD-99A2-1EB2F604D55C} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
+ {EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
+ {39732BD4-909F-410C-8737-1F9FE3E269A7} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
+ {E3A836DD-85DB-44FD-BC19-DDFE111D9EB0} = {D3DF9DF5-BD7D-48BC-8BE6-DBD0FABB8B45}
+ EndGlobalSection
EndGlobal
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..db05150
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
+WORKDIR /app
+
+# copy csproj and restore as distinct layers
+COPY . .
+RUN dotnet restore
+
+# copy everything else and build app
+COPY CleanArchitecture.Api/. ./CleanArchitecture.Api/
+WORKDIR /app/CleanArchitecture.Api
+RUN dotnet publish -c Release -o out
+
+FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime
+WORKDIR /app
+COPY --from=build /app/CleanArchitecture.Api/out ./
+ENTRYPOINT ["dotnet", "CleanArchitecture.Api.dll"]
diff --git a/Readme.md b/Readme.md
index 2455294..7f36628 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,4 +1,7 @@
# Clean Architecture Dotnet 7 API Project
+
+
+
This repository contains a sample API project built using the Clean Architecture principles, Onion Architecture, MediatR, and Entity Framework. The project also includes unit tests for all layers and integration tests using xUnit.
## Project Structure
diff --git a/Todo.txt b/Todo.txt
deleted file mode 100644
index 3931b92..0000000
--- a/Todo.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-- Remove warnings and apply suggestions
-- Add authentication and authorization