From f1881154192811f6dda4a776575205331895a565 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:25:26 +0100 Subject: [PATCH 01/10] Implement logic for getting customer by email --- .../Abstractions/ICustomerRepository.cs | 7 ++++-- .../Abstractions/ICustomerService.cs | 7 ++++-- .../Repositories/CustomerRepository.cs | 25 +++++++++++++++++-- .../Customers/Services/CustomerService.cs | 16 ++++++++++-- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 8dc6310..326b3eb 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Customers.Abstractions; +using TickAPI.Common.Result; +using TickAPI.Customers.Models; + +namespace TickAPI.Customers.Abstractions; public interface ICustomerRepository { - + Task> GetCustomerByEmailAsync(string customerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs index 8371ac2..e48ab11 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Customers.Abstractions; +using TickAPI.Common.Result; +using TickAPI.Customers.Models; + +namespace TickAPI.Customers.Abstractions; public interface ICustomerService { - + Task> GetCustomerByEmailAsync(string customerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index 8b21f7e..5dcaff7 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -1,8 +1,29 @@ -using TickAPI.Customers.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Result; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; namespace TickAPI.Customers.Repositories; public class CustomerRepository : ICustomerRepository { - + private readonly TickApiDbContext _tickApiDbContext; + + public CustomerRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + public async Task> GetCustomerByEmailAsync(string customerEmail) + { + var customer = await _tickApiDbContext.Customers.FirstOrDefaultAsync(customer => customer.Email == customerEmail); + + if (customer == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{customerEmail}' not found"); + } + + return Result.Success(customer); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Services/CustomerService.cs b/TickAPI/TickAPI/Customers/Services/CustomerService.cs index 4b75299..a4bfe77 100644 --- a/TickAPI/TickAPI/Customers/Services/CustomerService.cs +++ b/TickAPI/TickAPI/Customers/Services/CustomerService.cs @@ -1,8 +1,20 @@ -using TickAPI.Customers.Abstractions; +using TickAPI.Common.Result; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; namespace TickAPI.Customers.Services; public class CustomerService : ICustomerService { - + private readonly ICustomerRepository _customerRepository; + + public CustomerService(ICustomerRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task> GetCustomerByEmailAsync(string customerEmail) + { + return await _customerRepository.GetCustomerByEmailAsync(customerEmail); + } } \ No newline at end of file From 1cd6bfa9fef77d77a6baf234510c015cdec40aff Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:26:24 +0100 Subject: [PATCH 02/10] Create DTOs for login via google request for customer. --- TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs | 5 +++++ .../Customers/DTOs/Response/GoogleLoginResponseDto.cs | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs create mode 100644 TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs new file mode 100644 index 0000000..043b7ce --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Customers.DTOs.Request; + +public record GoogleLoginDto( + string IdToken +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs new file mode 100644 index 0000000..b2d5434 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleLoginResponseDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record GoogleLoginResponseDto( + string Token, + bool IsNewCustomer +); \ No newline at end of file From 6a3d0bad15c994619037002f8e4d5424baa232b2 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:27:16 +0100 Subject: [PATCH 03/10] Create endpoint for login via google for customer --- .../Controllers/CustomerController.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 44599d3..5ff2c17 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,4 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.DTOs.Request; +using TickAPI.Customers.DTOs.Response; namespace TickAPI.Customers.Controllers; @@ -6,5 +11,43 @@ namespace TickAPI.Customers.Controllers; [Route("api/[controller]")] public class CustomerController : ControllerBase { + private readonly IAuthService _authService; + private readonly IJwtService _jwtService; + private readonly ICustomerService _customerService; + public CustomerController(IAuthService authService, IJwtService jwtService, ICustomerService customerService) + { + _authService = authService; + _jwtService = jwtService; + _customerService = customerService; + } + + [HttpPost("google-login")] + public async Task> GoogleLogin([FromBody] GoogleLoginDto request) + { + var loginResult = await _authService.LoginAsync(request.IdToken); + if(loginResult.IsError) + return StatusCode(loginResult.StatusCode, loginResult.ErrorMsg); + + UserRole role; + bool isNewCustomer; + + var existingCustomerResult = await _customerService.GetCustomerByEmailAsync(loginResult.Value!); + if (existingCustomerResult.IsSuccess) + { + role = UserRole.Customer; + isNewCustomer = false; + } + else + { + role = UserRole.NewCustomer; + isNewCustomer = true; + } + + var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, role); + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!, isNewCustomer)); + } } \ No newline at end of file From b679fe50e6fbd7d249a5b798ed0b8514467d183f Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:41:40 +0100 Subject: [PATCH 04/10] Add our own `Authorize` attribute built on top of the original one to ensure enum type-safety --- .../Common/Auth/Attributes/AuthorizeWithPolicy.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs diff --git a/TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs b/TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs new file mode 100644 index 0000000..eba7fe9 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Authorization; +using TickAPI.Common.Auth.Enums; + +namespace TickAPI.Common.Auth.Attributes; + +public class AuthorizeWithPolicy : AuthorizeAttribute +{ + public AuthorizeWithPolicy(AuthPolicies policy) + { + Policy = policy.ToString(); + } +} \ No newline at end of file From f1e4fe929eebeb03ba185b5fa8befc3eb4267a33 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:57:48 +0100 Subject: [PATCH 05/10] Implement creating new customer --- .../Abstractions/ICustomerRepository.cs | 1 + .../Abstractions/ICustomerService.cs | 1 + .../Repositories/CustomerRepository.cs | 6 +++++ .../Customers/Services/CustomerService.cs | 24 ++++++++++++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 326b3eb..5177854 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -6,4 +6,5 @@ namespace TickAPI.Customers.Abstractions; public interface ICustomerRepository { Task> GetCustomerByEmailAsync(string customerEmail); + Task AddNewCustomerAsync(Customer customer); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs index e48ab11..ea4e7b8 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs @@ -6,4 +6,5 @@ namespace TickAPI.Customers.Abstractions; public interface ICustomerService { Task> GetCustomerByEmailAsync(string customerEmail); + Task> CreateNewCustomerAsync(string email, string firstName, string lastName); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index 5dcaff7..9adb003 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -26,4 +26,10 @@ public async Task> GetCustomerByEmailAsync(string customerEmail return Result.Success(customer); } + + public async Task AddNewCustomerAsync(Customer customer) + { + _tickApiDbContext.Customers.Add(customer); + await _tickApiDbContext.SaveChangesAsync(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Services/CustomerService.cs b/TickAPI/TickAPI/Customers/Services/CustomerService.cs index a4bfe77..33289c7 100644 --- a/TickAPI/TickAPI/Customers/Services/CustomerService.cs +++ b/TickAPI/TickAPI/Customers/Services/CustomerService.cs @@ -1,20 +1,42 @@ using TickAPI.Common.Result; +using TickAPI.Common.Time.Abstractions; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Models; +using TickAPI.Tickets.Models; namespace TickAPI.Customers.Services; public class CustomerService : ICustomerService { private readonly ICustomerRepository _customerRepository; + private readonly IDateTimeService _dateTimeService; - public CustomerService(ICustomerRepository customerRepository) + public CustomerService(ICustomerRepository customerRepository, IDateTimeService dateTimeService) { _customerRepository = customerRepository; + _dateTimeService = dateTimeService; } public async Task> GetCustomerByEmailAsync(string customerEmail) { return await _customerRepository.GetCustomerByEmailAsync(customerEmail); } + + public async Task> CreateNewCustomerAsync(string email, string firstName, string lastName) + { + var alreadyExistingResult = await GetCustomerByEmailAsync(email); + if (alreadyExistingResult.IsSuccess) + return Result.Failure(StatusCodes.Status400BadRequest, + $"customer with email '{email}' already exists"); + var customer = new Customer + { + Email = email, + FirstName = firstName, + LastName = lastName, + CreationDate = _dateTimeService.GetCurrentDateTime(), + Tickets = new List() + }; + await _customerRepository.AddNewCustomerAsync(customer); + return Result.Success(customer); + } } \ No newline at end of file From 2dfa5a082561779ddbc5343996b4340de768dc03 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 16 Mar 2025 23:58:23 +0100 Subject: [PATCH 06/10] Add dtos for creating new customer account via google request --- .../Customers/DTOs/Request/GoogleCreateNewAccountDto.cs | 6 ++++++ .../DTOs/Response/GoogleCreateNewAccountResponseDto.cs | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs create mode 100644 TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs new file mode 100644 index 0000000..f896265 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCreateNewAccountDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Customers.DTOs.Request; + +public record GoogleCreateNewAccountDto( + string FirstName, + string LastName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs new file mode 100644 index 0000000..b49ac83 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCreateNewAccountResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record GoogleCreateNewAccountResponseDto( + string Token +); \ No newline at end of file From 7e48f34b37d3b2d5947f4ba9d481ee64681fb8e4 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 08:54:33 +0100 Subject: [PATCH 07/10] Create endpoint for creating new account via google --- .../Controllers/CustomerController.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 5ff2c17..28433f9 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.JsonWebTokens; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Customers.Abstractions; using TickAPI.Customers.DTOs.Request; @@ -45,9 +47,27 @@ public async Task> GoogleLogin([FromBody] G } var jwtTokenResult = _jwtService.GenerateJwtToken(loginResult.Value, role); - if(jwtTokenResult.IsError) + if (jwtTokenResult.IsError) return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); return new ActionResult(new GoogleLoginResponseDto(jwtTokenResult.Value!, isNewCustomer)); } + + [AuthorizeWithPolicy(AuthPolicies.NewCustomerPolicy)] + public async Task> GoogleCreateNewAccount([FromBody] GoogleCreateNewAccountDto request) + { + var email = User.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Email)?.Value; + if (email == null) + return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); + + var newCustomerResult = await _customerService.CreateNewCustomerAsync(email, request.FirstName, request.LastName); + if (newCustomerResult.IsError) + return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); + + var jwtTokenResult = _jwtService.GenerateJwtToken(newCustomerResult.Value!.Email, UserRole.Customer); + if (jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleCreateNewAccountResponseDto(jwtTokenResult.Value!)); + } } \ No newline at end of file From 3469ccd1954ca83be29c7dc2e67822840bc4eb37 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 09:36:50 +0100 Subject: [PATCH 08/10] Small adjustments in endpoint for creating new account --- .../TickAPI/Customers/Controllers/CustomerController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 28433f9..fabb150 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.IdentityModel.JsonWebTokens; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; using TickAPI.Common.Auth.Abstractions; using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; @@ -54,9 +54,10 @@ public async Task> GoogleLogin([FromBody] G } [AuthorizeWithPolicy(AuthPolicies.NewCustomerPolicy)] + [HttpPost("google-create-new-account")] public async Task> GoogleCreateNewAccount([FromBody] GoogleCreateNewAccountDto request) { - var email = User.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Email)?.Value; + var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; if (email == null) return StatusCode(StatusCodes.Status400BadRequest, "missing email claim"); From f442b18d4704a81557a9a31790d1176a604a5adc Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 11:40:14 +0100 Subject: [PATCH 09/10] Create tests for `CustomerService` --- .../Services/CustomerServiceTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs diff --git a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs new file mode 100644 index 0000000..ba0960e --- /dev/null +++ b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using TickAPI.Common.Result; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; +using TickAPI.Customers.Services; + +namespace TickAPI.Tests.Customers.Services; + +public class CustomerServiceTests +{ + [Fact] + public async Task GetCustomerByEmailAsync_WhenCustomerWithEmailIsReturnedFromRepository_ShouldReturnUser() + { + var customer = new Customer + { + Email = "example@test.com" + }; + var dateTimeServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(customer.Email)).ReturnsAsync(Result.Success(customer)); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.GetCustomerByEmailAsync(customer.Email); + + Assert.True(result.IsSuccess); + Assert.Equal(customer, result.Value); + } + + [Fact] + public async Task GetCustomerByEmailAsync_WhenCustomerWithEmailIsNotReturnedFromRepository_ShouldReturnFailure() + { + const string email = "not@existing.com"; + var dateTimeServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.GetCustomerByEmailAsync(email); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"customer with email '{email}' not found", result.ErrorMsg); + } + + [Fact] + public async Task CreateNewCustomerAsync_WhenCustomerWithUniqueEmail_ShouldReturnNewCustomer() + { + const string email = "new@customer.com"; + const string firstName = "First"; + const string lastName = "Last"; + Guid id = Guid.NewGuid(); + DateTime createdAt = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(createdAt); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + customerRepositoryMock + .Setup(m => m.AddNewCustomerAsync(It.IsAny())) + .Callback(c => c.Id = id) + .Returns(Task.CompletedTask); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.CreateNewCustomerAsync(email, firstName, lastName); + + Assert.True(result.IsSuccess); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(createdAt, result.Value!.CreationDate); + Assert.Equal(id, result.Value!.Id); + } + + [Fact] + public async Task CreateNewCustomerAsync_WhenCustomerWithNotUniqueEmail_ShouldReturnFailure() + { + var customer = new Customer + { + Email = "already@exists.com" + }; + var dateTimeServiceMock = new Mock(); + var customerRepositoryMock = new Mock(); + customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(customer.Email)).ReturnsAsync(Result.Success(customer)); + var sut = new CustomerService(customerRepositoryMock.Object, dateTimeServiceMock.Object); + + var result = await sut.CreateNewCustomerAsync(customer.Email, "First", "Last"); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal($"customer with email '{customer.Email}' already exists", result.ErrorMsg); + } +} \ No newline at end of file From d5a5f5c14bc1427a0be549a681bf0776bc25e3e2 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 17 Mar 2025 22:53:14 +0100 Subject: [PATCH 10/10] Create tests for `CustomerController` --- .../Controllers/CustomerControllerTests.cs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs new file mode 100644 index 0000000..d36ac0f --- /dev/null +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -0,0 +1,150 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Result; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Controllers; +using TickAPI.Customers.DTOs.Request; +using TickAPI.Customers.Models; + +namespace TickAPI.Tests.Customers.Controllers; + +public class CustomerControllerTests +{ + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndCustomerExists_ShouldReturnTokenAndNotNewCustomer() + { + const string email = "existing@test.com"; + const string idToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var authServiceMock = new Mock(); + authServiceMock.Setup(m => m.LoginAsync(idToken)) + .ReturnsAsync(Result.Success(email)); + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Customer { Email = email })); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer)) + .Returns(Result.Success(jwtToken)); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var actionResult = await sut.GoogleLogin(new GoogleLoginDto(idToken)); + + Assert.Equal(jwtToken, actionResult.Value?.Token); + Assert.False(actionResult.Value?.IsNewCustomer); + } + + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldReturnTokenAndNewCustomer() + { + const string email = "new@test.com"; + const string idToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var authServiceMock = new Mock(); + authServiceMock.Setup(m => m.LoginAsync(idToken)) + .ReturnsAsync(Result.Success(email)); + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.NewCustomer)) + .Returns(Result.Success(jwtToken)); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var result = await sut.GoogleLogin(new GoogleLoginDto(idToken)); + + Assert.Equal(jwtToken, result.Value?.Token); + Assert.True(result.Value?.IsNewCustomer); + } + + [Fact] + public async Task GoogleCreateNewAccount_WhenCreatingAccountIsSuccessful_ShouldReturnToken() + { + const string email = "new@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string jwtToken = "valid-jwt-token"; + + var authServiceMock = new Mock(); + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.CreateNewCustomerAsync(email, firstName, lastName)) + .ReturnsAsync(Result.Success(new Customer + { + Id = Guid.NewGuid(), + Email = email, + FirstName = firstName, + LastName = lastName + })); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer)) + .Returns(Result.Success(jwtToken)); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var result = await sut.GoogleCreateNewAccount( + new GoogleCreateNewAccountDto( firstName, lastName )); + + Assert.Equal(jwtToken, result.Value?.Token); + } + + [Fact] + public async Task GoogleCreateNewAccount_WhenEmailClaimIsMissing_ShouldReturnBadRequest() + { + var authServiceMock = new Mock(); + var jwtServiceMock = new Mock(); + var customerServiceMock = new Mock(); + + var sut = new CustomerController( + authServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List())) + } + }; + + var result = await sut.GoogleCreateNewAccount( + new GoogleCreateNewAccountDto("First","Last")); + + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } +} \ No newline at end of file