From 0b7230aabe385df944300363cc5b43377da6bfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Mon, 15 Jun 2026 16:36:49 +0300 Subject: [PATCH 01/10] Merge branch 'dev' into current branch, accepting dev version of appsettings.json --- Streetcode/Streetcode.WebApi/appsettings.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Streetcode/Streetcode.WebApi/appsettings.json b/Streetcode/Streetcode.WebApi/appsettings.json index 71d6b733..110eedd0 100644 --- a/Streetcode/Streetcode.WebApi/appsettings.json +++ b/Streetcode/Streetcode.WebApi/appsettings.json @@ -72,5 +72,12 @@ "Issuer": "Streetcode.WebApi", "Audience": "Streetcode.Client", "AccessTokenLifetimeInMinutes": 120 + }, + "UkrPoshtaParser": { + "DownloadUrl": "https://www.ukrposhta.ua/files/shares/out/houses.zip" + }, + "Geocoding": { + "BaseUrl": "https://nominatim.openstreetmap.org/search" } } +} From 4c456c4f9c233842b50bb254c15076583f781970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 10:13:51 +0300 Subject: [PATCH 02/10] add models, mediatR,update controller for google login --- .../DTO/Users/GoogleLoginRequest.cs | 7 ++ .../DTO/Users/GoogleLoginRequestDto.cs | 9 ++ .../Users/LoginGoogle/GoogleLoginCommand.cs | 9 ++ .../Users/LoginGoogle/GoogleLoginHandler.cs | 84 +++++++++++++++++++ Streetcode/Streetcode.DAL/Enums/UserRole.cs | 3 +- .../Controllers/Users/AuthController.cs | 35 +++++++- .../Streetcode.WebApi.csproj | 1 + 7 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequest.cs create mode 100644 Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequestDto.cs create mode 100644 Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs create mode 100644 Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs diff --git a/Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequest.cs b/Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequest.cs new file mode 100644 index 00000000..b541e733 --- /dev/null +++ b/Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequest.cs @@ -0,0 +1,7 @@ +namespace Streetcode.BLL.DTO.Users +{ + public class GoogleLoginRequest + { + public string IdToken { get; set; } = string.Empty; + } +} diff --git a/Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequestDto.cs b/Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequestDto.cs new file mode 100644 index 00000000..f4f6b802 --- /dev/null +++ b/Streetcode/Streetcode.BLL/DTO/Users/GoogleLoginRequestDto.cs @@ -0,0 +1,9 @@ +namespace Streetcode.BLL.DTO.Users +{ + public class GoogleLoginRequestDto + { + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Surname { get; set; } = string.Empty; + } +} diff --git a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs new file mode 100644 index 00000000..1928171e --- /dev/null +++ b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs @@ -0,0 +1,9 @@ +using FluentResults; +using MediatR; +using Streetcode.BLL.DTO.Users; + +namespace Streetcode.BLL.MediatR.Users.LoginGoogle +{ + public record GoogleLoginCommand(GoogleLoginRequestDto googleLoginRequest) + : IRequest>; +} diff --git a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs new file mode 100644 index 00000000..40f9886d --- /dev/null +++ b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -0,0 +1,84 @@ +using System.IdentityModel.Tokens.Jwt; +using AutoMapper; +using FluentResults; +using MediatR; +using Microsoft.AspNetCore.Identity; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.BLL.Interfaces.Users; +using Streetcode.DAL.Entities.Users; +using Streetcode.DAL.Enums; + +namespace Streetcode.BLL.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandler : IRequestHandler> + { + private readonly UserManager _userManager; + private readonly ITokenService _tokenService; + private readonly IMapper _mapper; + + private readonly ILoggerService _logger; + + public GoogleLoginHandler( + UserManager userManager, + ITokenService tokenService, + ILoggerService logger, + IMapper mapper) + { + _userManager = userManager; + _tokenService = tokenService; + + _logger = logger; + _mapper = mapper; + } + + public async Task> Handle(GoogleLoginCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation($"Google login attempt for {request.googleLoginRequest.Email}"); + + var user = await _userManager.FindByEmailAsync(request.googleLoginRequest.Email); + + if (user == null) + { + user = new User + { + Email = request.googleLoginRequest.Email, + UserName = request.googleLoginRequest.Email, + Name = request.googleLoginRequest.Name, + Surname = request.googleLoginRequest.Surname + }; + + var result = await _userManager.CreateAsync(user); + if (!result.Succeeded) + { + _logger.LogError(request, $"Failed to create user {request.googleLoginRequest.Email}: {string.Join(", ", result.Errors.Select(e => e.Description))}"); + return Result.Fail("Failed to create user account."); + } + + var roleResult = await _userManager.AddToRoleAsync(user, UserRole.Client.ToString()); + if (!roleResult.Succeeded) + { + _logger.LogError(request, $"Failed to add role for user {user.Id}: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); + return Result.Fail("User created but failed to assign role."); + } + } + + var jwtToken = _tokenService.GenerateJWTToken(user); + var refreshToken = _tokenService.GenerateRefreshToken(); + + user.RefreshToken = refreshToken; + user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(7); + await _userManager.UpdateAsync(user); + + _logger.LogInformation($"User {user.Id} successfully logged in"); + + return Result.Ok(new LoginResultDto + { + User = _mapper.Map(user), + Token = new JwtSecurityTokenHandler().WriteToken(jwtToken), + RefreshToken = refreshToken, + ExpireAt = jwtToken.ValidTo + }); + } + } +} diff --git a/Streetcode/Streetcode.DAL/Enums/UserRole.cs b/Streetcode/Streetcode.DAL/Enums/UserRole.cs index 165523c4..8493165f 100644 --- a/Streetcode/Streetcode.DAL/Enums/UserRole.cs +++ b/Streetcode/Streetcode.DAL/Enums/UserRole.cs @@ -5,6 +5,7 @@ public enum UserRole { MainAdministrator, Administrator, - Moderator + Moderator, + Client } } diff --git a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs index 3be0382e..39494824 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs @@ -1,12 +1,14 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.BLL.DTO.Users; -using Microsoft.AspNetCore.Authorization; using Streetcode.BLL.MediatR.Users.Login; +using Streetcode.BLL.MediatR.Users.LoginGoogle; using Streetcode.BLL.MediatR.Users.Logout; -using Streetcode.BLL.MediatR.Users.Register; using Streetcode.BLL.MediatR.Users.RefreshToken; +using Streetcode.BLL.MediatR.Users.Register; +using Google.Apis.Auth; namespace Streetcode.WebApi.Controllers.Users; @@ -22,6 +24,35 @@ await base.Mediator.Send(new LoginUserCommand(loginRequest), cancellationToken) ); } + [HttpPost("google")] + public async Task GoogleLogin([FromBody] GoogleLoginRequest request) + { + try + { + var settings = new GoogleJsonWebSignature.ValidationSettings() + { + Audience = new List { "ВАШ_GOOGLE_CLIENT_ID.apps.googleusercontent.com" } + }; + + var payload = await GoogleJsonWebSignature.ValidateAsync(request.IdToken, settings); + + var loginDto = new GoogleLoginRequestDto + { + Email = payload.Email, + Name = payload.GivenName, + Surname = payload.FamilyName + }; + + return base.HandleResult( + await base.Mediator.Send(new GoogleLoginCommand(loginDto)) + ); + } + catch (Exception) + { + return Unauthorized("Invalid Google Token"); + } + } + [HttpPost("register")] public async Task Register([FromBody] UserRegisterDto registerRequest, CancellationToken cancellationToken = default) { diff --git a/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj b/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj index 71ba06b4..6c1a0aca 100644 --- a/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj +++ b/Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj @@ -21,6 +21,7 @@ + From 5086644bb456948f9bc7be41adbf6c7009ace6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 13:44:03 +0300 Subject: [PATCH 03/10] fix bud in AuthController --- .../MediatR/Users/LoginGoogle/GoogleLoginHandler.cs | 2 +- Streetcode/Streetcode.DAL/Enums/UserRole.cs | 3 +-- .../Controllers/Users/AuthController.cs | 13 +++++++++++-- Streetcode/Streetcode.slnx | 12 +++++++++--- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs index 40f9886d..72e924ff 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -55,7 +55,7 @@ public async Task> Handle(GoogleLoginCommand request, Can return Result.Fail("Failed to create user account."); } - var roleResult = await _userManager.AddToRoleAsync(user, UserRole.Client.ToString()); + var roleResult = await _userManager.AddToRoleAsync(user, UserRole.MainAdministrator.ToString()); if (!roleResult.Succeeded) { _logger.LogError(request, $"Failed to add role for user {user.Id}: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); diff --git a/Streetcode/Streetcode.DAL/Enums/UserRole.cs b/Streetcode/Streetcode.DAL/Enums/UserRole.cs index 8493165f..165523c4 100644 --- a/Streetcode/Streetcode.DAL/Enums/UserRole.cs +++ b/Streetcode/Streetcode.DAL/Enums/UserRole.cs @@ -5,7 +5,6 @@ public enum UserRole { MainAdministrator, Administrator, - Moderator, - Client + Moderator } } diff --git a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs index 39494824..986aca25 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs @@ -16,6 +16,13 @@ namespace Streetcode.WebApi.Controllers.Users; [ExcludeFromCodeCoverage] public sealed class AuthController : BaseApiController { + private readonly IConfiguration _configuration; + + public AuthController(IConfiguration configuration) + { + _configuration = configuration; + } + [HttpPost("login")] public async Task Login([FromBody] UserLoginDto loginRequest, CancellationToken cancellationToken = default) { @@ -24,14 +31,16 @@ await base.Mediator.Send(new LoginUserCommand(loginRequest), cancellationToken) ); } - [HttpPost("google")] + [HttpPost("google-login")] public async Task GoogleLogin([FromBody] GoogleLoginRequest request) { try { + var clientId = _configuration["GoogleAuth:ClientId"]; + var settings = new GoogleJsonWebSignature.ValidationSettings() { - Audience = new List { "ВАШ_GOOGLE_CLIENT_ID.apps.googleusercontent.com" } + Audience = new List { clientId! } }; var payload = await GoogleJsonWebSignature.ValidateAsync(request.IdToken, settings); diff --git a/Streetcode/Streetcode.slnx b/Streetcode/Streetcode.slnx index db05cc8b..b4cfa13a 100644 --- a/Streetcode/Streetcode.slnx +++ b/Streetcode/Streetcode.slnx @@ -13,12 +13,18 @@ - + + + - + + + - + + + From a108ab7c26a34a1560ae330faed41fc3a258ad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 15:48:05 +0300 Subject: [PATCH 04/10] add googleServise, Validatore, rests --- Streetcode/README.md | 38 +++++ .../Controllers/Users/AuthController.cs | 33 +++- .../Users/LoginGoogle/GoogleLoginCommand.cs | 9 ++ .../Users/LoginGoogle/GoogleLoginHandler.cs | 72 +++++++++ .../Models/DTO/GoogleLoginRequest.cs | 7 + .../Models/DTO/GoogleLoginRequestDto.cs | 9 ++ .../Resources/ErrorMessages.Designer.cs | 18 +++ .../Resources/ErrorMessages.resx | 6 + .../Interfaces/Users/IGoogleAuthService.cs | 9 ++ .../Services/Users/GoogleAuthService.cs | 33 ++++ .../Streetcode.Auth/Streetcode.Auth.csproj | 1 + .../Users/GoogleLoginRequestDtoValidator.cs | 24 +++ .../Users/GoogleLoginRequestValidator.cs | 16 ++ .../Resources/ErrorMessages.Designer.cs | 29 +++- .../Resources/ErrorMessages.resx | 9 ++ .../Users/GoogleLoginRequestDtoValidator.cs | 24 +++ .../Users/GoogleLoginRequestValidator.cs | 16 ++ .../Controllers/Users/AuthController.cs | 25 ++- .../Service/GoogleAuthService.cs | 33 ++++ .../Service/Interfaces/IGoogleAuthService.cs | 9 ++ .../Controllers/AuthControllerTests.cs | 56 ++++++- .../LoginGoogle/GoogleLoginHandlerTests.cs | 102 ++++++++++++ .../Services/GoogleAuthServiceTests.cs | 31 ++++ .../GoogleLoginRequestDtoValidatorTests.cs | 70 +++++++++ .../Users/GoogleLoginRequestValidatorTests.cs | 43 ++++++ .../LoginGoogle/GoogleLoginHandlerTests.cs | 97 ++++++++++++ .../GoogleLoginRequestDtoValidatorTests.cs | 70 +++++++++ .../Users/GoogleLoginRequestValidatorTests.cs | 43 ++++++ .../Streetcode.XUnitTest.csproj | 1 - .../WebApi/Controllers/AuthControllerTests.cs | 145 ++++++++++++++++++ 30 files changed, 1057 insertions(+), 21 deletions(-) create mode 100644 Streetcode/README.md create mode 100644 Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs create mode 100644 Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs create mode 100644 Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs create mode 100644 Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs create mode 100644 Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs create mode 100644 Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs create mode 100644 Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs create mode 100644 Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs create mode 100644 Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs create mode 100644 Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs create mode 100644 Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs create mode 100644 Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/Services/GoogleAuthServiceTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs diff --git a/Streetcode/README.md b/Streetcode/README.md new file mode 100644 index 00000000..1a80ec88 --- /dev/null +++ b/Streetcode/README.md @@ -0,0 +1,38 @@ +# Streetcode.Auth Service + +- GoogleAuth: +--- +## 1. Configure GoogleAuth__ClientId + +```bash +dotnet user-secrets set "GoogleAuth:ClientId" "123721973387-7i3rs06c8iui5lrb805f22o0k2s6gk1o.apps.googleusercontent.com" +``` + +## 2. Create .env file in repository root example + +```env +DB_PASSWORD=Admin@1234 +RABBITMQ_USERNAME=guest +RABBITMQ_PASSWORD=guest + +JWT_KEY=StreetcodeSuperSecretJwtKeyForDevelopmentOnly1234567890 +JWT_ISSUER=Streetcode +JWT_AUDIENCE=StreetcodeUsers +JWT_ACCESS_TOKEN_LIFETIME_IN_MINUTES=120 +JWT_REFRESH_TOKEN_LIFETIME_IN_DAYS=7 + + +EmailConfiguration:From = your@email.com +EmailConfiguration:SmtpServer = smtp.gmail.com +EmailConfiguration:Port = 465 +EmailConfiguration:UserName = your@email.com +EmailConfiguration:Password = your-app-password + +ADMIN_EMAIL=admin@gmail.com +ADMIN_PASSWORD=admin@1234 + +GoogleAuth__ClientId=123721973387-7i3rs06c8iui5lrb805f22o0k2s6gk1o.apps.googleusercontent.com +``` + + +--- \ No newline at end of file diff --git a/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs index a46e6aa7..532b64b5 100644 --- a/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs @@ -1,12 +1,15 @@ +using Google.Apis.Auth; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.Auth.Extensions; using Streetcode.Auth.MediatR.Users.Login; +using Streetcode.Auth.MediatR.Users.LoginGoogle; using Streetcode.Auth.MediatR.Users.Logout; using Streetcode.Auth.MediatR.Users.RefreshToken; using Streetcode.Auth.MediatR.Users.Register; using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Services.Interfaces.Users; namespace Streetcode.Auth.Controllers.Users; @@ -15,10 +18,12 @@ namespace Streetcode.Auth.Controllers.Users; public class AuthController : ControllerBase { private readonly IMediator _mediator; + private readonly IGoogleAuthService _googleAuthService; - public AuthController(IMediator mediator) + public AuthController(IMediator mediator, IGoogleAuthService googleAuthService) { _mediator = mediator; + _googleAuthService = googleAuthService; } [AllowAnonymous] @@ -29,6 +34,32 @@ public async Task Login(UserLoginDto request) return this.ToActionResult(result); } + [HttpPost("google-login")] + public async Task GoogleLogin([FromBody] GoogleLoginRequest request) + { + try + { + var payload = await _googleAuthService.ValidateTokenAsync(request.IdToken); + + if (payload == null) + return Unauthorized("Invalid Google Token"); + + var loginDto = new GoogleLoginRequestDto + { + Email = payload.Email, + Name = payload.GivenName, + Surname = payload.FamilyName + }; + + var result = await _mediator.Send(new GoogleLoginCommand(loginDto)); + return this.ToActionResult(result); + } + catch (Exception) + { + return Unauthorized("Invalid Google Token"); + } + } + [AllowAnonymous] [HttpPost("register")] public async Task Register(UserRegisterDto request) diff --git a/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs new file mode 100644 index 00000000..373b5e34 --- /dev/null +++ b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs @@ -0,0 +1,9 @@ +using FluentResults; +using MediatR; +using Streetcode.Auth.Models.DTO; + +namespace Streetcode.Auth.MediatR.Users.LoginGoogle +{ + public record GoogleLoginCommand(GoogleLoginRequestDto googleLoginRequest) + : IRequest>; +} diff --git a/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs new file mode 100644 index 00000000..c8a2f0eb --- /dev/null +++ b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using FluentResults; +using MediatR; +using Microsoft.AspNetCore.Identity; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Models.Entities; +using Streetcode.Auth.Services.Interfaces.Logging; +using Streetcode.Auth.Services.Interfaces.Users; +using Streetcode.Common.Enums; + +namespace Streetcode.Auth.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandler : IRequestHandler> + { + private readonly UserManager _userManager; + private readonly IAuthService _authService; + private readonly IMapper _mapper; + + private readonly ILoggerService _logger; + + public GoogleLoginHandler( + UserManager userManager, + IAuthService authService, + ILoggerService logger, + IMapper mapper) + { + _userManager = userManager; + _authService = authService; + + _logger = logger; + _mapper = mapper; + } + + public async Task> Handle(GoogleLoginCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation($"Google login attempt for {request.googleLoginRequest.Email}"); + + var user = await _userManager.FindByEmailAsync(request.googleLoginRequest.Email); + + if (user == null) + { + user = new User + { + Email = request.googleLoginRequest.Email, + UserName = request.googleLoginRequest.Email, + Name = request.googleLoginRequest.Name, + Surname = request.googleLoginRequest.Surname + }; + + var result = await _userManager.CreateAsync(user); + if (!result.Succeeded) + { + _logger.LogError(request, $"Failed to create user {request.googleLoginRequest.Email}: {string.Join(", ", result.Errors.Select(e => e.Description))}"); + return Result.Fail("Failed to create user account."); + } + + var roleResult = await _userManager.AddToRoleAsync(user, UserRole.MainAdministrator.ToString()); + if (!roleResult.Succeeded) + { + _logger.LogError(request, $"Failed to add role for user {user.Id}: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); + return Result.Fail("User created but failed to assign role."); + } + } + + var registrResult = await _authService.CreateLoginResultAsync(user); + + _logger.LogInformation($"User {user.Id} successfully logged in"); + + return Result.Ok(registrResult); + } + } +} diff --git a/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs new file mode 100644 index 00000000..7a72e426 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs @@ -0,0 +1,7 @@ +namespace Streetcode.Auth.Models.DTO +{ + public class GoogleLoginRequest + { + public string IdToken { get; set; } = string.Empty; + } +} diff --git a/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs new file mode 100644 index 00000000..125bff41 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs @@ -0,0 +1,9 @@ +namespace Streetcode.Auth.Models.DTO +{ + public class GoogleLoginRequestDto + { + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Surname { get; set; } = string.Empty; + } +} diff --git a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs index 665b1120..64bd21e3 100644 --- a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs +++ b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs @@ -78,6 +78,15 @@ public static string EmailMustNotExceedCharacters { } } + /// + /// Looks up a localized string similar to Google ID Token is required.. + /// + public static string GoogleIDTokenIsRequired { + get { + return ResourceManager.GetString("GoogleIDTokenIsRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid email format.. /// @@ -87,6 +96,15 @@ public static string InvalidEmailFormat { } } + /// + /// Looks up a localized string similar to Invalid Google Token Format.. + /// + public static string InvalidTokenFormat { + get { + return ResourceManager.GetString("InvalidTokenFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid user role.. /// diff --git a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx index 77b66f2c..c946ddb8 100644 --- a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx +++ b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx @@ -162,4 +162,10 @@ Passwords did not match. + + Google ID Token is required. + + + Invalid Google Token Format. + \ No newline at end of file diff --git a/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs b/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs new file mode 100644 index 00000000..c9f59e32 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs @@ -0,0 +1,9 @@ +using Google.Apis.Auth; + +namespace Streetcode.Auth.Services.Interfaces.Users +{ + public interface IGoogleAuthService + { + Task ValidateTokenAsync(string idToken); + } +} diff --git a/Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs b/Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs new file mode 100644 index 00000000..9e691ee2 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs @@ -0,0 +1,33 @@ +using Google.Apis.Auth; +using Streetcode.Auth.Services.Interfaces.Users; + +namespace Streetcode.Auth.Services.Users +{ + public class GoogleAuthService : IGoogleAuthService + { + private readonly IConfiguration _configuration; + + public GoogleAuthService(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task ValidateTokenAsync(string idToken) + { + try + { + var clientId = _configuration["GoogleAuth:ClientId"]; + var settings = new GoogleJsonWebSignature.ValidationSettings() + { + Audience = new List { clientId! } + }; + + return await GoogleJsonWebSignature.ValidateAsync(idToken, settings); + } + catch + { + return null; + } + } + } +} diff --git a/Streetcode/Streetcode.Auth/Streetcode.Auth.csproj b/Streetcode/Streetcode.Auth/Streetcode.Auth.csproj index 36cf4b86..216817ef 100644 --- a/Streetcode/Streetcode.Auth/Streetcode.Auth.csproj +++ b/Streetcode/Streetcode.Auth/Streetcode.Auth.csproj @@ -12,6 +12,7 @@ + diff --git a/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs new file mode 100644 index 00000000..3c5feb25 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Resources; + +namespace Streetcode.Auth.Validators.Users +{ + public class GoogleLoginRequestDtoValidator : AbstractValidator + { + public GoogleLoginRequestDtoValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage(ErrorMessages.EmailIsRequired) + .EmailAddress().WithMessage(ErrorMessages.InvalidEmailFormat); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + + RuleFor(x => x.Surname) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + } + } +} diff --git a/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs new file mode 100644 index 00000000..310af6fc --- /dev/null +++ b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Resources; + +namespace Streetcode.Auth.Validators.Users +{ + public class GoogleLoginRequestValidator : AbstractValidator + { + public GoogleLoginRequestValidator() + { + RuleFor(x => x.IdToken) + .NotEmpty().WithMessage(ErrorMessages.GoogleIDTokenIsRequired) + .MinimumLength(100).WithMessage(ErrorMessages.InvalidTokenFormat); + } + } +} diff --git a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs index a1f421d3..1917f506 100644 --- a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs +++ b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs @@ -19,7 +19,7 @@ namespace Streetcode.BLL.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class ErrorMessages { @@ -618,6 +618,15 @@ public static string EmailFromMustNotExceedCharacters { } } + /// + /// Looks up a localized string similar to Invalid email format.. + /// + public static string EmailIsInvalid { + get { + return ResourceManager.GetString("EmailIsInvalid", resourceCulture); + } + } + /// /// Looks up a localized string similar to Email is required.. /// @@ -906,6 +915,15 @@ public static string FoundResultMatchingNull { } } + /// + /// Looks up a localized string similar to Google ID Token is required.. + /// + public static string GoogleIDTokenIsRequired { + get { + return ResourceManager.GetString("GoogleIDTokenIsRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Historical contexts collection is required.. /// @@ -1041,6 +1059,15 @@ public static string InvalidToken { } } + /// + /// Looks up a localized string similar to Invalid Google Token Format.. + /// + public static string InvalidTokenFormat { + get { + return ResourceManager.GetString("InvalidTokenFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid user role.. /// diff --git a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx index 569de4b3..4e5520d8 100644 --- a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx +++ b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx @@ -711,4 +711,13 @@ News with id {0} was not found + + Invalid email format. + + + Google ID Token is required. + + + Invalid Google Token Format. + \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs new file mode 100644 index 00000000..a71ced36 --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Resources; + +namespace Streetcode.BLL.Validators.Users +{ + public class GoogleLoginRequestDtoValidator : AbstractValidator + { + public GoogleLoginRequestDtoValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage(ErrorMessages.EmailIsRequired) + .EmailAddress().WithMessage(ErrorMessages.InvalidEmailFormat); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + + RuleFor(x => x.Surname) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + } + } +} diff --git a/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs new file mode 100644 index 00000000..1b8a25eb --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Resources; + +namespace Streetcode.BLL.Validators.Users +{ + public class GoogleLoginRequestValidator : AbstractValidator + { + public GoogleLoginRequestValidator() + { + RuleFor(x => x.IdToken) + .NotEmpty().WithMessage(ErrorMessages.GoogleIDTokenIsRequired) + .MinimumLength(100).WithMessage(ErrorMessages.InvalidTokenFormat); + } + } +} diff --git a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs index 986aca25..1b784c2c 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Security.Claims; +using Google.Apis.Auth; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.BLL.DTO.Users; @@ -8,7 +7,9 @@ using Streetcode.BLL.MediatR.Users.Logout; using Streetcode.BLL.MediatR.Users.RefreshToken; using Streetcode.BLL.MediatR.Users.Register; -using Google.Apis.Auth; +using Streetcode.WebApi.Service.Interfaces; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; namespace Streetcode.WebApi.Controllers.Users; @@ -16,11 +17,11 @@ namespace Streetcode.WebApi.Controllers.Users; [ExcludeFromCodeCoverage] public sealed class AuthController : BaseApiController { - private readonly IConfiguration _configuration; + private readonly IGoogleAuthService _googleAuthService; - public AuthController(IConfiguration configuration) + public AuthController(IGoogleAuthService googleAuthService) { - _configuration = configuration; + _googleAuthService = googleAuthService; } [HttpPost("login")] @@ -36,14 +37,12 @@ public async Task GoogleLogin([FromBody] GoogleLoginRequest reque { try { - var clientId = _configuration["GoogleAuth:ClientId"]; - - var settings = new GoogleJsonWebSignature.ValidationSettings() - { - Audience = new List { clientId! } - }; + var payload = await _googleAuthService.ValidateTokenAsync(request.IdToken); - var payload = await GoogleJsonWebSignature.ValidateAsync(request.IdToken, settings); + if (payload == null) + { + return Unauthorized("Invalid Google Token"); + } var loginDto = new GoogleLoginRequestDto { diff --git a/Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs b/Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs new file mode 100644 index 00000000..53fef0e6 --- /dev/null +++ b/Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs @@ -0,0 +1,33 @@ +using Google.Apis.Auth; +using Streetcode.WebApi.Service.Interfaces; + +namespace Streetcode.WebApi.Service +{ + public class GoogleAuthService : IGoogleAuthService + { + private readonly IConfiguration _configuration; + + public GoogleAuthService(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task ValidateTokenAsync(string idToken) + { + try + { + var clientId = _configuration["GoogleAuth:ClientId"]; + var settings = new GoogleJsonWebSignature.ValidationSettings() + { + Audience = new List { clientId! } + }; + + return await GoogleJsonWebSignature.ValidateAsync(idToken, settings); + } + catch + { + return null!; + } + } + } +} diff --git a/Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs b/Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs new file mode 100644 index 00000000..dfaeafaa --- /dev/null +++ b/Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs @@ -0,0 +1,9 @@ +using Google.Apis.Auth; + +namespace Streetcode.WebApi.Service.Interfaces +{ + public interface IGoogleAuthService + { + Task ValidateTokenAsync(string idToken); + } +} diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs index 6741a776..51048da6 100644 --- a/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs @@ -1,16 +1,18 @@ using FluentAssertions; using FluentResults; +using Google.Apis.Auth; using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Moq; using Streetcode.Auth.Controllers.Users; using Streetcode.Auth.MediatR.Users.Login; +using Streetcode.Auth.MediatR.Users.LoginGoogle; using Streetcode.Auth.MediatR.Users.Logout; using Streetcode.Auth.MediatR.Users.RefreshToken; using Streetcode.Auth.MediatR.Users.Register; using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Services.Interfaces.Users; +using Streetcode.Common.Enums; using Xunit; namespace Streetcode.XUnitTest.AuthService.Controllers; @@ -18,19 +20,21 @@ namespace Streetcode.XUnitTest.AuthService.Controllers; public class AuthControllerTests { private readonly Mock _mediatorMock; + private readonly Mock _googleAuthServiceMock; private readonly AuthController _controller; public AuthControllerTests() { _mediatorMock = new Mock(); - _controller = new AuthController(_mediatorMock.Object); + _googleAuthServiceMock = new Mock(); + _controller = new AuthController(_mediatorMock.Object, _googleAuthServiceMock.Object); } [Fact] public async Task Login_ShouldReturnOk() { // Arrange - var request = new UserLoginDto + var request = new Auth.Models.DTO.UserLoginDto { Login = "test", Password = "123" @@ -38,7 +42,7 @@ public async Task Login_ShouldReturnOk() var authResult = new AuthResponseDto { - User = new UserDto + User = new Auth.Models.DTO.UserDto { Id = 1, Email = "test@mail.com", @@ -181,4 +185,46 @@ public async Task Logout_ShouldReturnOk_WhenTokenIsValid() It.IsAny()), Times.Once); } + + [Fact] + public async Task GoogleLogin_ShouldReturnOk_WhenTokenIsValid() + { + // Arrange + var request = new GoogleLoginRequest { IdToken = "valid-token" }; + var payload = new GoogleJsonWebSignature.Payload { Email = "test@test.com", GivenName = "John", FamilyName = "Doe" }; + + var authResponse = new AuthResponseDto + { + User = new UserDto + { + Id = 1, + Name = "John", + Surname = "Doe", + Email = "john@example.com", + Login = "johndoe", + Role = UserRole.MainAdministrator + }, + Token = "jwt-token", + RefreshToken = "refresh-token", + ExpireAt = DateTime.UtcNow.AddHours(1) + }; + + _googleAuthServiceMock.Setup(x => x.ValidateTokenAsync("valid-token")) + .ReturnsAsync(payload); + + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(authResponse)); + + var result = await _controller.GoogleLogin(request); + + // Assert + result.Should().BeOfType(); + + var okResult = result as OkObjectResult; + okResult?.Value.Should().BeEquivalentTo(authResponse); + + _mediatorMock.Verify( + x => x.Send(It.IsAny(), It.IsAny()), + Times.Once); + } } \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs new file mode 100644 index 00000000..644bec99 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using Microsoft.AspNetCore.Identity; +using Moq; +using Streetcode.Auth.MediatR.Users.LoginGoogle; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Models.Entities; +using Streetcode.Auth.Services.Interfaces.Logging; +using Streetcode.Auth.Services.Interfaces.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandlerTests + { + private readonly Mock> _userManagerMock; + private readonly Mock _authServiceMock; + private readonly Mock _mapperMock; + private readonly Mock _loggerMock; + private readonly GoogleLoginHandler _handler; + + public GoogleLoginHandlerTests() + { + var store = new Mock>(); + _userManagerMock = new Mock>(store.Object, null!, null!, null!, null!, null!, null!, null!, null!); + + _authServiceMock = new Mock(); + _mapperMock = new Mock(); + _loggerMock = new Mock(); + + _handler = new GoogleLoginHandler( + _userManagerMock.Object, + _authServiceMock.Object, + _loggerMock.Object, + _mapperMock.Object); + } + + [Fact] + public async Task Handle_ExistingUser_ReturnsSuccess() + { + // Arrange + var user = new User + { + Email = "test@test.com", + Id = 1, + Name = "Test", + Surname = "User" + }; + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "test@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(request.googleLoginRequest.Email)) + .ReturnsAsync(user); + + _authServiceMock.Setup(x => x.CreateLoginResultAsync(user)) + .ReturnsAsync(new AuthResponseDto + { + Token = "jwt", + RefreshToken = "refresh", + User = new UserDto { Id = 1 } + }); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("refresh", result.Value.RefreshToken); + _authServiceMock.Verify(x => x.CreateLoginResultAsync(user), Times.Once); + } + + [Fact] + public async Task Handle_NewUser_CreatesAndAssignsRole() + { + // Arrange + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "new@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(It.IsAny())) + .ReturnsAsync((User?)null); + + _userManagerMock.Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _userManagerMock.Setup(x => x.AddToRoleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _authServiceMock.Setup(x => x.CreateLoginResultAsync(It.IsAny())) + .ReturnsAsync(new AuthResponseDto + { + Token = "jwt", + RefreshToken = "refresh", + User = new UserDto { Id = 1 } + }); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + _userManagerMock.Verify(x => x.CreateAsync(It.IsAny()), Times.Once); + _userManagerMock.Verify(x => x.AddToRoleAsync(It.IsAny(), It.IsAny()), Times.Once); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Services/GoogleAuthServiceTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Services/GoogleAuthServiceTests.cs new file mode 100644 index 00000000..1fec52ac --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Services/GoogleAuthServiceTests.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Configuration; +using Streetcode.Auth.Services.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.Services +{ + public class GoogleAuthServiceTests + { + [Fact] + public async Task ValidateTokenAsync_ShouldReturnNull_WhenTokenIsInvalid() + { + // Arrange + var myConfiguration = new Dictionary + { + {"GoogleAuth:ClientId", "fake-client-id"} + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(myConfiguration!) + .Build(); + + var service = new GoogleAuthService(configuration); + + // Act + var result = await service.ValidateTokenAsync("invalid-token-string"); + + // Assert + Assert.Null(result); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs new file mode 100644 index 00000000..443f9717 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs @@ -0,0 +1,70 @@ +using FluentValidation.TestHelper; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.Validators.Users +{ + public class GoogleLoginRequestDtoValidatorTests + { + private readonly GoogleLoginRequestDtoValidator _validator; + + public GoogleLoginRequestDtoValidatorTests() + { + _validator = new GoogleLoginRequestDtoValidator(); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Empty() + { + var model = new GoogleLoginRequestDto { Email = "", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Invalid() + { + var model = new GoogleLoginRequestDto { Email = "invalid-email", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Should_Have_Error_When_Name_Is_Empty(string? name) + { + var model = new GoogleLoginRequestDto { Email = "test@test.com", Name = name!, Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Have_Error_When_Name_Is_Too_Long() + { + var model = new GoogleLoginRequestDto + { + Email = "test@test.com", + Name = new string('a', 51), + Surname = "Valid" + }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Not_Have_Error_When_Data_Is_Valid() + { + var model = new GoogleLoginRequestDto + { + Email = "valid@test.com", + Name = "John", + Surname = "Doe" + }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs new file mode 100644 index 00000000..8ba9a9ee --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs @@ -0,0 +1,43 @@ +using FluentValidation.TestHelper; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.Validators.Users +{ + public class GoogleLoginRequestValidatorTests + { + private readonly GoogleLoginRequestValidator _validator; + + public GoogleLoginRequestValidatorTests() + { + _validator = new GoogleLoginRequestValidator(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Should_Have_Error_When_IdToken_Is_Empty(string? token) + { + var model = new GoogleLoginRequest { IdToken = token! }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Have_Error_When_IdToken_Is_Too_Short() + { + var model = new GoogleLoginRequest { IdToken = "short-token" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Not_Have_Error_When_IdToken_Is_Valid_Length() + { + var model = new GoogleLoginRequest { IdToken = new string('a', 101) }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs new file mode 100644 index 00000000..51f7369c --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs @@ -0,0 +1,97 @@ +using System.IdentityModel.Tokens.Jwt; +using AutoMapper; +using Microsoft.AspNetCore.Identity; +using Moq; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.BLL.Interfaces.Users; +using Streetcode.BLL.MediatR.Users.LoginGoogle; +using Streetcode.DAL.Entities.Users; + +using Xunit; + +namespace Streetcode.XUnitTest.BLL.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandlerTests + { + private readonly Mock> _userManagerMock; + private readonly Mock _tokenServiceMock; + private readonly Mock _mapperMock; + private readonly Mock _loggerMock; + private readonly GoogleLoginHandler _handler; + + public GoogleLoginHandlerTests() + { + var store = new Mock>(); + _userManagerMock = new Mock>(store.Object, null!, null!, null!, null!, null!, null!, null!, null!); + + _tokenServiceMock = new Mock(); + _mapperMock = new Mock(); + _loggerMock = new Mock(); + + _handler = new GoogleLoginHandler( + _userManagerMock.Object, + _tokenServiceMock.Object, + _loggerMock.Object, + _mapperMock.Object); + } + + [Fact] + public async Task Handle_ExistingUser_ReturnsSuccess() + { + // Arrange + var user = new User + { + Email = "test@test.com", + Id = 1, + Name = "Test", + Surname = "User" + }; + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "test@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(request.googleLoginRequest.Email)) + .ReturnsAsync(user); + + _tokenServiceMock.Setup(x => x.GenerateJWTToken(user)) + .Returns(new JwtSecurityToken()); + + _tokenServiceMock.Setup(x => x.GenerateRefreshToken()) + .Returns("refresh_token"); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("refresh_token", result.Value.RefreshToken); + _userManagerMock.Verify(x => x.UpdateAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_NewUser_CreatesAndAssignsRole() + { + // Arrange + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "new@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(It.IsAny())) + .ReturnsAsync((User?)null); + + _userManagerMock.Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _userManagerMock.Setup(x => x.AddToRoleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _tokenServiceMock.Setup(x => x.GenerateJWTToken(It.IsAny())) + .Returns(new JwtSecurityToken()); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + _userManagerMock.Verify(x => x.CreateAsync(It.IsAny()), Times.Once); + _userManagerMock.Verify(x => x.AddToRoleAsync(It.IsAny(), It.IsAny()), Times.Once); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs new file mode 100644 index 00000000..07716de6 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs @@ -0,0 +1,70 @@ +using FluentValidation.TestHelper; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.BLL.Validators.Users +{ + public class GoogleLoginRequestDtoValidatorTests + { + private readonly GoogleLoginRequestDtoValidator _validator; + + public GoogleLoginRequestDtoValidatorTests() + { + _validator = new GoogleLoginRequestDtoValidator(); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Empty() + { + var model = new GoogleLoginRequestDto { Email = "", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Invalid() + { + var model = new GoogleLoginRequestDto { Email = "invalid-email", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Should_Have_Error_When_Name_Is_Empty(string? name) + { + var model = new GoogleLoginRequestDto { Email = "test@test.com", Name = name!, Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Have_Error_When_Name_Is_Too_Long() + { + var model = new GoogleLoginRequestDto + { + Email = "test@test.com", + Name = new string('a', 51), + Surname = "Valid" + }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Not_Have_Error_When_Data_Is_Valid() + { + var model = new GoogleLoginRequestDto + { + Email = "valid@test.com", + Name = "John", + Surname = "Doe" + }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs new file mode 100644 index 00000000..2df9d368 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs @@ -0,0 +1,43 @@ +using FluentValidation.TestHelper; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.BLL.Validators.Users +{ + public class GoogleLoginRequestValidatorTests + { + private readonly GoogleLoginRequestValidator _validator; + + public GoogleLoginRequestValidatorTests() + { + _validator = new GoogleLoginRequestValidator(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Should_Have_Error_When_IdToken_Is_Empty(string? token) + { + var model = new GoogleLoginRequest { IdToken = token! }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Have_Error_When_IdToken_Is_Too_Short() + { + var model = new GoogleLoginRequest { IdToken = "short-token" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Not_Have_Error_When_IdToken_Is_Valid_Length() + { + var model = new GoogleLoginRequest { IdToken = new string('a', 101) }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj b/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj index 70d4582e..00a67463 100644 --- a/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj +++ b/Streetcode/Streetcode.XUnitTest/Streetcode.XUnitTest.csproj @@ -45,7 +45,6 @@ - \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs b/Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs new file mode 100644 index 00000000..53cb8ed2 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs @@ -0,0 +1,145 @@ +using System.Security.Claims; +using FluentAssertions; +using FluentResults; +using Google.Apis.Auth; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.MediatR.Users.Login; +using Streetcode.BLL.MediatR.Users.LoginGoogle; +using Streetcode.BLL.MediatR.Users.RefreshToken; +using Streetcode.BLL.MediatR.Users.Register; +using Streetcode.DAL.Enums; +using Streetcode.WebApi.Controllers; +using Streetcode.WebApi.Controllers.Users; +using Streetcode.WebApi.Service.Interfaces; +using Xunit; + +namespace Streetcode.XUnitTest.WebApi.Controllers; + +public class AuthControllerTests +{ + private readonly Mock _mediatorMock; + private readonly Mock _googleAuthServiceMock; + private readonly AuthController _controller; + + public AuthControllerTests() + { + _mediatorMock = new Mock(); + _googleAuthServiceMock = new Mock(); + + _controller = new AuthController(_googleAuthServiceMock.Object); + + var field = typeof(BaseApiController).GetField( + "k__BackingField", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + + if (field != null) + { + field.SetValue(_controller, _mediatorMock.Object); + } + else + { + throw new Exception("Не удалось найти поле k__BackingField в BaseApiController."); + } + } + + private static LoginResultDto CreateLoginResult() => new LoginResultDto + { + User = new UserDto { Id = 1, Email = "test@mail.com", Name = "John", Surname = "Doe", Login = "jd", Role = UserRole.MainAdministrator }, + Token = "jwt-token", + RefreshToken = "refresh-token", + ExpireAt = DateTime.UtcNow.AddHours(1) + }; + + [Fact] + public async Task Login_ShouldReturnOk() + { + var request = new UserLoginDto { Login = "test", Password = "123" }; + var loginResult = CreateLoginResult(); + + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(loginResult)); + + var result = await _controller.Login(request); + + result.Should().BeOfType(); + ((OkObjectResult)result).Value.Should().BeEquivalentTo(loginResult); + } + + [Fact] + public async Task Register_ShouldReturnOk() + { + // Arrange + var request = new UserRegisterDto + { + Email = "test@mail.com", + Password = "123", + PasswordConfirmation = "123", + Name = "John", + Surname = "Doe" + }; + + _mediatorMock + .Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok()); + + // Act + var result = await _controller.Register(request); + + // Assert + result.Should().BeOfType(); + + _mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task RefreshToken_ShouldReturnOk() + { + var request = new RefreshTokenRequestDto { Token = "tiken", RefreshToken = "token" }; + var loginResult = CreateLoginResult(); + + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(loginResult)); + + var result = await _controller.RefreshToken(request); + + result.Should().BeOfType(); + ((OkObjectResult)result).Value.Should().BeEquivalentTo(loginResult); + } + + [Fact] + public async Task Logout_ShouldReturnUnauthorized_WhenUserClaimsAreInvalid() + { + // Arrange + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity()) } + }; + + // Act + var result = await _controller.Logout(CancellationToken.None); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task GoogleLogin_ShouldReturnOk_WhenTokenIsValid() + { + var request = new GoogleLoginRequest { IdToken = "valid-token" }; + var payload = new GoogleJsonWebSignature.Payload { Email = "test@test.com", GivenName = "John", FamilyName = "Doe" }; + var loginResult = CreateLoginResult(); + + _googleAuthServiceMock.Setup(x => x.ValidateTokenAsync("valid-token")).ReturnsAsync(payload); + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(loginResult)); + + var result = await _controller.GoogleLogin(request); + + result.Should().BeOfType(); + ((OkObjectResult)result).Value.Should().BeEquivalentTo(loginResult); + } +} \ No newline at end of file From 5d6469583f84ec287f91600d18bc35386f6e94eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 19:26:36 +0300 Subject: [PATCH 05/10] fix controller, servise, add mediatr, validators, tests for google login --- .../Controllers/Users/AuthController.cs | 33 +++- .../Users/LoginGoogle/GoogleLoginCommand.cs | 9 ++ .../Users/LoginGoogle/GoogleLoginHandler.cs | 72 +++++++++ .../Models/DTO/GoogleLoginRequest.cs | 7 + .../Models/DTO/GoogleLoginRequestDto.cs | 9 ++ Streetcode/Streetcode.Auth/Program.cs | 2 + .../Resources/ErrorMessages.Designer.cs | 18 +++ .../Resources/ErrorMessages.resx | 6 + .../Interfaces/Users/IGoogleAuthService.cs | 9 ++ .../Services/Users/GoogleAuthService.cs | 33 ++++ .../Users/GoogleLoginRequestDtoValidator.cs | 24 +++ .../Users/GoogleLoginRequestValidator.cs | 16 ++ .../Resources/ErrorMessages.Designer.cs | 20 ++- .../Resources/ErrorMessages.resx | 6 + .../Users/GoogleLoginRequestDtoValidator.cs | 24 +++ .../Users/GoogleLoginRequestValidator.cs | 16 ++ .../Controllers/Users/AuthController.cs | 25 ++- Streetcode/Streetcode.WebApi/Program.cs | 8 +- .../Service/GoogleAuthService.cs | 33 ++++ .../Service/Interfaces/IGoogleAuthService.cs | 9 ++ .../Controllers/AuthControllerTests.cs | 56 ++++++- .../LoginGoogle/GoogleLoginHandlerTests.cs | 102 ++++++++++++ .../GoogleLoginRequestDtoValidatorTests.cs | 70 +++++++++ .../Users/GoogleLoginRequestValidatorTests.cs | 43 ++++++ .../LoginGoogle/GoogleLoginHandlerTests.cs | 97 ++++++++++++ .../GoogleLoginRequestDtoValidatorTests.cs | 70 +++++++++ .../Users/GoogleLoginRequestValidatorTests.cs | 43 ++++++ .../WebApi/Controllers/AuthControllerTests.cs | 145 ++++++++++++++++++ 28 files changed, 983 insertions(+), 22 deletions(-) create mode 100644 Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs create mode 100644 Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs create mode 100644 Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs create mode 100644 Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs create mode 100644 Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs create mode 100644 Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs create mode 100644 Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs create mode 100644 Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs create mode 100644 Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs create mode 100644 Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs create mode 100644 Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs create mode 100644 Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs create mode 100644 Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs diff --git a/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs index a46e6aa7..532b64b5 100644 --- a/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.Auth/Controllers/Users/AuthController.cs @@ -1,12 +1,15 @@ +using Google.Apis.Auth; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.Auth.Extensions; using Streetcode.Auth.MediatR.Users.Login; +using Streetcode.Auth.MediatR.Users.LoginGoogle; using Streetcode.Auth.MediatR.Users.Logout; using Streetcode.Auth.MediatR.Users.RefreshToken; using Streetcode.Auth.MediatR.Users.Register; using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Services.Interfaces.Users; namespace Streetcode.Auth.Controllers.Users; @@ -15,10 +18,12 @@ namespace Streetcode.Auth.Controllers.Users; public class AuthController : ControllerBase { private readonly IMediator _mediator; + private readonly IGoogleAuthService _googleAuthService; - public AuthController(IMediator mediator) + public AuthController(IMediator mediator, IGoogleAuthService googleAuthService) { _mediator = mediator; + _googleAuthService = googleAuthService; } [AllowAnonymous] @@ -29,6 +34,32 @@ public async Task Login(UserLoginDto request) return this.ToActionResult(result); } + [HttpPost("google-login")] + public async Task GoogleLogin([FromBody] GoogleLoginRequest request) + { + try + { + var payload = await _googleAuthService.ValidateTokenAsync(request.IdToken); + + if (payload == null) + return Unauthorized("Invalid Google Token"); + + var loginDto = new GoogleLoginRequestDto + { + Email = payload.Email, + Name = payload.GivenName, + Surname = payload.FamilyName + }; + + var result = await _mediator.Send(new GoogleLoginCommand(loginDto)); + return this.ToActionResult(result); + } + catch (Exception) + { + return Unauthorized("Invalid Google Token"); + } + } + [AllowAnonymous] [HttpPost("register")] public async Task Register(UserRegisterDto request) diff --git a/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs new file mode 100644 index 00000000..373b5e34 --- /dev/null +++ b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginCommand.cs @@ -0,0 +1,9 @@ +using FluentResults; +using MediatR; +using Streetcode.Auth.Models.DTO; + +namespace Streetcode.Auth.MediatR.Users.LoginGoogle +{ + public record GoogleLoginCommand(GoogleLoginRequestDto googleLoginRequest) + : IRequest>; +} diff --git a/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs new file mode 100644 index 00000000..c8a2f0eb --- /dev/null +++ b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using FluentResults; +using MediatR; +using Microsoft.AspNetCore.Identity; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Models.Entities; +using Streetcode.Auth.Services.Interfaces.Logging; +using Streetcode.Auth.Services.Interfaces.Users; +using Streetcode.Common.Enums; + +namespace Streetcode.Auth.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandler : IRequestHandler> + { + private readonly UserManager _userManager; + private readonly IAuthService _authService; + private readonly IMapper _mapper; + + private readonly ILoggerService _logger; + + public GoogleLoginHandler( + UserManager userManager, + IAuthService authService, + ILoggerService logger, + IMapper mapper) + { + _userManager = userManager; + _authService = authService; + + _logger = logger; + _mapper = mapper; + } + + public async Task> Handle(GoogleLoginCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation($"Google login attempt for {request.googleLoginRequest.Email}"); + + var user = await _userManager.FindByEmailAsync(request.googleLoginRequest.Email); + + if (user == null) + { + user = new User + { + Email = request.googleLoginRequest.Email, + UserName = request.googleLoginRequest.Email, + Name = request.googleLoginRequest.Name, + Surname = request.googleLoginRequest.Surname + }; + + var result = await _userManager.CreateAsync(user); + if (!result.Succeeded) + { + _logger.LogError(request, $"Failed to create user {request.googleLoginRequest.Email}: {string.Join(", ", result.Errors.Select(e => e.Description))}"); + return Result.Fail("Failed to create user account."); + } + + var roleResult = await _userManager.AddToRoleAsync(user, UserRole.MainAdministrator.ToString()); + if (!roleResult.Succeeded) + { + _logger.LogError(request, $"Failed to add role for user {user.Id}: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); + return Result.Fail("User created but failed to assign role."); + } + } + + var registrResult = await _authService.CreateLoginResultAsync(user); + + _logger.LogInformation($"User {user.Id} successfully logged in"); + + return Result.Ok(registrResult); + } + } +} diff --git a/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs new file mode 100644 index 00000000..7a72e426 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequest.cs @@ -0,0 +1,7 @@ +namespace Streetcode.Auth.Models.DTO +{ + public class GoogleLoginRequest + { + public string IdToken { get; set; } = string.Empty; + } +} diff --git a/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs new file mode 100644 index 00000000..125bff41 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Models/DTO/GoogleLoginRequestDto.cs @@ -0,0 +1,9 @@ +namespace Streetcode.Auth.Models.DTO +{ + public class GoogleLoginRequestDto + { + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Surname { get; set; } = string.Empty; + } +} diff --git a/Streetcode/Streetcode.Auth/Program.cs b/Streetcode/Streetcode.Auth/Program.cs index 73df4af8..eda42e7e 100644 --- a/Streetcode/Streetcode.Auth/Program.cs +++ b/Streetcode/Streetcode.Auth/Program.cs @@ -23,6 +23,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddScoped(); + builder.Services.AddControllers() .AddJsonOptions(options => { diff --git a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs index 665b1120..64bd21e3 100644 --- a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs +++ b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.Designer.cs @@ -78,6 +78,15 @@ public static string EmailMustNotExceedCharacters { } } + /// + /// Looks up a localized string similar to Google ID Token is required.. + /// + public static string GoogleIDTokenIsRequired { + get { + return ResourceManager.GetString("GoogleIDTokenIsRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid email format.. /// @@ -87,6 +96,15 @@ public static string InvalidEmailFormat { } } + /// + /// Looks up a localized string similar to Invalid Google Token Format.. + /// + public static string InvalidTokenFormat { + get { + return ResourceManager.GetString("InvalidTokenFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid user role.. /// diff --git a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx index 77b66f2c..c946ddb8 100644 --- a/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx +++ b/Streetcode/Streetcode.Auth/Resources/ErrorMessages.resx @@ -162,4 +162,10 @@ Passwords did not match. + + Google ID Token is required. + + + Invalid Google Token Format. + \ No newline at end of file diff --git a/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs b/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs new file mode 100644 index 00000000..c9f59e32 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs @@ -0,0 +1,9 @@ +using Google.Apis.Auth; + +namespace Streetcode.Auth.Services.Interfaces.Users +{ + public interface IGoogleAuthService + { + Task ValidateTokenAsync(string idToken); + } +} diff --git a/Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs b/Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs new file mode 100644 index 00000000..9e691ee2 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Services/Users/GoogleAuthService.cs @@ -0,0 +1,33 @@ +using Google.Apis.Auth; +using Streetcode.Auth.Services.Interfaces.Users; + +namespace Streetcode.Auth.Services.Users +{ + public class GoogleAuthService : IGoogleAuthService + { + private readonly IConfiguration _configuration; + + public GoogleAuthService(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task ValidateTokenAsync(string idToken) + { + try + { + var clientId = _configuration["GoogleAuth:ClientId"]; + var settings = new GoogleJsonWebSignature.ValidationSettings() + { + Audience = new List { clientId! } + }; + + return await GoogleJsonWebSignature.ValidateAsync(idToken, settings); + } + catch + { + return null; + } + } + } +} diff --git a/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs new file mode 100644 index 00000000..3c5feb25 --- /dev/null +++ b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Resources; + +namespace Streetcode.Auth.Validators.Users +{ + public class GoogleLoginRequestDtoValidator : AbstractValidator + { + public GoogleLoginRequestDtoValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage(ErrorMessages.EmailIsRequired) + .EmailAddress().WithMessage(ErrorMessages.InvalidEmailFormat); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + + RuleFor(x => x.Surname) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + } + } +} diff --git a/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs new file mode 100644 index 00000000..310af6fc --- /dev/null +++ b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Resources; + +namespace Streetcode.Auth.Validators.Users +{ + public class GoogleLoginRequestValidator : AbstractValidator + { + public GoogleLoginRequestValidator() + { + RuleFor(x => x.IdToken) + .NotEmpty().WithMessage(ErrorMessages.GoogleIDTokenIsRequired) + .MinimumLength(100).WithMessage(ErrorMessages.InvalidTokenFormat); + } + } +} diff --git a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs index a1f421d3..ba0e0f28 100644 --- a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs +++ b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.Designer.cs @@ -19,7 +19,7 @@ namespace Streetcode.BLL.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class ErrorMessages { @@ -906,6 +906,15 @@ public static string FoundResultMatchingNull { } } + /// + /// Looks up a localized string similar to Google ID Token is required.. + /// + public static string GoogleIDTokenIsRequired { + get { + return ResourceManager.GetString("GoogleIDTokenIsRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Historical contexts collection is required.. /// @@ -1041,6 +1050,15 @@ public static string InvalidToken { } } + /// + /// Looks up a localized string similar to Invalid Google token format.. + /// + public static string InvalidTokenFormat { + get { + return ResourceManager.GetString("InvalidTokenFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid user role.. /// diff --git a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx index 569de4b3..8cc2a954 100644 --- a/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx +++ b/Streetcode/Streetcode.BLL/Resources/ErrorMessages.resx @@ -711,4 +711,10 @@ News with id {0} was not found + + Google ID Token is required. + + + Invalid Google token format. + \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs new file mode 100644 index 00000000..a71ced36 --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Resources; + +namespace Streetcode.BLL.Validators.Users +{ + public class GoogleLoginRequestDtoValidator : AbstractValidator + { + public GoogleLoginRequestDtoValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage(ErrorMessages.EmailIsRequired) + .EmailAddress().WithMessage(ErrorMessages.InvalidEmailFormat); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + + RuleFor(x => x.Surname) + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + } + } +} diff --git a/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs new file mode 100644 index 00000000..1b8a25eb --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Resources; + +namespace Streetcode.BLL.Validators.Users +{ + public class GoogleLoginRequestValidator : AbstractValidator + { + public GoogleLoginRequestValidator() + { + RuleFor(x => x.IdToken) + .NotEmpty().WithMessage(ErrorMessages.GoogleIDTokenIsRequired) + .MinimumLength(100).WithMessage(ErrorMessages.InvalidTokenFormat); + } + } +} diff --git a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs index 986aca25..2a322205 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Security.Claims; +using Google.Apis.Auth; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.BLL.DTO.Users; @@ -8,7 +7,9 @@ using Streetcode.BLL.MediatR.Users.Logout; using Streetcode.BLL.MediatR.Users.RefreshToken; using Streetcode.BLL.MediatR.Users.Register; -using Google.Apis.Auth; +using Streetcode.WebApi.Service.Interfaces; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; namespace Streetcode.WebApi.Controllers.Users; @@ -16,11 +17,11 @@ namespace Streetcode.WebApi.Controllers.Users; [ExcludeFromCodeCoverage] public sealed class AuthController : BaseApiController { - private readonly IConfiguration _configuration; + private readonly IGoogleAuthService _googleAuthService; - public AuthController(IConfiguration configuration) + public AuthController(IGoogleAuthService googleAuthService) { - _configuration = configuration; + _googleAuthService = googleAuthService; } [HttpPost("login")] @@ -36,14 +37,12 @@ public async Task GoogleLogin([FromBody] GoogleLoginRequest reque { try { - var clientId = _configuration["GoogleAuth:ClientId"]; + var payload = await _googleAuthService.ValidateTokenAsync(request.IdToken); - var settings = new GoogleJsonWebSignature.ValidationSettings() + if (payload == null) { - Audience = new List { clientId! } - }; - - var payload = await GoogleJsonWebSignature.ValidateAsync(request.IdToken, settings); + return Unauthorized("Invalid Google Token"); + } var loginDto = new GoogleLoginRequestDto { @@ -83,7 +82,7 @@ await base.Mediator.Send(new RefreshTokenCommand(refreshTokenRequest), cancellat public async Task Logout(CancellationToken cancellationToken = default) { string? user_id_claim = User.FindFirstValue(ClaimTypes.NameIdentifier); - if(string.IsNullOrEmpty(user_id_claim) || !int.TryParse(user_id_claim, out int user_id)) + if (string.IsNullOrEmpty(user_id_claim) || !int.TryParse(user_id_claim, out int user_id)) { return base.Unauthorized(); } diff --git a/Streetcode/Streetcode.WebApi/Program.cs b/Streetcode/Streetcode.WebApi/Program.cs index 8f9e45a5..f65e9b07 100644 --- a/Streetcode/Streetcode.WebApi/Program.cs +++ b/Streetcode/Streetcode.WebApi/Program.cs @@ -1,10 +1,12 @@ using FluentValidation; using Hangfire; +using Streetcode.BLL.Interfaces.WebParsingUtils; using Streetcode.BLL.Services.BlobStorageService; +using Streetcode.BLL.Services.WebParsingUtils; using Streetcode.BLL.Validators; using Streetcode.WebApi.Extensions; -using Streetcode.BLL.Interfaces.WebParsingUtils; -using Streetcode.BLL.Services.WebParsingUtils; +using Streetcode.WebApi.Service; +using Streetcode.WebApi.Service.Interfaces; var builder = WebApplication.CreateBuilder(args); @@ -36,6 +38,8 @@ builder.Services.Configure(builder.Configuration.GetSection("UkrPoshtaParser")); builder.Services.Configure(builder.Configuration.GetSection("Geocoding")); +builder.Services.AddScoped(); + var app = builder.Build(); if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Local") diff --git a/Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs b/Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs new file mode 100644 index 00000000..53fef0e6 --- /dev/null +++ b/Streetcode/Streetcode.WebApi/Service/GoogleAuthService.cs @@ -0,0 +1,33 @@ +using Google.Apis.Auth; +using Streetcode.WebApi.Service.Interfaces; + +namespace Streetcode.WebApi.Service +{ + public class GoogleAuthService : IGoogleAuthService + { + private readonly IConfiguration _configuration; + + public GoogleAuthService(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task ValidateTokenAsync(string idToken) + { + try + { + var clientId = _configuration["GoogleAuth:ClientId"]; + var settings = new GoogleJsonWebSignature.ValidationSettings() + { + Audience = new List { clientId! } + }; + + return await GoogleJsonWebSignature.ValidateAsync(idToken, settings); + } + catch + { + return null!; + } + } + } +} diff --git a/Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs b/Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs new file mode 100644 index 00000000..dfaeafaa --- /dev/null +++ b/Streetcode/Streetcode.WebApi/Service/Interfaces/IGoogleAuthService.cs @@ -0,0 +1,9 @@ +using Google.Apis.Auth; + +namespace Streetcode.WebApi.Service.Interfaces +{ + public interface IGoogleAuthService + { + Task ValidateTokenAsync(string idToken); + } +} diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs index 6741a776..51048da6 100644 --- a/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Controllers/AuthControllerTests.cs @@ -1,16 +1,18 @@ using FluentAssertions; using FluentResults; +using Google.Apis.Auth; using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Moq; using Streetcode.Auth.Controllers.Users; using Streetcode.Auth.MediatR.Users.Login; +using Streetcode.Auth.MediatR.Users.LoginGoogle; using Streetcode.Auth.MediatR.Users.Logout; using Streetcode.Auth.MediatR.Users.RefreshToken; using Streetcode.Auth.MediatR.Users.Register; using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Services.Interfaces.Users; +using Streetcode.Common.Enums; using Xunit; namespace Streetcode.XUnitTest.AuthService.Controllers; @@ -18,19 +20,21 @@ namespace Streetcode.XUnitTest.AuthService.Controllers; public class AuthControllerTests { private readonly Mock _mediatorMock; + private readonly Mock _googleAuthServiceMock; private readonly AuthController _controller; public AuthControllerTests() { _mediatorMock = new Mock(); - _controller = new AuthController(_mediatorMock.Object); + _googleAuthServiceMock = new Mock(); + _controller = new AuthController(_mediatorMock.Object, _googleAuthServiceMock.Object); } [Fact] public async Task Login_ShouldReturnOk() { // Arrange - var request = new UserLoginDto + var request = new Auth.Models.DTO.UserLoginDto { Login = "test", Password = "123" @@ -38,7 +42,7 @@ public async Task Login_ShouldReturnOk() var authResult = new AuthResponseDto { - User = new UserDto + User = new Auth.Models.DTO.UserDto { Id = 1, Email = "test@mail.com", @@ -181,4 +185,46 @@ public async Task Logout_ShouldReturnOk_WhenTokenIsValid() It.IsAny()), Times.Once); } + + [Fact] + public async Task GoogleLogin_ShouldReturnOk_WhenTokenIsValid() + { + // Arrange + var request = new GoogleLoginRequest { IdToken = "valid-token" }; + var payload = new GoogleJsonWebSignature.Payload { Email = "test@test.com", GivenName = "John", FamilyName = "Doe" }; + + var authResponse = new AuthResponseDto + { + User = new UserDto + { + Id = 1, + Name = "John", + Surname = "Doe", + Email = "john@example.com", + Login = "johndoe", + Role = UserRole.MainAdministrator + }, + Token = "jwt-token", + RefreshToken = "refresh-token", + ExpireAt = DateTime.UtcNow.AddHours(1) + }; + + _googleAuthServiceMock.Setup(x => x.ValidateTokenAsync("valid-token")) + .ReturnsAsync(payload); + + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(authResponse)); + + var result = await _controller.GoogleLogin(request); + + // Assert + result.Should().BeOfType(); + + var okResult = result as OkObjectResult; + okResult?.Value.Should().BeEquivalentTo(authResponse); + + _mediatorMock.Verify( + x => x.Send(It.IsAny(), It.IsAny()), + Times.Once); + } } \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs new file mode 100644 index 00000000..644bec99 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using Microsoft.AspNetCore.Identity; +using Moq; +using Streetcode.Auth.MediatR.Users.LoginGoogle; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Models.Entities; +using Streetcode.Auth.Services.Interfaces.Logging; +using Streetcode.Auth.Services.Interfaces.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandlerTests + { + private readonly Mock> _userManagerMock; + private readonly Mock _authServiceMock; + private readonly Mock _mapperMock; + private readonly Mock _loggerMock; + private readonly GoogleLoginHandler _handler; + + public GoogleLoginHandlerTests() + { + var store = new Mock>(); + _userManagerMock = new Mock>(store.Object, null!, null!, null!, null!, null!, null!, null!, null!); + + _authServiceMock = new Mock(); + _mapperMock = new Mock(); + _loggerMock = new Mock(); + + _handler = new GoogleLoginHandler( + _userManagerMock.Object, + _authServiceMock.Object, + _loggerMock.Object, + _mapperMock.Object); + } + + [Fact] + public async Task Handle_ExistingUser_ReturnsSuccess() + { + // Arrange + var user = new User + { + Email = "test@test.com", + Id = 1, + Name = "Test", + Surname = "User" + }; + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "test@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(request.googleLoginRequest.Email)) + .ReturnsAsync(user); + + _authServiceMock.Setup(x => x.CreateLoginResultAsync(user)) + .ReturnsAsync(new AuthResponseDto + { + Token = "jwt", + RefreshToken = "refresh", + User = new UserDto { Id = 1 } + }); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("refresh", result.Value.RefreshToken); + _authServiceMock.Verify(x => x.CreateLoginResultAsync(user), Times.Once); + } + + [Fact] + public async Task Handle_NewUser_CreatesAndAssignsRole() + { + // Arrange + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "new@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(It.IsAny())) + .ReturnsAsync((User?)null); + + _userManagerMock.Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _userManagerMock.Setup(x => x.AddToRoleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _authServiceMock.Setup(x => x.CreateLoginResultAsync(It.IsAny())) + .ReturnsAsync(new AuthResponseDto + { + Token = "jwt", + RefreshToken = "refresh", + User = new UserDto { Id = 1 } + }); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + _userManagerMock.Verify(x => x.CreateAsync(It.IsAny()), Times.Once); + _userManagerMock.Verify(x => x.AddToRoleAsync(It.IsAny(), It.IsAny()), Times.Once); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs new file mode 100644 index 00000000..443f9717 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs @@ -0,0 +1,70 @@ +using FluentValidation.TestHelper; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.Validators.Users +{ + public class GoogleLoginRequestDtoValidatorTests + { + private readonly GoogleLoginRequestDtoValidator _validator; + + public GoogleLoginRequestDtoValidatorTests() + { + _validator = new GoogleLoginRequestDtoValidator(); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Empty() + { + var model = new GoogleLoginRequestDto { Email = "", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Invalid() + { + var model = new GoogleLoginRequestDto { Email = "invalid-email", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Should_Have_Error_When_Name_Is_Empty(string? name) + { + var model = new GoogleLoginRequestDto { Email = "test@test.com", Name = name!, Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Have_Error_When_Name_Is_Too_Long() + { + var model = new GoogleLoginRequestDto + { + Email = "test@test.com", + Name = new string('a', 51), + Surname = "Valid" + }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Not_Have_Error_When_Data_Is_Valid() + { + var model = new GoogleLoginRequestDto + { + Email = "valid@test.com", + Name = "John", + Surname = "Doe" + }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs new file mode 100644 index 00000000..8ba9a9ee --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/AuthService/Validators/Users/GoogleLoginRequestValidatorTests.cs @@ -0,0 +1,43 @@ +using FluentValidation.TestHelper; +using Streetcode.Auth.Models.DTO; +using Streetcode.Auth.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.AuthService.Validators.Users +{ + public class GoogleLoginRequestValidatorTests + { + private readonly GoogleLoginRequestValidator _validator; + + public GoogleLoginRequestValidatorTests() + { + _validator = new GoogleLoginRequestValidator(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Should_Have_Error_When_IdToken_Is_Empty(string? token) + { + var model = new GoogleLoginRequest { IdToken = token! }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Have_Error_When_IdToken_Is_Too_Short() + { + var model = new GoogleLoginRequest { IdToken = "short-token" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Not_Have_Error_When_IdToken_Is_Valid_Length() + { + var model = new GoogleLoginRequest { IdToken = new string('a', 101) }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs new file mode 100644 index 00000000..51f7369c --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs @@ -0,0 +1,97 @@ +using System.IdentityModel.Tokens.Jwt; +using AutoMapper; +using Microsoft.AspNetCore.Identity; +using Moq; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Interfaces.Logging; +using Streetcode.BLL.Interfaces.Users; +using Streetcode.BLL.MediatR.Users.LoginGoogle; +using Streetcode.DAL.Entities.Users; + +using Xunit; + +namespace Streetcode.XUnitTest.BLL.MediatR.Users.LoginGoogle +{ + public class GoogleLoginHandlerTests + { + private readonly Mock> _userManagerMock; + private readonly Mock _tokenServiceMock; + private readonly Mock _mapperMock; + private readonly Mock _loggerMock; + private readonly GoogleLoginHandler _handler; + + public GoogleLoginHandlerTests() + { + var store = new Mock>(); + _userManagerMock = new Mock>(store.Object, null!, null!, null!, null!, null!, null!, null!, null!); + + _tokenServiceMock = new Mock(); + _mapperMock = new Mock(); + _loggerMock = new Mock(); + + _handler = new GoogleLoginHandler( + _userManagerMock.Object, + _tokenServiceMock.Object, + _loggerMock.Object, + _mapperMock.Object); + } + + [Fact] + public async Task Handle_ExistingUser_ReturnsSuccess() + { + // Arrange + var user = new User + { + Email = "test@test.com", + Id = 1, + Name = "Test", + Surname = "User" + }; + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "test@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(request.googleLoginRequest.Email)) + .ReturnsAsync(user); + + _tokenServiceMock.Setup(x => x.GenerateJWTToken(user)) + .Returns(new JwtSecurityToken()); + + _tokenServiceMock.Setup(x => x.GenerateRefreshToken()) + .Returns("refresh_token"); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal("refresh_token", result.Value.RefreshToken); + _userManagerMock.Verify(x => x.UpdateAsync(It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_NewUser_CreatesAndAssignsRole() + { + // Arrange + var request = new GoogleLoginCommand(new GoogleLoginRequestDto { Email = "new@test.com" }); + + _userManagerMock.Setup(x => x.FindByEmailAsync(It.IsAny())) + .ReturnsAsync((User?)null); + + _userManagerMock.Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _userManagerMock.Setup(x => x.AddToRoleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityResult.Success); + + _tokenServiceMock.Setup(x => x.GenerateJWTToken(It.IsAny())) + .Returns(new JwtSecurityToken()); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + Assert.True(result.IsSuccess); + _userManagerMock.Verify(x => x.CreateAsync(It.IsAny()), Times.Once); + _userManagerMock.Verify(x => x.AddToRoleAsync(It.IsAny(), It.IsAny()), Times.Once); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs new file mode 100644 index 00000000..07716de6 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestDtoValidatorTests.cs @@ -0,0 +1,70 @@ +using FluentValidation.TestHelper; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.BLL.Validators.Users +{ + public class GoogleLoginRequestDtoValidatorTests + { + private readonly GoogleLoginRequestDtoValidator _validator; + + public GoogleLoginRequestDtoValidatorTests() + { + _validator = new GoogleLoginRequestDtoValidator(); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Empty() + { + var model = new GoogleLoginRequestDto { Email = "", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Fact] + public void Should_Have_Error_When_Email_Is_Invalid() + { + var model = new GoogleLoginRequestDto { Email = "invalid-email", Name = "Valid", Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Email); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Should_Have_Error_When_Name_Is_Empty(string? name) + { + var model = new GoogleLoginRequestDto { Email = "test@test.com", Name = name!, Surname = "Valid" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Have_Error_When_Name_Is_Too_Long() + { + var model = new GoogleLoginRequestDto + { + Email = "test@test.com", + Name = new string('a', 51), + Surname = "Valid" + }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.Name); + } + + [Fact] + public void Should_Not_Have_Error_When_Data_Is_Valid() + { + var model = new GoogleLoginRequestDto + { + Email = "valid@test.com", + Name = "John", + Surname = "Doe" + }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs new file mode 100644 index 00000000..2df9d368 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/BLL/Validators/Users/GoogleLoginRequestValidatorTests.cs @@ -0,0 +1,43 @@ +using FluentValidation.TestHelper; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.Validators.Users; +using Xunit; + +namespace Streetcode.XUnitTest.BLL.Validators.Users +{ + public class GoogleLoginRequestValidatorTests + { + private readonly GoogleLoginRequestValidator _validator; + + public GoogleLoginRequestValidatorTests() + { + _validator = new GoogleLoginRequestValidator(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Should_Have_Error_When_IdToken_Is_Empty(string? token) + { + var model = new GoogleLoginRequest { IdToken = token! }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Have_Error_When_IdToken_Is_Too_Short() + { + var model = new GoogleLoginRequest { IdToken = "short-token" }; + var result = _validator.TestValidate(model); + result.ShouldHaveValidationErrorFor(x => x.IdToken); + } + + [Fact] + public void Should_Not_Have_Error_When_IdToken_Is_Valid_Length() + { + var model = new GoogleLoginRequest { IdToken = new string('a', 101) }; + var result = _validator.TestValidate(model); + result.ShouldNotHaveAnyValidationErrors(); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs b/Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs new file mode 100644 index 00000000..53cb8ed2 --- /dev/null +++ b/Streetcode/Streetcode.XUnitTest/WebApi/Controllers/AuthControllerTests.cs @@ -0,0 +1,145 @@ +using System.Security.Claims; +using FluentAssertions; +using FluentResults; +using Google.Apis.Auth; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Streetcode.BLL.DTO.Users; +using Streetcode.BLL.MediatR.Users.Login; +using Streetcode.BLL.MediatR.Users.LoginGoogle; +using Streetcode.BLL.MediatR.Users.RefreshToken; +using Streetcode.BLL.MediatR.Users.Register; +using Streetcode.DAL.Enums; +using Streetcode.WebApi.Controllers; +using Streetcode.WebApi.Controllers.Users; +using Streetcode.WebApi.Service.Interfaces; +using Xunit; + +namespace Streetcode.XUnitTest.WebApi.Controllers; + +public class AuthControllerTests +{ + private readonly Mock _mediatorMock; + private readonly Mock _googleAuthServiceMock; + private readonly AuthController _controller; + + public AuthControllerTests() + { + _mediatorMock = new Mock(); + _googleAuthServiceMock = new Mock(); + + _controller = new AuthController(_googleAuthServiceMock.Object); + + var field = typeof(BaseApiController).GetField( + "k__BackingField", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + + if (field != null) + { + field.SetValue(_controller, _mediatorMock.Object); + } + else + { + throw new Exception("Не удалось найти поле k__BackingField в BaseApiController."); + } + } + + private static LoginResultDto CreateLoginResult() => new LoginResultDto + { + User = new UserDto { Id = 1, Email = "test@mail.com", Name = "John", Surname = "Doe", Login = "jd", Role = UserRole.MainAdministrator }, + Token = "jwt-token", + RefreshToken = "refresh-token", + ExpireAt = DateTime.UtcNow.AddHours(1) + }; + + [Fact] + public async Task Login_ShouldReturnOk() + { + var request = new UserLoginDto { Login = "test", Password = "123" }; + var loginResult = CreateLoginResult(); + + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(loginResult)); + + var result = await _controller.Login(request); + + result.Should().BeOfType(); + ((OkObjectResult)result).Value.Should().BeEquivalentTo(loginResult); + } + + [Fact] + public async Task Register_ShouldReturnOk() + { + // Arrange + var request = new UserRegisterDto + { + Email = "test@mail.com", + Password = "123", + PasswordConfirmation = "123", + Name = "John", + Surname = "Doe" + }; + + _mediatorMock + .Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok()); + + // Act + var result = await _controller.Register(request); + + // Assert + result.Should().BeOfType(); + + _mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task RefreshToken_ShouldReturnOk() + { + var request = new RefreshTokenRequestDto { Token = "tiken", RefreshToken = "token" }; + var loginResult = CreateLoginResult(); + + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(loginResult)); + + var result = await _controller.RefreshToken(request); + + result.Should().BeOfType(); + ((OkObjectResult)result).Value.Should().BeEquivalentTo(loginResult); + } + + [Fact] + public async Task Logout_ShouldReturnUnauthorized_WhenUserClaimsAreInvalid() + { + // Arrange + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity()) } + }; + + // Act + var result = await _controller.Logout(CancellationToken.None); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task GoogleLogin_ShouldReturnOk_WhenTokenIsValid() + { + var request = new GoogleLoginRequest { IdToken = "valid-token" }; + var payload = new GoogleJsonWebSignature.Payload { Email = "test@test.com", GivenName = "John", FamilyName = "Doe" }; + var loginResult = CreateLoginResult(); + + _googleAuthServiceMock.Setup(x => x.ValidateTokenAsync("valid-token")).ReturnsAsync(payload); + _mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Ok(loginResult)); + + var result = await _controller.GoogleLogin(request); + + result.Should().BeOfType(); + ((OkObjectResult)result).Value.Should().BeEquivalentTo(loginResult); + } +} \ No newline at end of file From c092a0042374527c4ff5e143a56d10150599b5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 19:32:00 +0300 Subject: [PATCH 06/10] fix --- .../Streetcode.WebApi/Controllers/Users/AuthController.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs index 2a322205..fbba432d 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Users/AuthController.cs @@ -1,4 +1,5 @@ -using Google.Apis.Auth; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.BLL.DTO.Users; @@ -8,8 +9,6 @@ using Streetcode.BLL.MediatR.Users.RefreshToken; using Streetcode.BLL.MediatR.Users.Register; using Streetcode.WebApi.Service.Interfaces; -using System.Diagnostics.CodeAnalysis; -using System.Security.Claims; namespace Streetcode.WebApi.Controllers.Users; From 75780e1351565f3a8b3e8455722f16706fd7487d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 19:50:05 +0300 Subject: [PATCH 07/10] fix sonar issues --- Streetcode/DbUpdate/Program.cs | 3 +-- Streetcode/Streetcode.Auth/Program.cs | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Streetcode/DbUpdate/Program.cs b/Streetcode/DbUpdate/Program.cs index de935da6..467ab9f1 100644 --- a/Streetcode/DbUpdate/Program.cs +++ b/Streetcode/DbUpdate/Program.cs @@ -5,8 +5,7 @@ public class Program { static int Main(string[] args) { - string migrationPath = Path.Combine(Directory.GetCurrentDirectory(), - "Streetcode.DAL", "Persistence", "ScriptsMigration"); + string migrationPath = Path.Combine(Directory.GetCurrentDirectory(),"Streetcode.DAL", "Persistence", "ScriptsMigration"); var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Local"; diff --git a/Streetcode/Streetcode.Auth/Program.cs b/Streetcode/Streetcode.Auth/Program.cs index eda42e7e..71132378 100644 --- a/Streetcode/Streetcode.Auth/Program.cs +++ b/Streetcode/Streetcode.Auth/Program.cs @@ -5,6 +5,8 @@ using Streetcode.Auth.MediatR.Behaviors; using Streetcode.Auth.Services; using Streetcode.Auth.Services.Interfaces; +using Streetcode.Auth.Services.Interfaces.Users; +using Streetcode.Auth.Services.Users; using Streetcode.Common.Models; using Streetcode.Shared.Web.Middleware; using System.Reflection; From 5178f15f44fbf56c8e58ccb28a3381b0f3df1d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 20:35:50 +0300 Subject: [PATCH 08/10] fix dublicate in rhe code --- .../Users/LoginGoogle/GoogleLoginHandler.cs | 50 +++++++--------- .../Users/GoogleLoginRequestDtoValidator.cs | 19 ++++-- .../Users/LoginGoogle/GoogleLoginHandler.cs | 59 +++++++++---------- .../Users/GoogleLoginRequestDtoValidator.cs | 9 +-- .../Validators/Users/ValidationExtensions.cs | 15 +++++ .../LoginGoogle/GoogleLoginHandlerTests.cs | 8 +-- 6 files changed, 82 insertions(+), 78 deletions(-) create mode 100644 Streetcode/Streetcode.BLL/Validators/Users/ValidationExtensions.cs diff --git a/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs index c8a2f0eb..b22566b9 100644 --- a/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs +++ b/Streetcode/Streetcode.Auth/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using FluentResults; +using FluentResults; using MediatR; using Microsoft.AspNetCore.Identity; using Streetcode.Auth.Models.DTO; @@ -14,59 +13,52 @@ public class GoogleLoginHandler : IRequestHandler _userManager; private readonly IAuthService _authService; - private readonly IMapper _mapper; private readonly ILoggerService _logger; public GoogleLoginHandler( UserManager userManager, IAuthService authService, - ILoggerService logger, - IMapper mapper) + ILoggerService logger) { _userManager = userManager; _authService = authService; _logger = logger; - _mapper = mapper; } public async Task> Handle(GoogleLoginCommand request, CancellationToken cancellationToken) { - _logger.LogInformation($"Google login attempt for {request.googleLoginRequest.Email}"); + var loginDto = request.googleLoginRequest; + _logger.LogInformation($"Google login attempt for {loginDto.Email}"); - var user = await _userManager.FindByEmailAsync(request.googleLoginRequest.Email); + var user = await _userManager.FindByEmailAsync(loginDto.Email); if (user == null) { - user = new User - { - Email = request.googleLoginRequest.Email, - UserName = request.googleLoginRequest.Email, - Name = request.googleLoginRequest.Name, - Surname = request.googleLoginRequest.Surname - }; + user = new User { Email = loginDto.Email, UserName = loginDto.Email, Name = loginDto.Name, Surname = loginDto.Surname }; - var result = await _userManager.CreateAsync(user); - if (!result.Succeeded) - { - _logger.LogError(request, $"Failed to create user {request.googleLoginRequest.Email}: {string.Join(", ", result.Errors.Select(e => e.Description))}"); - return Result.Fail("Failed to create user account."); - } + var createResult = await _userManager.CreateAsync(user); + var errorResult = CheckIdentityResult(createResult, request, $"Failed to create user {loginDto.Email}"); + if (errorResult != null) return errorResult; var roleResult = await _userManager.AddToRoleAsync(user, UserRole.MainAdministrator.ToString()); - if (!roleResult.Succeeded) - { - _logger.LogError(request, $"Failed to add role for user {user.Id}: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); - return Result.Fail("User created but failed to assign role."); - } + errorResult = CheckIdentityResult(roleResult, request, $"Failed to add role for user {user.Id}"); + if (errorResult != null) return errorResult; } - var registrResult = await _authService.CreateLoginResultAsync(user); - + var authResult = await _authService.CreateLoginResultAsync(user); _logger.LogInformation($"User {user.Id} successfully logged in"); + return Result.Ok(authResult); + } + + private Result CheckIdentityResult(IdentityResult result, object request, string errorMessage) + { + if (result.Succeeded) return null!; - return Result.Ok(registrResult); + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + _logger.LogError(request, $"{errorMessage}: {errors}"); + return Result.Fail(errorMessage); } } } diff --git a/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs index 3c5feb25..9539dc7c 100644 --- a/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs +++ b/Streetcode/Streetcode.Auth/Validators/Users/GoogleLoginRequestDtoValidator.cs @@ -9,16 +9,23 @@ public class GoogleLoginRequestDtoValidator : AbstractValidator x.Email) - .NotEmpty().WithMessage(ErrorMessages.EmailIsRequired) - .EmailAddress().WithMessage(ErrorMessages.InvalidEmailFormat); + .ValidEmail( + maxLength: 255, + requiredMessage: ErrorMessages.EmailIsRequired, + formatMessage: ErrorMessages.InvalidEmailFormat, + lengthMessage: ErrorMessages.EmailMustNotExceedCharacters); RuleFor(x => x.Name) - .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) - .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + .RequiredWithMaxLength( + maxLength: 50, + requiredMessage: ErrorMessages.NameIsRequired, + lengthMessage: ErrorMessages.NameMustNotExceedCharacters); RuleFor(x => x.Surname) - .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) - .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + .RequiredWithMaxLength( + maxLength: 50, + requiredMessage: ErrorMessages.NameIsRequired, + lengthMessage: ErrorMessages.NameMustNotExceedCharacters); } } } diff --git a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs index 72e924ff..02713c3a 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -16,53 +16,52 @@ public class GoogleLoginHandler : IRequestHandler _userManager; private readonly ITokenService _tokenService; private readonly IMapper _mapper; - private readonly ILoggerService _logger; - public GoogleLoginHandler( - UserManager userManager, - ITokenService tokenService, - ILoggerService logger, - IMapper mapper) + public GoogleLoginHandler(UserManager userManager, ITokenService tokenService, ILoggerService logger, IMapper mapper) { _userManager = userManager; _tokenService = tokenService; - _logger = logger; _mapper = mapper; } public async Task> Handle(GoogleLoginCommand request, CancellationToken cancellationToken) { - _logger.LogInformation($"Google login attempt for {request.googleLoginRequest.Email}"); + var req = request.googleLoginRequest; + _logger.LogInformation($"Google login attempt for {req.Email}"); - var user = await _userManager.FindByEmailAsync(request.googleLoginRequest.Email); + var user = await _userManager.FindByEmailAsync(req.Email); if (user == null) { - user = new User - { - Email = request.googleLoginRequest.Email, - UserName = request.googleLoginRequest.Email, - Name = request.googleLoginRequest.Name, - Surname = request.googleLoginRequest.Surname - }; + user = new User { Email = req.Email, UserName = req.Email, Name = req.Name, Surname = req.Surname }; - var result = await _userManager.CreateAsync(user); - if (!result.Succeeded) - { - _logger.LogError(request, $"Failed to create user {request.googleLoginRequest.Email}: {string.Join(", ", result.Errors.Select(e => e.Description))}"); - return Result.Fail("Failed to create user account."); - } + var createResult = await _userManager.CreateAsync(user); + if (IsFailure(createResult, request, $"Failed to create user {req.Email}", out var error)) return error; var roleResult = await _userManager.AddToRoleAsync(user, UserRole.MainAdministrator.ToString()); - if (!roleResult.Succeeded) - { - _logger.LogError(request, $"Failed to add role for user {user.Id}: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); - return Result.Fail("User created but failed to assign role."); - } + if (IsFailure(roleResult, request, $"Failed to add role for user {user.Id}", out error)) return error; + } + + return Result.Ok(await GenerateLoginResult(user)); + } + + private bool IsFailure(IdentityResult result, object request, string message, out Result failure) + { + failure = null!; + if (result.Succeeded) + { + return false; } + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + _logger.LogError(request, $"{message}: {errors}"); + failure = Result.Fail(message); + return true; + } + private async Task GenerateLoginResult(User user) + { var jwtToken = _tokenService.GenerateJWTToken(user); var refreshToken = _tokenService.GenerateRefreshToken(); @@ -72,13 +71,13 @@ public async Task> Handle(GoogleLoginCommand request, Can _logger.LogInformation($"User {user.Id} successfully logged in"); - return Result.Ok(new LoginResultDto + return new LoginResultDto { User = _mapper.Map(user), Token = new JwtSecurityTokenHandler().WriteToken(jwtToken), RefreshToken = refreshToken, ExpireAt = jwtToken.ValidTo - }); + }; } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs index a71ced36..1179f585 100644 --- a/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs +++ b/Streetcode/Streetcode.BLL/Validators/Users/GoogleLoginRequestDtoValidator.cs @@ -12,13 +12,8 @@ public GoogleLoginRequestDtoValidator() .NotEmpty().WithMessage(ErrorMessages.EmailIsRequired) .EmailAddress().WithMessage(ErrorMessages.InvalidEmailFormat); - RuleFor(x => x.Name) - .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) - .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); - - RuleFor(x => x.Surname) - .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) - .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + RuleFor(x => x.Name).MustBeValidName(); + RuleFor(x => x.Surname).MustBeValidName(); } } } diff --git a/Streetcode/Streetcode.BLL/Validators/Users/ValidationExtensions.cs b/Streetcode/Streetcode.BLL/Validators/Users/ValidationExtensions.cs new file mode 100644 index 00000000..c386d76d --- /dev/null +++ b/Streetcode/Streetcode.BLL/Validators/Users/ValidationExtensions.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Streetcode.BLL.Resources; + +namespace Streetcode.BLL.Validators.Users +{ + public static class ValidationExtensions + { + public static IRuleBuilderOptions MustBeValidName(this IRuleBuilder ruleBuilder) + { + return ruleBuilder + .NotEmpty().WithMessage(ErrorMessages.NameIsRequired) + .MaximumLength(50).WithMessage(string.Format(ErrorMessages.NameMustNotExceedCharacters, 50)); + } + } +} diff --git a/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs b/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs index 644bec99..82a4307b 100644 --- a/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs +++ b/Streetcode/Streetcode.XUnitTest/AuthService/MediatR/Users/LoginGoogle/GoogleLoginHandlerTests.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Moq; using Streetcode.Auth.MediatR.Users.LoginGoogle; using Streetcode.Auth.Models.DTO; @@ -14,7 +13,6 @@ public class GoogleLoginHandlerTests { private readonly Mock> _userManagerMock; private readonly Mock _authServiceMock; - private readonly Mock _mapperMock; private readonly Mock _loggerMock; private readonly GoogleLoginHandler _handler; @@ -24,14 +22,12 @@ public GoogleLoginHandlerTests() _userManagerMock = new Mock>(store.Object, null!, null!, null!, null!, null!, null!, null!, null!); _authServiceMock = new Mock(); - _mapperMock = new Mock(); _loggerMock = new Mock(); _handler = new GoogleLoginHandler( _userManagerMock.Object, _authServiceMock.Object, - _loggerMock.Object, - _mapperMock.Object); + _loggerMock.Object); } [Fact] From bee7715ce1715596dec8479b87fb01e7d3b8ab26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 20:46:36 +0300 Subject: [PATCH 09/10] fix --- Streetcode/DbUpdate/Program.cs | 73 ++++++++++--------- .../Interfaces/Users/IGoogleAuthService.cs | 2 +- .../Users/LoginGoogle/GoogleLoginHandler.cs | 10 ++- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/Streetcode/DbUpdate/Program.cs b/Streetcode/DbUpdate/Program.cs index 467ab9f1..1339b3cf 100644 --- a/Streetcode/DbUpdate/Program.cs +++ b/Streetcode/DbUpdate/Program.cs @@ -1,53 +1,56 @@ using DbUp; using Microsoft.Extensions.Configuration; -public class Program +namespace Streetcode.DbUpdate { - static int Main(string[] args) + public class Program { - string migrationPath = Path.Combine(Directory.GetCurrentDirectory(),"Streetcode.DAL", "Persistence", "ScriptsMigration"); + private static int Main(string[] args) + { + string migrationPath = Path.Combine(Directory.GetCurrentDirectory(), "Streetcode.DAL", "Persistence", "ScriptsMigration"); - var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Local"; + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Local"; - var configuration = new ConfigurationBuilder() - .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "Streetcode.WebApi")) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables("STREETCODE_") - .Build(); + var configuration = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "Streetcode.WebApi")) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables("STREETCODE_") + .Build(); - var connectionString = configuration.GetConnectionString("DefaultConnection"); + var connectionString = configuration.GetConnectionString("DefaultConnection"); - string pathToScript = ""; + string pathToScript = ""; - Console.WriteLine("Enter '-m' to MIGRATE or '-s' to SEED db:"); - pathToScript = Console.ReadLine(); + Console.WriteLine("Enter '-m' to MIGRATE or '-s' to SEED db:"); + pathToScript = Console.ReadLine() ?? string.Empty; - pathToScript = migrationPath; - - var upgrader = - DeployChanges.To - .SqlDatabase(connectionString) - .WithScriptsFromFileSystem(pathToScript) - .LogToConsole() - .Build(); + pathToScript = migrationPath; - var result = upgrader.PerformUpgrade(); + var upgrader = + DeployChanges.To + .SqlDatabase(connectionString) + .WithScriptsFromFileSystem(pathToScript) + .LogToConsole() + .Build(); - if (!result.Successful) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(result.Error); - Console.ResetColor(); + var result = upgrader.PerformUpgrade(); + + if (!result.Successful) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(result.Error); + Console.ResetColor(); #if DEBUG - Console.ReadLine(); + Console.ReadLine(); #endif - return -1; - } + return -1; + } - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Success!"); - Console.ResetColor(); - return 0; + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Success!"); + Console.ResetColor(); + return 0; + } } } \ No newline at end of file diff --git a/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs b/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs index c9f59e32..3f43ede2 100644 --- a/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs +++ b/Streetcode/Streetcode.Auth/Services/Interfaces/Users/IGoogleAuthService.cs @@ -4,6 +4,6 @@ namespace Streetcode.Auth.Services.Interfaces.Users { public interface IGoogleAuthService { - Task ValidateTokenAsync(string idToken); + Task ValidateTokenAsync(string idToken); } } diff --git a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs index 02713c3a..41355dd9 100644 --- a/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs +++ b/Streetcode/Streetcode.BLL/MediatR/Users/LoginGoogle/GoogleLoginHandler.cs @@ -38,10 +38,16 @@ public async Task> Handle(GoogleLoginCommand request, Can user = new User { Email = req.Email, UserName = req.Email, Name = req.Name, Surname = req.Surname }; var createResult = await _userManager.CreateAsync(user); - if (IsFailure(createResult, request, $"Failed to create user {req.Email}", out var error)) return error; + if (IsFailure(createResult, request, $"Failed to create user {req.Email}", out var error)) + { + return error; + } var roleResult = await _userManager.AddToRoleAsync(user, UserRole.MainAdministrator.ToString()); - if (IsFailure(roleResult, request, $"Failed to add role for user {user.Id}", out error)) return error; + if (IsFailure(roleResult, request, $"Failed to add role for user {user.Id}", out error)) + { + return error; + } } return Result.Ok(await GenerateLoginResult(user)); From 2e9f78d48be2b6eb2d2b40589b2c078570eb1d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Tue, 16 Jun 2026 20:56:30 +0300 Subject: [PATCH 10/10] fix --- Streetcode/DbUpdate/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Streetcode/DbUpdate/Program.cs b/Streetcode/DbUpdate/Program.cs index 1339b3cf..ae4643c4 100644 --- a/Streetcode/DbUpdate/Program.cs +++ b/Streetcode/DbUpdate/Program.cs @@ -23,7 +23,7 @@ private static int Main(string[] args) string pathToScript = ""; Console.WriteLine("Enter '-m' to MIGRATE or '-s' to SEED db:"); - pathToScript = Console.ReadLine() ?? string.Empty; + var command = Console.ReadLine(); pathToScript = migrationPath;