mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-07-04 21:14:00 +00:00
Merge pull request #1 from alex289/feature/authentication
Add user authentication
This commit is contained in:
commit
8cd79c37fe
@ -1,21 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>64377c40-44d6-4989-9662-5d778f8b3b92</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj"/>
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
|
||||
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj"/>
|
||||
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<IEnumerable<UserViewModel>>))]
|
||||
public async Task<IActionResult> 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<UserViewModel>))]
|
||||
public async Task<IActionResult> 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<UserViewModel>))]
|
||||
public async Task<IActionResult> GetCurrentUserAsync()
|
||||
{
|
||||
var user = await _userService.GetCurrentUserAsync();
|
||||
return Response(user);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[SwaggerOperation("Create a new user")]
|
||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
|
||||
public async Task<IActionResult> 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<Guid>))]
|
||||
public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid id)
|
||||
{
|
||||
await _userService.DeleteUserAsync(id);
|
||||
return Response(id);
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpPut]
|
||||
[SwaggerOperation("Update a user")]
|
||||
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))]
|
||||
public async Task<IActionResult> 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<ChangePasswordViewModel>))]
|
||||
public async Task<IActionResult> 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<string>))]
|
||||
public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel)
|
||||
{
|
||||
var token = await _userService.LoginUserAsync(viewModel);
|
||||
return Response(token);
|
||||
}
|
||||
}
|
@ -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"]
|
@ -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<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
@ -24,38 +70,72 @@ builder.Services.AddDbContext<ApplicationDbContext>(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<TokenSettings>()
|
||||
.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<UsersApiImplementation>();
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
ApplicationDbContext appDbContext = services.GetRequiredService<ApplicationDbContext>();
|
||||
var appDbContext = services.GetRequiredService<ApplicationDbContext>();
|
||||
|
||||
appDbContext.EnsureMigrationsApplied();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
// Needed for integration tests webapplication factory
|
||||
public partial class Program { }
|
||||
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
|
||||
{
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="MockQueryable.Moq" Version="7.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/>
|
||||
<PackageReference Include="MockQueryable.Moq" Version="7.0.0"/>
|
||||
<PackageReference Include="Moq" Version="4.18.4"/>
|
||||
<PackageReference Include="xunit" Version="2.4.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -24,8 +24,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj"/>
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -7,7 +7,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries;
|
||||
public class QueryHandlerBaseFixture
|
||||
{
|
||||
public Mock<IMediatorHandler> Bus { get; } = new();
|
||||
|
||||
|
||||
public QueryHandlerBaseFixture VerifyExistingNotification(string key, string errorCode, string message)
|
||||
{
|
||||
Bus.Verify(
|
||||
|
@ -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<IUserRepository>();
|
||||
|
||||
Handler = new GetAllUsersQueryHandler(UserRepository.Object);
|
||||
}
|
||||
|
||||
private Mock<IUserRepository> 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<User>(() =>
|
||||
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<User>(() =>
|
||||
new User(
|
||||
ExistingUserId,
|
||||
"max@mustermann.com",
|
||||
"Max",
|
||||
"Mustermann"));
|
||||
new User(
|
||||
ExistingUserId,
|
||||
"max@mustermann.com",
|
||||
"Max",
|
||||
"Mustermann",
|
||||
"Password",
|
||||
UserRole.User));
|
||||
|
||||
user.Object.Delete();
|
||||
|
||||
|
@ -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<IUserRepository>();
|
||||
|
||||
Handler = new GetUserByIdQueryHandler(UserRepository.Object, Bus.Object);
|
||||
}
|
||||
|
||||
private Mock<IUserRepository> 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<User>(() =>
|
||||
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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -6,11 +6,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -14,15 +14,15 @@ public static class ServiceCollectionExtension
|
||||
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
|
||||
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -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<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted);
|
||||
public Task<UserViewModel?> GetCurrentUserAsync();
|
||||
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
|
||||
public Task<Guid> CreateUserAsync(CreateUserViewModel user);
|
||||
public Task UpdateUserAsync(UpdateUserViewModel user);
|
||||
public Task DeleteUserAsync(Guid userId);
|
||||
public Task ChangePasswordAsync(ChangePasswordViewModel viewModel);
|
||||
public Task<string> LoginUserAsync(LoginUserViewModel viewModel);
|
||||
}
|
@ -4,4 +4,4 @@ using MediatR;
|
||||
|
||||
namespace CleanArchitecture.Application.Queries.Users.GetAll;
|
||||
|
||||
public sealed record GetAllUsersQuery : IRequest<IEnumerable<UserViewModel>>;
|
||||
public sealed record GetAllUsersQuery : IRequest<IEnumerable<UserViewModel>>;
|
@ -4,4 +4,4 @@ using MediatR;
|
||||
|
||||
namespace CleanArchitecture.Application.Queries.Users.GetUserById;
|
||||
|
||||
public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest<UserViewModel?>;
|
||||
public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest<UserViewModel?>;
|
@ -13,8 +13,8 @@ namespace CleanArchitecture.Application.Queries.Users.GetUserById;
|
||||
public sealed class GetUserByIdQueryHandler :
|
||||
IRequestHandler<GetUserByIdQuery, UserViewModel?>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<UserViewModel?> 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<UserViewModel?> GetCurrentUserAsync()
|
||||
{
|
||||
return await _bus.QueryAsync(new GetUserByIdQuery(_user.GetUserId(), false));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()
|
||||
{
|
||||
return await _bus.QueryAsync(new GetAllUsersQuery());
|
||||
}
|
||||
|
||||
|
||||
public async Task<Guid> 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<string> LoginUserAsync(LoginUserViewModel viewModel)
|
||||
{
|
||||
return await _bus.QueryAsync(new LoginUserCommand(viewModel.Email, viewModel.Password));
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
namespace CleanArchitecture.Application.ViewModels.Users;
|
||||
|
||||
public sealed record ChangePasswordViewModel(string Password, string NewPassword);
|
@ -0,0 +1,3 @@
|
||||
namespace CleanArchitecture.Application.ViewModels.Users;
|
||||
|
||||
public sealed record LoginUserViewModel(string Email, string Password);
|
@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users;
|
||||
public sealed record CreateUserViewModel(
|
||||
string Email,
|
||||
string Surname,
|
||||
string GivenName);
|
||||
string GivenName,
|
||||
string Password);
|
@ -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);
|
||||
string GivenName,
|
||||
UserRole Role);
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -8,10 +8,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3"/>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/>
|
||||
<PackageReference Include="Moq" Version="4.18.4"/>
|
||||
<PackageReference Include="xunit" Version="2.4.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -23,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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<PasswordChangedEvent>(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<UserUpdatedEvent>()
|
||||
.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<UserUpdatedEvent>()
|
||||
.VerifyAnyDomainNotification()
|
||||
.VerifyExistingNotification(
|
||||
DomainErrorCodes.UserPasswordIncorrect,
|
||||
"The password is incorrect");
|
||||
}
|
||||
}
|
@ -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<IUserRepository>();
|
||||
|
||||
CommandHandler = new ChangePasswordCommandHandler(
|
||||
Bus.Object,
|
||||
UnitOfWork.Object,
|
||||
NotificationHandler.Object,
|
||||
UserRepository.Object,
|
||||
User.Object);
|
||||
}
|
||||
|
||||
public ChangePasswordCommandHandler CommandHandler { get; }
|
||||
private Mock<IUserRepository> 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<Guid>(y => y == user.Id)))
|
||||
.ReturnsAsync(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public Guid SetupMissingUser()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
User.Setup(x => x.GetUserId()).Returns(id);
|
||||
return id;
|
||||
}
|
||||
}
|
@ -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<ChangePasswordCommand, ChangePasswordCommandValidation>
|
||||
{
|
||||
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<string>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
@ -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<UserCreatedEvent>(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
|
||||
|
@ -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<IUserRepository> UserRepository { get; }
|
||||
|
||||
public CreateUserCommandTestFixture()
|
||||
{
|
||||
UserRepository = new Mock<IUserRepository>();
|
||||
|
||||
CommandHandler = new(
|
||||
|
||||
CommandHandler = new CreateUserCommandHandler(
|
||||
Bus.Object,
|
||||
UnitOfWork.Object,
|
||||
NotificationHandler.Object,
|
||||
UserRepository.Object);
|
||||
}
|
||||
|
||||
|
||||
public CreateUserCommandHandler CommandHandler { get; }
|
||||
private Mock<IUserRepository> 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<Guid>(y => y == user.Id)))
|
||||
|
@ -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<string>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
@ -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<UserDeletedEvent>(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
|
||||
|
@ -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<IUserRepository> UserRepository { get; }
|
||||
|
||||
public DeleteUserCommandTestFixture()
|
||||
{
|
||||
UserRepository = new Mock<IUserRepository>();
|
||||
|
||||
CommandHandler = new (
|
||||
|
||||
CommandHandler = new DeleteUserCommandHandler(
|
||||
Bus.Object,
|
||||
UnitOfWork.Object,
|
||||
NotificationHandler.Object,
|
||||
UserRepository.Object);
|
||||
UserRepository.Object,
|
||||
User.Object);
|
||||
}
|
||||
|
||||
public DeleteUserCommandHandler CommandHandler { get; }
|
||||
private Mock<IUserRepository> 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<Guid>(y => y == user.Id)))
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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<IUserRepository>();
|
||||
|
||||
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<IUserRepository> UserRepository { get; set; }
|
||||
public IOptions<TokenSettings> 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<string>(y => y == user.Email)))
|
||||
.ReturnsAsync(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
@ -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<LoginUserCommand, LoginUserCommandValidation>
|
||||
{
|
||||
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<string>
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
@ -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<UserUpdatedEvent>(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
|
||||
|
@ -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<IUserRepository> UserRepository { get; }
|
||||
|
||||
public UpdateUserCommandTestFixture()
|
||||
{
|
||||
UserRepository = new Mock<IUserRepository>();
|
||||
|
||||
CommandHandler = new(
|
||||
|
||||
CommandHandler = new UpdateUserCommandHandler(
|
||||
Bus.Object,
|
||||
UnitOfWork.Object,
|
||||
NotificationHandler.Object,
|
||||
UserRepository.Object);
|
||||
UserRepository.Object,
|
||||
User.Object);
|
||||
}
|
||||
|
||||
|
||||
public UpdateUserCommandHandler CommandHandler { get; }
|
||||
private Mock<IUserRepository> 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<Guid>(y => y == user.Id)))
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<IMediatorHandler> Bus { get; }
|
||||
protected Mock<IUnitOfWork> UnitOfWork { get; }
|
||||
protected Mock<DomainNotificationHandler> NotificationHandler { get; }
|
||||
|
||||
protected CommandHandlerFixtureBase()
|
||||
{
|
||||
Bus = new Mock<IMediatorHandler>();
|
||||
UnitOfWork = new Mock<IUnitOfWork>();
|
||||
NotificationHandler = new Mock<DomainNotificationHandler>();
|
||||
User = new Mock<IUser>();
|
||||
|
||||
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<IMediatorHandler> Bus { get; }
|
||||
protected Mock<IUnitOfWork> UnitOfWork { get; }
|
||||
protected Mock<DomainNotificationHandler> NotificationHandler { get; }
|
||||
protected Mock<IUser> User { get; }
|
||||
|
||||
public CommandHandlerFixtureBase VerifyExistingNotification(string errorCode, string message)
|
||||
{
|
||||
Bus.Verify(
|
||||
|
@ -8,7 +8,7 @@ namespace CleanArchitecture.Domain.Tests;
|
||||
|
||||
public class ValidationTestBase<TCommand, TValidation>
|
||||
where TCommand : CommandBase
|
||||
where TValidation: AbstractValidator<TCommand>
|
||||
where TValidation : AbstractValidator<TCommand>
|
||||
{
|
||||
private readonly TValidation _validation;
|
||||
|
||||
@ -54,7 +54,7 @@ public class ValidationTestBase<TCommand, TValidation>
|
||||
}
|
||||
|
||||
protected void ShouldHaveExpectedErrors(
|
||||
TCommand command,
|
||||
TCommand command,
|
||||
params KeyValuePair<string, string>[] expectedErrors)
|
||||
{
|
||||
var result = _validation.Validate(command);
|
||||
@ -70,4 +70,22 @@ public class ValidationTestBase<TCommand, TValidation>
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
59
CleanArchitecture.Domain/ApiUser.cs
Normal file
59
CleanArchitecture.Domain/ApiUser.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
@ -6,8 +6,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="11.5.1" />
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3"/>
|
||||
<PackageReference Include="FluentValidation" Version="11.5.1"/>
|
||||
<PackageReference Include="MediatR" Version="12.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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();
|
||||
}
|
@ -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<DomainNotification> notifications)
|
||||
{
|
||||
_bus = bus;
|
||||
Bus = bus;
|
||||
_unitOfWork = unitOfWork;
|
||||
_notifications = (DomainNotificationHandler)notifications;
|
||||
}
|
||||
|
||||
public async Task<bool> CommitAsync()
|
||||
|
||||
protected async Task<bool> 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<bool> 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));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<ChangePasswordCommand>
|
||||
{
|
||||
private readonly IUser _user;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ChangePasswordCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using CleanArchitecture.Domain.Extensions.Validation;
|
||||
using FluentValidation;
|
||||
|
||||
namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
|
||||
|
||||
public sealed class ChangePasswordCommandValidation : AbstractValidator<ChangePasswordCommand>
|
||||
{
|
||||
public ChangePasswordCommandValidation()
|
||||
{
|
||||
AddRuleForPassword();
|
||||
AddRuleForNewPassword();
|
||||
}
|
||||
|
||||
private void AddRuleForPassword()
|
||||
{
|
||||
RuleFor(cmd => cmd.Password)
|
||||
.Password();
|
||||
}
|
||||
|
||||
private void AddRuleForNewPassword()
|
||||
{
|
||||
RuleFor(cmd => cmd.NewPassword)
|
||||
.Password();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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<CreateUserCommand>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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<CreateUserCo
|
||||
AddRuleForEmail();
|
||||
AddRuleForSurname();
|
||||
AddRuleForGivenName();
|
||||
AddRuleForPassword();
|
||||
}
|
||||
|
||||
private void AddRuleForId()
|
||||
@ -20,7 +22,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserEmptyId)
|
||||
.WithMessage("User id may not be empty");
|
||||
}
|
||||
|
||||
|
||||
private void AddRuleForEmail()
|
||||
{
|
||||
RuleFor(cmd => cmd.Email)
|
||||
@ -31,7 +33,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
|
||||
.WithMessage("Email may not be longer than 320 characters");
|
||||
}
|
||||
|
||||
|
||||
private void AddRuleForSurname()
|
||||
{
|
||||
RuleFor(cmd => cmd.Surname)
|
||||
@ -42,7 +44,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserSurnameExceedsMaxLength)
|
||||
.WithMessage("Surname may not be longer than 100 characters");
|
||||
}
|
||||
|
||||
|
||||
private void AddRuleForGivenName()
|
||||
{
|
||||
RuleFor(cmd => cmd.GivenName)
|
||||
@ -53,4 +55,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength)
|
||||
.WithMessage("Given name may not be longer than 100 characters");
|
||||
}
|
||||
|
||||
private void AddRuleForPassword()
|
||||
{
|
||||
RuleFor(cmd => cmd.Password)
|
||||
.Password();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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<DeleteUserCommand>
|
||||
{
|
||||
private readonly IUser _user;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
|
||||
public DeleteUserCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using MediatR;
|
||||
|
||||
namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
|
||||
|
||||
public sealed class LoginUserCommand : CommandBase,
|
||||
IRequest<string>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<LoginUserCommand, string>
|
||||
{
|
||||
private const double ExpiryDurationMinutes = 30;
|
||||
private readonly TokenSettings _tokenSettings;
|
||||
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public LoginUserCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> notifications,
|
||||
IUserRepository userRepository,
|
||||
IOptions<TokenSettings> tokenSettings) : base(bus, unitOfWork, notifications)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_tokenSettings = tokenSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<string> 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);
|
||||
}
|
||||
}
|
@ -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<LoginUserCommand>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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<UpdateUserCommand>
|
||||
{
|
||||
private readonly IUser _user;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
|
||||
public UpdateUserCommandHandler(
|
||||
IMediatorHandler bus,
|
||||
IUnitOfWork unitOfWork,
|
||||
INotificationHandler<DomainNotification> 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
AddRuleForEmail();
|
||||
AddRuleForSurname();
|
||||
AddRuleForGivenName();
|
||||
AddRuleForRole();
|
||||
}
|
||||
|
||||
private void AddRuleForId()
|
||||
@ -20,7 +21,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserEmptyId)
|
||||
.WithMessage("User id may not be empty");
|
||||
}
|
||||
|
||||
|
||||
private void AddRuleForEmail()
|
||||
{
|
||||
RuleFor(cmd => cmd.Email)
|
||||
@ -31,7 +32,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
|
||||
.WithMessage("Email may not be longer than 320 characters");
|
||||
}
|
||||
|
||||
|
||||
private void AddRuleForSurname()
|
||||
{
|
||||
RuleFor(cmd => cmd.Surname)
|
||||
@ -42,7 +43,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserSurnameExceedsMaxLength)
|
||||
.WithMessage("Surname may not be longer than 100 characters");
|
||||
}
|
||||
|
||||
|
||||
private void AddRuleForGivenName()
|
||||
{
|
||||
RuleFor(cmd => cmd.GivenName)
|
||||
@ -53,4 +54,12 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
|
||||
.WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength)
|
||||
.WithMessage("Given name may not be longer than 100 characters");
|
||||
}
|
||||
|
||||
private void AddRuleForRole()
|
||||
{
|
||||
RuleFor(cmd => cmd.Role)
|
||||
.IsInEnum()
|
||||
.WithErrorCode(DomainErrorCodes.UserInvalidRole)
|
||||
.WithMessage("Role is not a valid role");
|
||||
}
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
7
CleanArchitecture.Domain/Enums/UserRole.cs
Normal file
7
CleanArchitecture.Domain/Enums/UserRole.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace CleanArchitecture.Domain.Enums;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Admin,
|
||||
User
|
||||
}
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -8,9 +8,10 @@ namespace CleanArchitecture.Domain.EventHandler;
|
||||
public sealed class UserEventHandler :
|
||||
INotificationHandler<UserDeletedEvent>,
|
||||
INotificationHandler<UserCreatedEvent>,
|
||||
INotificationHandler<UserUpdatedEvent>
|
||||
INotificationHandler<UserUpdatedEvent>,
|
||||
INotificationHandler<PasswordChangedEvent>
|
||||
{
|
||||
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;
|
||||
|
13
CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs
Normal file
13
CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs
Normal file
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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<IRequestHandler<CreateUserCommand>, CreateUserCommandHandler>();
|
||||
services.AddScoped<IRequestHandler<UpdateUserCommand>, UpdateUserCommandHandler>();
|
||||
services.AddScoped<IRequestHandler<DeleteUserCommand>, DeleteUserCommandHandler>();
|
||||
|
||||
services.AddScoped<IRequestHandler<ChangePasswordCommand>, ChangePasswordCommandHandler>();
|
||||
services.AddScoped<IRequestHandler<LoginUserCommand, string>, LoginUserCommandHandler>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static IServiceCollection AddNotificationHandlers(this IServiceCollection services)
|
||||
{
|
||||
// User
|
||||
services.AddScoped<INotificationHandler<UserCreatedEvent>, UserEventHandler>();
|
||||
services.AddScoped<INotificationHandler<UserUpdatedEvent>, UserEventHandler>();
|
||||
services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>();
|
||||
|
||||
services.AddScoped<INotificationHandler<PasswordChangedEvent>, UserEventHandler>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddApiUser(this IServiceCollection services)
|
||||
{
|
||||
// User
|
||||
services.AddScoped<IUser, ApiUser>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -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<T, string> StringMustBeBase64<T>(this IRuleBuilder<T, string> 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<T, string> Password<T>(
|
||||
this IRuleBuilder<T, string> 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;
|
||||
}
|
||||
}
|
11
CleanArchitecture.Domain/Interfaces/IUser.cs
Normal file
11
CleanArchitecture.Domain/Interfaces/IUser.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.Enums;
|
||||
|
||||
namespace CleanArchitecture.Domain.Interfaces;
|
||||
|
||||
public interface IUser
|
||||
{
|
||||
string Name { get; }
|
||||
Guid GetUserId();
|
||||
UserRole GetUserRole();
|
||||
}
|
@ -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; }
|
||||
}
|
@ -15,18 +15,18 @@ public class DomainNotificationHandler : INotificationHandler<DomainNotification
|
||||
_notifications = new List<DomainNotification>();
|
||||
}
|
||||
|
||||
public virtual List<DomainNotification> GetNotifications()
|
||||
{
|
||||
return _notifications;
|
||||
}
|
||||
|
||||
public Task Handle(DomainNotification notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_notifications.Add(notification);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
public virtual List<DomainNotification> GetNotifications()
|
||||
{
|
||||
return _notifications;
|
||||
}
|
||||
|
||||
public virtual bool HasNotifications()
|
||||
{
|
||||
return GetNotifications().Any();
|
||||
|
8
CleanArchitecture.Domain/Settings/TokenSettings.cs
Normal file
8
CleanArchitecture.Domain/Settings/TokenSettings.cs
Normal file
@ -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!;
|
||||
}
|
@ -8,10 +8,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/>
|
||||
<PackageReference Include="Moq" Version="4.18.4"/>
|
||||
<PackageReference Include="xunit" Version="2.4.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -23,7 +23,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -19,18 +19,18 @@ public sealed class UnitOfWorkTests
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
var dbContextMock = new Mock<ApplicationDbContext>(options.Options);
|
||||
var loggerMock = new Mock<ILogger<UnitOfWork<ApplicationDbContext>>>();
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -6,18 +6,18 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MediatR" Version="12.0.1"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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<User>
|
||||
.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));
|
||||
}
|
||||
}
|
@ -6,12 +6,12 @@ namespace CleanArchitecture.Infrastructure.Database;
|
||||
|
||||
public class ApplicationDbContext : DbContext
|
||||
{
|
||||
public DbSet<User> Users { get; set; } = null!;
|
||||
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.ApplyConfiguration(new UserConfiguration());
|
||||
|
@ -18,4 +18,4 @@ public static class DbContextExtension
|
||||
context.Database.Migrate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,4 +22,4 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ public sealed class InMemoryBus : IMediatorHandler
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
|
||||
public Task<TResponse> QueryAsync<TResponse>(IRequest<TResponse> query)
|
||||
{
|
||||
return _mediator.Send(query);
|
||||
|
82
CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs
generated
Normal file
82
CleanArchitecture.Infrastructure/Migrations/20230320204057_AddUserRoleAndPassword.Designer.cs
generated
Normal file
@ -0,0 +1,82 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(320)
|
||||
.HasColumnType("nvarchar(320)");
|
||||
|
||||
b.Property<string>("GivenName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CleanArchitecture.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserRoleAndPassword : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Password",
|
||||
table: "Users",
|
||||
type: "nvarchar(128)",
|
||||
maxLength: 128,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
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" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
|
@ -50,19 +50,6 @@ public class BaseRepository<TEntity> : IRepository<TEntity> 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<TEntity> : IRepository<TEntity> 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<TEntity> : IRepository<TEntity> where TEntity : Enti
|
||||
}
|
||||
}
|
||||
|
||||
public int SaveChanges()
|
||||
{
|
||||
return _dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_dbContext.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -45,4 +45,4 @@ public sealed class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContex
|
||||
_context.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,14 +8,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0"/>
|
||||
<PackageReference Include="xunit" Version="2.4.2"/>
|
||||
<PackageReference Include="Xunit.Priority" Version="1.1.6"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -27,8 +27,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArchitecture.Api\CleanArchitecture.Api.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\CleanArchitecture.Api\CleanArchitecture.Api.csproj"/>
|
||||
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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<UserTestFixture>
|
||||
_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<IEnumerable<UserViewModel>>();
|
||||
|
||||
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<UserTestFixture>
|
||||
_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<string>();
|
||||
|
||||
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<UserTestFixture>
|
||||
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<UserViewModel>();
|
||||
|
||||
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<UserTestFixture>
|
||||
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<UserTestFixture>
|
||||
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<ChangePasswordViewModel>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<UserTestFixture>
|
||||
|
||||
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<UserTestFixture>
|
||||
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<IEnumerable<UserViewModel>>();
|
||||
|
||||
message?.Data.Should().NotBeNull();
|
||||
|
||||
var content = message!.Data!;
|
||||
|
||||
content.Should().BeNullOrEmpty();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<TContext>(this IServiceCollection services, DbConnection connection) where TContext : DbContext
|
||||
public static IServiceCollection SetupTestDatabase<TContext>(this IServiceCollection services,
|
||||
DbConnection connection) where TContext : DbContext
|
||||
{
|
||||
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>));
|
||||
if (descriptor != null)
|
||||
services.Remove(descriptor);
|
||||
|
||||
services.AddScoped(p =>
|
||||
DbContextOptionsFactory<TContext>(
|
||||
p,
|
||||
(_, options) => options
|
||||
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
|
||||
.UseLazyLoadingProxies()
|
||||
.UseSqlite(connection)));
|
||||
DbContextOptionsFactory<TContext>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ public static class HttpExtensions
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private static T? Deserialize<T>(string json)
|
||||
@ -55,4 +55,4 @@ public static class HttpExtensions
|
||||
|
||||
return httpClient.PutAsync(url, content);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -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}");
|
||||
}
|
||||
}
|
@ -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<ApplicationDbContext>();
|
||||
@ -62,4 +62,4 @@ public sealed class CleanArchitectureWebApplicationFactory : WebApplicationFacto
|
||||
_registerCustomServicesHandler?.Invoke(services, sp, scopedServices);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user