diff --git a/TickAPI/TickAPI.Tests/Admins/Controllers/AdminsControllerTests.cs b/TickAPI/TickAPI.Tests/Admins/Controllers/AdminsControllerTests.cs new file mode 100644 index 0000000..a96bbfd --- /dev/null +++ b/TickAPI/TickAPI.Tests/Admins/Controllers/AdminsControllerTests.cs @@ -0,0 +1,183 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Admins.Abstractions; +using TickAPI.Admins.Controllers; +using TickAPI.Admins.DTOs.Request; +using TickAPI.Admins.DTOs.Response; +using TickAPI.Admins.Models; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Results.Generic; +using TickAPI.Customers.DTOs.Request; +using Xunit; + +namespace TickAPI.Tests.Admins.Controllers; + +public class AdminsControllerTests +{ + private readonly Mock _mockGoogleAuthService; + private readonly Mock _mockJwtService; + private readonly Mock _mockAdminService; + private readonly Mock _mockClaimsService; + private readonly AdminsController _controller; + + public AdminsControllerTests() + { + _mockGoogleAuthService = new Mock(); + _mockJwtService = new Mock(); + _mockAdminService = new Mock(); + _mockClaimsService = new Mock(); + + _controller = new AdminsController( + _mockGoogleAuthService.Object, + _mockJwtService.Object, + _mockAdminService.Object, + _mockClaimsService.Object + ); + } + + [Fact] + public async Task GoogleLogin_WithValidAccessToken_ReturnsJwtToken() + { + // Arrange + const string email = "existing@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + _mockGoogleAuthService + .Setup(x => x.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + _mockAdminService + .Setup(x => x.GetAdminByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Admin{Email = email})); + + _mockJwtService + .Setup(x => x.GenerateJwtToken(email, UserRole.Admin)) + .Returns(Result.Success(jwtToken)); + + // Act + var result = await _controller.GoogleLogin(new GoogleAdminLoginDto(accessToken)); + + // Assert + var actionResult = Assert.IsType>(result); + var okResult = Assert.IsType(actionResult.Value); + Assert.Equal(jwtToken, okResult.Token); + } + + [Fact] + public async Task GoogleLogin_WithInvalidAccessToken_ReturnsErrorStatusCode() + { + // Arrange + const string accessToken = "valid-google-token"; + + _mockGoogleAuthService + .Setup(x => x.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Failure(StatusCodes.Status401Unauthorized, "Invalid token")); + + // Act + var result = await _controller.GoogleLogin(new GoogleAdminLoginDto(accessToken)); + + // Assert + var actionResult = Assert.IsType>(result); + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status401Unauthorized, objectResult.StatusCode); + Assert.Equal("Invalid token", objectResult.Value); + } + + [Fact] + public async Task GoogleLogin_WithNonAdminEmail_ReturnsErrorStatusCode() + { + // Arrange + const string email = "nonadmin@test.com"; + const string accessToken = "valid-google-token"; + + _mockGoogleAuthService + .Setup(x => x.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + _mockAdminService + .Setup(x => x.GetAdminByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, "User is not an admin")); + + // Act + var result = await _controller.GoogleLogin(new GoogleAdminLoginDto(accessToken)); + + // Assert + var actionResult = Assert.IsType>(result); + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status404NotFound, objectResult.StatusCode); + Assert.Equal("User is not an admin", objectResult.Value); + } + + + [Fact] + public async Task AboutMe_WithValidClaims_ReturnsAdminData() + { + // Arrange + var email = "admin@example.com"; + var admin = new Admin { Email = email, Login = "admin" }; + + // Mock the HttpContext and User + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var identity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(identity); + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = principal } + }; + + _mockClaimsService + .Setup(x => x.GetEmailFromClaims(It.IsAny>())) + .Returns(Result.Success(email)); + + _mockAdminService + .Setup(x => x.GetAdminByEmailAsync(email)) + .ReturnsAsync(Result.Success(admin)); + + // Act + var result = await _controller.AboutMe(); + + // Assert + var actionResult = Assert.IsType>(result); + var okResult = Assert.IsType(actionResult.Value); + Assert.Equal(admin.Email, okResult.Email); + Assert.Equal(admin.Login, okResult.Login); + } + + [Fact] + public async Task AboutMe_WithInvalidClaims_ReturnsErrorStatusCode() + { + // Arrange + var claims = new List(); + var identity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(identity); + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = principal } + }; + + _mockClaimsService + .Setup(x => x.GetEmailFromClaims(It.IsAny>())) + .Returns(Result.Failure(StatusCodes.Status401Unauthorized, "Email claim not found")); + + // Act + var result = await _controller.AboutMe(); + + // Assert + var actionResult = Assert.IsType>(result); + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status401Unauthorized, objectResult.StatusCode); + Assert.Equal("Email claim not found", objectResult.Value); + } + +} diff --git a/TickAPI/TickAPI.Tests/Admins/Services/AdminsServiceTests.cs b/TickAPI/TickAPI.Tests/Admins/Services/AdminsServiceTests.cs new file mode 100644 index 0000000..7374e3e --- /dev/null +++ b/TickAPI/TickAPI.Tests/Admins/Services/AdminsServiceTests.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using TickAPI.Admins.Abstractions; +using TickAPI.Admins.Models; +using TickAPI.Admins.Services; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.Models; +using TickAPI.Customers.Services; + +namespace TickAPI.Tests.Admins.Services; + +public class AdminsServiceTests +{ + [Fact] + public async Task GetAdminByEmailAsync_WhenAdminWithEmailIsReturnedFromRepository_ShouldReturnUser() + { + var admin = new Admin + { + Email = "example@test.com" + }; + var adminRepositoryMock = new Mock(); + adminRepositoryMock.Setup(m => m.GetAdminByEmailAsync(admin.Email)).ReturnsAsync(Result.Success(admin)); + var sut = new AdminService(adminRepositoryMock.Object); + + var result = await sut.GetAdminByEmailAsync(admin.Email); + + Assert.True(result.IsSuccess); + Assert.Equal(admin, result.Value); + } + + [Fact] + public async Task GetAdminByEmailAsync_WhenAdminWithEmailIsNotReturnedFromRepository_ShouldReturnFailure() + { + const string email = "not@existing.com"; + var adminRepositoryMock = new Mock(); + adminRepositoryMock.Setup(m => m.GetAdminByEmailAsync(email)).ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"admin with email '{email}' not found")); + var sut = new AdminService(adminRepositoryMock.Object); + + var result = await sut.GetAdminByEmailAsync(email); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"admin with email '{email}' not found", result.ErrorMsg); + } + +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/Abstractions/IAdminRepository.cs b/TickAPI/TickAPI/Admins/Abstractions/IAdminRepository.cs index ed85466..313f985 100644 --- a/TickAPI/TickAPI/Admins/Abstractions/IAdminRepository.cs +++ b/TickAPI/TickAPI/Admins/Abstractions/IAdminRepository.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Admins.Abstractions; +using TickAPI.Admins.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Admins.Abstractions; public interface IAdminRepository { - + Task> GetAdminByEmailAsync(string adminEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/Abstractions/IAdminService.cs b/TickAPI/TickAPI/Admins/Abstractions/IAdminService.cs index 927d3e7..6277195 100644 --- a/TickAPI/TickAPI/Admins/Abstractions/IAdminService.cs +++ b/TickAPI/TickAPI/Admins/Abstractions/IAdminService.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Admins.Abstractions; +using TickAPI.Admins.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Admins.Abstractions; public interface IAdminService { - + public Task> GetAdminByEmailAsync(string adminEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs b/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs index cbc77bb..e6d70c2 100644 --- a/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs +++ b/TickAPI/TickAPI/Admins/Controllers/AdminsController.cs @@ -1,4 +1,12 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Admins.Abstractions; +using TickAPI.Admins.DTOs.Request; +using TickAPI.Admins.DTOs.Response; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Customers.DTOs.Request; namespace TickAPI.Admins.Controllers; @@ -6,5 +14,60 @@ namespace TickAPI.Admins.Controllers; [Route("api/[controller]")] public class AdminsController : ControllerBase { + private readonly IGoogleAuthService _googleAuthService; + private readonly IJwtService _jwtService; + private readonly IAdminService _adminService; + private readonly IClaimsService _claimsService; -} \ No newline at end of file + public AdminsController(IGoogleAuthService googleAuthService, IJwtService jwtService, IAdminService adminService, IClaimsService claimsService) + { + _googleAuthService = googleAuthService; + _jwtService = jwtService; + _adminService = adminService; + _claimsService = claimsService; + } + + [HttpPost("google-login")] + public async Task> GoogleLogin([FromBody] GoogleAdminLoginDto request) + { + var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); + if(userDataResult.IsError) + return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + + var userData = userDataResult.Value!; + + var adminResult = await _adminService.GetAdminByEmailAsync(userData.Email); + if (adminResult.IsError) + { + return StatusCode(adminResult.StatusCode, adminResult.ErrorMsg); + } + + var jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Admin); + if (jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleAdminLoginResponseDto(jwtTokenResult.Value!)); + } + + [AuthorizeWithPolicy(AuthPolicies.AdminPolicy)] + [HttpGet("about-me")] + public async Task> AboutMe() + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var adminResult = await _adminService.GetAdminByEmailAsync(email); + if (adminResult.IsError) + return StatusCode(StatusCodes.Status500InternalServerError, + "cannot find user with admin privilages in database for authorized admin request"); + + var admin = adminResult.Value!; + var aboutMeResponse = + new AboutMeAdminResponseDto(admin.Email, admin.Login); + return new ActionResult(aboutMeResponse); + } +} diff --git a/TickAPI/TickAPI/Admins/DTOs/Request/GoogleAdminLoginDto.cs b/TickAPI/TickAPI/Admins/DTOs/Request/GoogleAdminLoginDto.cs new file mode 100644 index 0000000..e72552e --- /dev/null +++ b/TickAPI/TickAPI/Admins/DTOs/Request/GoogleAdminLoginDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Admins.DTOs.Request; + +public record GoogleAdminLoginDto( + string AccessToken +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/DTOs/Response/AboutMeAdminResponseDto.cs b/TickAPI/TickAPI/Admins/DTOs/Response/AboutMeAdminResponseDto.cs new file mode 100644 index 0000000..9eac606 --- /dev/null +++ b/TickAPI/TickAPI/Admins/DTOs/Response/AboutMeAdminResponseDto.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Admins.DTOs.Response; + +public record AboutMeAdminResponseDto( + string Email, + string Login +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/DTOs/Response/GoogleAdminLoginResponseDto.cs b/TickAPI/TickAPI/Admins/DTOs/Response/GoogleAdminLoginResponseDto.cs new file mode 100644 index 0000000..4653f38 --- /dev/null +++ b/TickAPI/TickAPI/Admins/DTOs/Response/GoogleAdminLoginResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Admins.DTOs.Response; + +public record GoogleAdminLoginResponseDto( + string Token +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/Repositories/AdminRepository.cs b/TickAPI/TickAPI/Admins/Repositories/AdminRepository.cs index 304ca05..fe5f392 100644 --- a/TickAPI/TickAPI/Admins/Repositories/AdminRepository.cs +++ b/TickAPI/TickAPI/Admins/Repositories/AdminRepository.cs @@ -1,8 +1,29 @@ -using TickAPI.Admins.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Admins.Abstractions; +using TickAPI.Admins.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Customers.Models; namespace TickAPI.Admins.Repositories; public class AdminRepository : IAdminRepository { - + private readonly TickApiDbContext _tickApiDbContext; + + public AdminRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + public async Task> GetAdminByEmailAsync(string adminEmail) + { + var admin = await _tickApiDbContext.Admins.FirstOrDefaultAsync(admin => admin.Email == adminEmail); + + if (admin == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"admin with email '{adminEmail}' not found"); + } + + return Result.Success(admin); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/Services/AdminService.cs b/TickAPI/TickAPI/Admins/Services/AdminService.cs index 745b50e..da7b1f0 100644 --- a/TickAPI/TickAPI/Admins/Services/AdminService.cs +++ b/TickAPI/TickAPI/Admins/Services/AdminService.cs @@ -1,8 +1,20 @@ using TickAPI.Admins.Abstractions; +using TickAPI.Admins.Models; +using TickAPI.Common.Results.Generic; namespace TickAPI.Admins.Services; public class AdminService : IAdminService { - + private readonly IAdminRepository _adminRepository; + + public AdminService(IAdminRepository adminRepository) + { + _adminRepository = adminRepository; + } + + public async Task> GetAdminByEmailAsync(string adminEmail) + { + return await _adminRepository.GetAdminByEmailAsync(adminEmail); + } } \ No newline at end of file