Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs
Original file line number Diff line number Diff line change
@@ -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<IAuthService>();
authServiceMock.Setup(m => m.LoginAsync(idToken))
.ReturnsAsync(Result<string>.Success(email));

var customerServiceMock = new Mock<ICustomerService>();
customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email))
.ReturnsAsync(Result<Customer>.Success(new Customer { Email = email }));

var jwtServiceMock = new Mock<IJwtService>();
jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer))
.Returns(Result<string>.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<IAuthService>();
authServiceMock.Setup(m => m.LoginAsync(idToken))
.ReturnsAsync(Result<string>.Success(email));

var customerServiceMock = new Mock<ICustomerService>();
customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email))
.ReturnsAsync(Result<Customer>.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found"));

var jwtServiceMock = new Mock<IJwtService>();
jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.NewCustomer))
.Returns(Result<string>.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<IAuthService>();
var customerServiceMock = new Mock<ICustomerService>();
customerServiceMock.Setup(m => m.CreateNewCustomerAsync(email, firstName, lastName))
.ReturnsAsync(Result<Customer>.Success(new Customer
{
Id = Guid.NewGuid(),
Email = email,
FirstName = firstName,
LastName = lastName
}));

var jwtServiceMock = new Mock<IJwtService>();
jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.Customer))
.Returns(Result<string>.Success(jwtToken));

var sut = new CustomerController(
authServiceMock.Object,
jwtServiceMock.Object,
customerServiceMock.Object);

var claims = new List<Claim>
{
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<IAuthService>();
var jwtServiceMock = new Mock<IJwtService>();
var customerServiceMock = new Mock<ICustomerService>();

var sut = new CustomerController(
authServiceMock.Object,
jwtServiceMock.Object,
customerServiceMock.Object);

sut.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>()))
}
};

var result = await sut.GoogleCreateNewAccount(
new GoogleCreateNewAccountDto("First","Last"));

var objectResult = Assert.IsType<ObjectResult>(result.Result);
Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode);
Assert.Equal("missing email claim", objectResult.Value);
}
}
93 changes: 93 additions & 0 deletions TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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<IDateTimeService>();
var customerRepositoryMock = new Mock<ICustomerRepository>();
customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(customer.Email)).ReturnsAsync(Result<Customer>.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<IDateTimeService>();
var customerRepositoryMock = new Mock<ICustomerRepository>();
customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result<Customer>.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<IDateTimeService>();
dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(createdAt);
var customerRepositoryMock = new Mock<ICustomerRepository>();
customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(email)).ReturnsAsync(Result<Customer>.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found"));
customerRepositoryMock
.Setup(m => m.AddNewCustomerAsync(It.IsAny<Customer>()))
.Callback<Customer>(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<IDateTimeService>();
var customerRepositoryMock = new Mock<ICustomerRepository>();
customerRepositoryMock.Setup(m => m.GetCustomerByEmailAsync(customer.Email)).ReturnsAsync(Result<Customer>.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);
}
}
12 changes: 12 additions & 0 deletions TickAPI/TickAPI/Common/Auth/Attributes/AuthorizeWithPolicy.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
8 changes: 6 additions & 2 deletions TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
namespace TickAPI.Customers.Abstractions;
using TickAPI.Common.Result;
using TickAPI.Customers.Models;

namespace TickAPI.Customers.Abstractions;

public interface ICustomerRepository
{

Task<Result<Customer>> GetCustomerByEmailAsync(string customerEmail);
Task AddNewCustomerAsync(Customer customer);
}
8 changes: 6 additions & 2 deletions TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
namespace TickAPI.Customers.Abstractions;
using TickAPI.Common.Result;
using TickAPI.Customers.Models;

namespace TickAPI.Customers.Abstractions;

public interface ICustomerService
{

Task<Result<Customer>> GetCustomerByEmailAsync(string customerEmail);
Task<Result<Customer>> CreateNewCustomerAsync(string email, string firstName, string lastName);
}
66 changes: 65 additions & 1 deletion TickAPI/TickAPI/Customers/Controllers/CustomerController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,74 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using TickAPI.Common.Auth.Abstractions;
using TickAPI.Common.Auth.Attributes;
using TickAPI.Common.Auth.Enums;
using TickAPI.Customers.Abstractions;
using TickAPI.Customers.DTOs.Request;
using TickAPI.Customers.DTOs.Response;

namespace TickAPI.Customers.Controllers;

[ApiController]
[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<ActionResult<GoogleLoginResponseDto>> 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<GoogleLoginResponseDto>(new GoogleLoginResponseDto(jwtTokenResult.Value!, isNewCustomer));
}

[AuthorizeWithPolicy(AuthPolicies.NewCustomerPolicy)]
[HttpPost("google-create-new-account")]
public async Task<ActionResult<GoogleCreateNewAccountResponseDto>> GoogleCreateNewAccount([FromBody] GoogleCreateNewAccountDto request)
{
var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.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<GoogleCreateNewAccountResponseDto>(new GoogleCreateNewAccountResponseDto(jwtTokenResult.Value!));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TickAPI.Customers.DTOs.Request;

public record GoogleCreateNewAccountDto(
string FirstName,
string LastName
);
5 changes: 5 additions & 0 deletions TickAPI/TickAPI/Customers/DTOs/Request/GoogleLoginDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace TickAPI.Customers.DTOs.Request;

public record GoogleLoginDto(
string IdToken
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace TickAPI.Customers.DTOs.Response;

public record GoogleCreateNewAccountResponseDto(
string Token
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TickAPI.Customers.DTOs.Response;

public record GoogleLoginResponseDto(
string Token,
bool IsNewCustomer
);
Loading
Loading