diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index f1978cd..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/contentModel.xml -/projectSettingsUpdater.xml -/.idea.api.iml -/modules.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/indexLayout.xml b/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index d843f34..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/README.md b/README.md index 05eaf91..a869df0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ Resellio API is the backend service for the Resellio application, providing an i - dotnet version >= `9.0` - docker version >= `28.0` +### External Services + +This project uses the following external services: + +- **PostgreSQL** – as the main relational database. +- **Redis** – for caching and other fast in-memory operations. + +Both services are managed using **Docker Compose**, and are defined in the `docker-compose.yml` file. When running locally, Docker will automatically provision and run these services. + ### Running locally 1. Clone the repository: @@ -23,13 +32,12 @@ Resellio API is the backend service for the Resellio application, providing an i ```bash docker compose up ``` - + 3. Set up environment variables: Create an `appsettings.json` file in the root of the project, following the structure of `appsettings.example.json` found in `TickAPI/TickAPI/appsettings.example.json`. - 4. Run application: ```bash diff --git a/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs new file mode 100644 index 0000000..7a0c8f0 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Categories/Controllers/CategoryControllerTests.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Controllers; +using TickAPI.Categories.DTOs; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Response; + +namespace TickAPI.Tests.Categories.Controllers; + +public class CategoryControllerTests +{ + [Fact] + public async Task GetCategories_WhenDataIsValid_ShouldReturnOk() + { + // Arrange + int pageSize = 20; + int pageNumber = 0; + var categoryServiceMock = new Mock(); + categoryServiceMock.Setup(m => m.GetCategoriesResponsesAsync(pageSize, pageNumber)).ReturnsAsync( + Result>.Success(new PaginatedData(new List(), pageNumber, pageSize, true, true, + new PaginationDetails(0, 0)))); + + var sut = new CategoryController(categoryServiceMock.Object); + + // Act + var res = await sut.GetCategories(pageSize, pageNumber); + + // Assert + var result = Assert.IsType>>(res); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(200, objectResult.StatusCode); + Assert.NotNull(objectResult.Value); + } + + [Fact] + public async Task CreateCategory_WhenDataIsValid_ShouldReturnSuccess() + { + // Arrange + const string categoryName = "TestCategory"; + var createCategoryDto = new CreateCategoryDto(categoryName); + + var categoryServiceMock = new Mock(); + categoryServiceMock + .Setup(m => m.CreateNewCategoryAsync(categoryName)) + .ReturnsAsync(Result.Success(new Category())); + + var sut = new CategoryController(categoryServiceMock.Object); + + // Act + var res = await sut.CreateCategory(createCategoryDto); + + // Assert + var objectResult = Assert.IsType(res); + Assert.Equal(200, objectResult.StatusCode); + Assert.Equal("category created successfully", objectResult.Value); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs new file mode 100644 index 0000000..5f38aeb --- /dev/null +++ b/TickAPI/TickAPI.Tests/Categories/Services/CategoryServiceTests.cs @@ -0,0 +1,152 @@ +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; +using TickAPI.Categories.Services; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Tests.Categories.Services; + +public class CategoryServiceTests +{ + [Fact] + public async Task GetCategoriesResponsesAsync_WhenDataIsValid_ShouldReturnOk() + { + // Arrange + int pageSize = 10; + int page = 0; + var allCategories = new List().AsQueryable(); + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock.Setup(repo => repo.GetCategories()) + .Returns(allCategories); + + var paginationServiceMock = new Mock(); + paginationServiceMock.Setup(p => p.PaginateAsync(allCategories, pageSize, page)) + .Returns(Task.FromResult( + Result>.Success(new PaginatedData( + new List(), + page, + pageSize, + false, + false, + new PaginationDetails(0, 0)) + ) + )); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.GetCategoriesResponsesAsync(pageSize, page); + + // Assert + var result = Assert.IsType>>(res); + Assert.True(result.IsSuccess); + } + + [Fact] + public async Task GetCategoryByNameAsync_WhenCategoryWithNameIsReturnedFromRepository_ShouldReturnSuccess() + { + // Arrange + const string categoryName = "TestCategory"; + + var category = new Category() + { + Name = categoryName + }; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Success(category)); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.GetCategoryByNameAsync(categoryName); + + // Assert + Assert.True(res.IsSuccess); + Assert.Equal(category, res.Value); + } + + [Fact] + public async Task GetCategoryByNameAsync_WhenCategoryWithNameIsNotReturnedFromRepository_ShouldReturnFailure() + { + // Arrange + const string categoryName = "TestCategory"; + const string errorMsg = $"category with name '{categoryName}' not found"; + const int statusCode = 404; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Failure(statusCode, errorMsg)); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.GetCategoryByNameAsync(categoryName); + + // Assert + Assert.True(res.IsError); + Assert.Equal(errorMsg, res.ErrorMsg); + Assert.Equal(statusCode, res.StatusCode); + } + + [Fact] + public async Task CreateNewCategoryAsync_WhenCategoryDataIsValid_ShouldReturnNewCategory() + { + // Arrange + const string categoryName = "TestCategory"; + const string errorMsg = $"category with name '{categoryName}' not found"; + const int statusCode = 404; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Failure(statusCode, errorMsg)); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.CreateNewCategoryAsync(categoryName); + + // Assert + Assert.True(res.IsSuccess); + Assert.Equal(categoryName, res.Value!.Name); + } + + [Fact] + public async Task CreateNewCategoryAsync_WhenWithNotUniqueName_ShouldReturnFailure() + { + // Arrange + const string categoryName = "TestCategory"; + + var categoryRepositoryMock = new Mock(); + categoryRepositoryMock + .Setup(m => m.GetCategoryByNameAsync(categoryName)) + .ReturnsAsync(Result.Success(new Category())); + + var paginationServiceMock = new Mock(); + + var sut = new CategoryService(categoryRepositoryMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.CreateNewCategoryAsync(categoryName); + + // Assert + Assert.True(res.IsError); + Assert.Equal(400, res.StatusCode); + Assert.Equal($"category with name '{categoryName}' already exists", res.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs new file mode 100644 index 0000000..70835f9 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/GoogleAuthServiceTests.cs @@ -0,0 +1,93 @@ +using System.Net; +using Microsoft.AspNetCore.Http; +using Moq; +using System.Text.Json; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Auth.Services; + +namespace TickAPI.Tests.Common.Auth.Services; + +public class GoogleAuthServiceTests +{ + private readonly Mock _googleDataFetcherMock; + + public GoogleAuthServiceTests() + { + var validMessage = new HttpResponseMessage(HttpStatusCode.OK); + validMessage.Content = new StringContent(JsonSerializer.Serialize(new GoogleUserData("example@test.com", "Name", "Surname"))); + + var unauthorizedMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized); + + var wrongContentMessage = new HttpResponseMessage(HttpStatusCode.OK); + wrongContentMessage.Content = new StringContent("null"); + + _googleDataFetcherMock = new Mock(); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("validToken")) + .ReturnsAsync(validMessage); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("invalidToken")) + .ReturnsAsync(unauthorizedMessage); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("nullToken")) + .ReturnsAsync(wrongContentMessage); + _googleDataFetcherMock + .Setup(m => m.FetchUserDataAsync("throwToken")) + .ThrowsAsync(new Exception("An exception occured")); + } + + [Fact] + public async Task GetUserDataFromAccessToken_WhenDataFetcherReturnsValidResponse_ShouldReturnUserDataFromResponse() + { + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); + + var result = await sut.GetUserDataFromAccessToken("validToken"); + + Assert.NotNull(result); + Assert.True(result.IsSuccess); + Assert.Equal("example@test.com", result.Value!.Email); + Assert.Equal("Name", result.Value!.GivenName); + Assert.Equal("Surname", result.Value!.FamilyName); + } + + [Fact] + public async Task + GetUserDataFromAccessToken_WhenDataFetcherReturnsResponseWithErrorStatusCode_ShouldReturnFailure() + { + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); + + var result = await sut.GetUserDataFromAccessToken("invalidToken"); + + Assert.NotNull(result); + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); + Assert.Equal("Invalid Google access token", result.ErrorMsg); + } + + [Fact] + public async Task GetUserDataFromAccessToken_WhenDataFetcherReturnsNullResponse_ShouldReturnFailure() + { + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); + + var result = await sut.GetUserDataFromAccessToken("nullToken"); + + Assert.NotNull(result); + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("Failed to parse Google user info", result.ErrorMsg); + } + + [Fact] + public async Task GetUserDataFromAccessToken_WhenDataFetcherThrowsAnException_ShouldReturnFailure() + { + var sut = new GoogleAuthService(_googleDataFetcherMock.Object); + + var result = await sut.GetUserDataFromAccessToken("throwToken"); + + Assert.NotNull(result); + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal($"Error fetching user data: An exception occured", result.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs new file mode 100644 index 0000000..ef57062 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Auth/Services/JwtServiceTests.cs @@ -0,0 +1,127 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Moq; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Auth.Services; +using TickAPI.Common.Time.Abstractions; + +namespace TickAPI.Tests.Common.Auth.Services; + +public class JwtServiceTests +{ + private readonly Mock _mockConfiguration; + private readonly Mock _mockDateTimeService; + + public JwtServiceTests() + { + _mockConfiguration = new Mock(); + _mockConfiguration + .Setup(m => m["Authentication:Jwt:SecurityKey"]) + .Returns("ExampleSecurityKey-01234567890123456789"); + _mockConfiguration + .Setup(m => m["Authentication:Jwt:Issuer"]) + .Returns("Issuer"); + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("3600"); + + _mockDateTimeService = new Mock(); + _mockDateTimeService + .Setup(m => m.GetCurrentDateTime()) + .Returns(new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc)); + + } + + [Fact] + public void GenerateJwtToken_WhenGivenValidData_ShouldReturnJwtToken() + { + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + var handler = new JwtSecurityTokenHandler(); + var jwt = handler.ReadJwtToken(result.Value); + + Assert.True(result.IsSuccess); + Assert.NotNull(jwt); + Assert.Equal("Issuer", jwt.Issuer); + Assert.Equal(new DateTime(1970, 1, 1, 9, 0, 0, DateTimeKind.Utc), jwt.ValidTo); + Assert.Contains(jwt.Claims, c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "example@test.com"); + Assert.Contains(jwt.Claims, c => c.Type == ClaimTypes.Role && c.Value == "Customer"); + } + + [Fact] + public void GenerateJwtToken_WhenUserEmailIsEmpty_ShouldReturnError() + { + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken(null, UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'userEmail' parameter cannot be null or empty", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenSecurityKeyIsTooShort_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:SecurityKey"]) + .Returns("too-short"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'SecurityKey' must be at least 256 bits", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenExpirySecondsIsZero_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("0"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'ExpirySeconds' must be a positive integer", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenExpirySecondsIsNegative_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("-3600"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'ExpirySeconds' must be a positive integer", result.ErrorMsg); + } + + [Fact] + public void GenerateJwtToken_WhenExpirySecondsIsNotANumber_ShouldReturnError() + { + _mockConfiguration + .Setup(m => m["Authentication:Jwt:ExpirySeconds"]) + .Returns("not-a-number"); + var sut = new JwtService(_mockConfiguration.Object, _mockDateTimeService.Object); + + var result = sut.GenerateJwtToken("example@test.com", UserRole.Customer); + + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode); + Assert.Equal("'ExpirySeconds' must be a positive integer", result.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs new file mode 100644 index 0000000..f9a0eb3 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Claims/Services/ClaimsServiceTests.cs @@ -0,0 +1,49 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Claims.Services; + +namespace TickAPI.Tests.Common.Claims.Services; + +public class ClaimsServiceTests +{ + private readonly IClaimsService _claimsService; + + public ClaimsServiceTests() + { + _claimsService = new ClaimsService(); + } + + [Fact] + public void GetEmailFromClaims_WhenEmailInClaims_ShouldReturnEmail() + { + // Arrange + var email = "test@gmail.com"; + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + // Act + var result = _claimsService.GetEmailFromClaims(claims); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(email, result.Value!); + } + + [Fact] + public void GetEmailFromClaims_WhenEmailNotInClaims_ShouldReturnFailure() + { + // Arrange + var claims = new List(); + + // Act + var result = _claimsService.GetEmailFromClaims(claims); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("missing email claim", result.ErrorMsg); + } +} diff --git a/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs new file mode 100644 index 0000000..2f6ede0 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Pagination/Services/PaginationServiceTests.cs @@ -0,0 +1,194 @@ +using Microsoft.AspNetCore.Http; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Pagination.Services; + +namespace TickAPI.Tests.Common.Pagination.Services; + +public class PaginationServiceTests +{ + private readonly PaginationService _paginationService = new(); + + [Fact] + public async Task Paginate_WhenPageSizeNegative_ShouldReturnFailure() + { + // Act + var result = await _paginationService.PaginateAsync(new List().AsQueryable(), -5, 0); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'pageSize' param must be > 0, got: -5", result.ErrorMsg); + } + + [Fact] + public async Task Paginate_WhenPageSizeZero_ShouldReturnFailure() + { + // Act + var result = await _paginationService.PaginateAsync(new List().AsQueryable(), 0, 0); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'pageSize' param must be > 0, got: 0", result.ErrorMsg); + } + + [Fact] + public async Task Paginate_WhenPageNegative_ShouldReturnFailure() + { + // Act + var result = await _paginationService.PaginateAsync(new List().AsQueryable(), 1, -12); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'page' param must be >= 0, got: -12", result.ErrorMsg); + } + + [Fact] + public async Task Paginate_WhenCollectionLengthSmallerThanPageSize_ShouldReturnAllElements() + { + // Arrange + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); + int pageSize = data.Count() + 1; + const int pageNumber = 0; + + // Act + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(data, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.False(result.Value?.HasPreviousPage); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); + } + + [Fact] + public async Task Paginate_WhenCollectionLengthBiggerThanPageSize_ShouldReturnPartOfCollection() + { + // Arrange + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); + const int pageSize = 2; + const int pageNumber = 0; + + // Act + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(new List {1, 2}, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.True(result.Value?.HasNextPage); + Assert.False(result.Value?.HasPreviousPage); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); + } + + [Fact] + public async Task Paginate_WhenTakingElementsFromTheMiddle_ShouldReturnPaginatedDataWithBothBooleansTrue() + { + // Arrange + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); + const int pageSize = 2; + const int pageNumber = 1; + + // Act + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(new List {3, 4}, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.True(result.Value?.HasNextPage); + Assert.True(result.Value?.HasPreviousPage); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); + } + + [Fact] + public async Task Paginate_WhenExceededMaxPageNumber_ShouldReturnFailure() + { + // Arrange + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); + const int pageSize = 2; + const int pageNumber = 3; + + // Act + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("'page' param must be <= 2, got: 3", result.ErrorMsg); + } + + [Fact] + public async Task Paginate_WhenOnLastPage_ShouldReturnHasNextPageSetToFalse() + { + // Arrange + var data = new List { 1, 2, 3, 4, 5 }.AsQueryable(); + const int pageSize = 2; + const int pageNumber = 2; + + // Act + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(new List() { 5 }, result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.True(result.Value?.HasPreviousPage); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(2, result.Value?.PaginationDetails.MaxPageNumber); + } + + [Fact] + public async Task Paginate_WhenCollectionEmptyAndFirstPageIsRequested_ShouldReturnSuccess() + { + // Arrange + var data = new List().AsQueryable(); + const int pageSize = 2; + const int pageNumber = 0; + + // Act + var result = await _paginationService.PaginateAsync(data, pageSize, pageNumber); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(new List(), result.Value?.Data); + Assert.Equal(pageNumber, result.Value?.PageNumber); + Assert.Equal(pageSize, result.Value?.PageSize); + Assert.False(result.Value?.HasNextPage); + Assert.False(result.Value?.HasPreviousPage); + Assert.Equal(data.Count(), result.Value?.PaginationDetails.AllElementsCount); + Assert.Equal(0, result.Value?.PaginationDetails.MaxPageNumber); + } + + [Fact] + public void MapData_ShouldApplyLambdaToEachObject() + { + // Arrange + var data = new List() {1,2,3,4,5}; + var paginatedData = new PaginatedData(data, 0, 5, true, false, new PaginationDetails(1, 10)); + Func lambda = i => i * 2; + var expectedData = new List() { 2, 4, 6, 8, 10 }; + + // Act + var result = _paginationService.MapData(paginatedData, lambda); + + // Assert + Assert.Equal(expectedData, result.Data); + Assert.Equal(paginatedData.PageNumber, result.PageNumber); + Assert.Equal(paginatedData.PageSize, result.PageSize); + Assert.Equal(paginatedData.HasPreviousPage, result.HasPreviousPage); + Assert.Equal(paginatedData.HasNextPage, result.HasNextPage); + Assert.Equal(paginatedData.PaginationDetails, result.PaginationDetails); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs new file mode 100644 index 0000000..ffb1390 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Results/Generic/ResultTests.cs @@ -0,0 +1,85 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Tests.Common.Results.Generic; + +public class ResultTests +{ + [Fact] + public void Success_ShouldReturnResultWithValue() + { + const int value = 123; + + var result = Result.Success(value); + + Assert.Equal(value, result.Value); + Assert.True(result.IsSuccess); + Assert.False(result.IsError); + Assert.Equal("", result.ErrorMsg); + Assert.Equal(200, result.StatusCode); + } + + [Fact] + public void Failure_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "example error msg"; + + var result = Result.Failure(500, errorMsg); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenNonGenericResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success("abc"); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } + + [Fact] + public void PropagateError_WhenNonGenericResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs new file mode 100644 index 0000000..f09668d --- /dev/null +++ b/TickAPI/TickAPI.Tests/Common/Results/ResultTests.cs @@ -0,0 +1,57 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Tests.Common.Results; + +public class ResultTests +{ + [Fact] + public void Success_ShouldReturnResultWithSuccess() + { + var result = Result.Success(); + + Assert.True(result.IsSuccess); + Assert.False(result.IsError); + Assert.Equal("", result.ErrorMsg); + Assert.Equal(200, result.StatusCode); + } + + [Fact] + public void Failure_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "example error msg"; + + var result = Result.Failure(500, errorMsg); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenGenericResultWithErrorPassed_ShouldReturnResultWithError() + { + const int statusCode = 500; + const string errorMsg = "error message"; + var resultWithError = Result.Failure(statusCode, errorMsg); + + var result = Result.PropagateError(resultWithError); + + Assert.True(result.IsError); + Assert.False(result.IsSuccess); + Assert.Equal(errorMsg, result.ErrorMsg); + Assert.Equal(statusCode, result.StatusCode); + } + + [Fact] + public void PropagateError_WhenGenericResultWithSuccessPassed_ShouldThrowArgumentException() + { + var resultWithSuccess = Result.Success(123); + + var act = () => Result.PropagateError(resultWithSuccess); + + Assert.Throws(act); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs new file mode 100644 index 0000000..8e9eadd --- /dev/null +++ b/TickAPI/TickAPI.Tests/Customers/Controllers/CustomerControllerTests.cs @@ -0,0 +1,197 @@ +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.Auth.Responses; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Results.Generic; +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_ShouldReturnToken() + { + // Arrange + const string email = "existing@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock.Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + 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 claimsServiceMock = new Mock(); + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object, + claimsServiceMock.Object); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleCustomerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value?.Token); + } + + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndCustomerDoesNotExist_ShouldCreateCustomerAndReturnToken() + { + // Arrange + const string email = "new@test.com"; + const string accessToken = "valid-google-token"; + const string firstName = "First"; + const string lastName = "Last"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock.Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"customer with email '{email}' not found")); + 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 claimsServiceMock = new Mock(); + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object, + claimsServiceMock.Object); + + // Act + var result = await sut.GoogleLogin(new GoogleCustomerLoginDto( accessToken )); + + // Assert + Assert.Equal(jwtToken, result.Value?.Token); + } + + [Fact] + public async Task AboutMe_WithValidEmailClaim_ShouldReturnCustomerDetails() + { + // Arrange + const string email = "test@example.com"; + const string firstName = "John"; + const string lastName = "Doe"; + var creationDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var customer = new Customer + { + Email = email, + FirstName = firstName, + LastName = lastName, + CreationDate = creationDate + }; + + var customerServiceMock = new Mock(); + customerServiceMock.Setup(m => m.GetCustomerByEmailAsync(email)) + .ReturnsAsync(Result.Success(customer)); + + var googleAuthServiceMock = new Mock(); + var jwtServiceMock = new Mock(); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var identity = new ClaimsIdentity(claims); + var claimsPrincipal = new ClaimsPrincipal(identity); + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = claimsPrincipal + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object, + claimsServiceMock.Object); + + + sut.ControllerContext = controllerContext; + + // Act + var result = await sut.AboutMe(); + + // Assert + Assert.Equal(email, result.Value?.Email); + Assert.Equal(firstName, result.Value?.FirstName); + Assert.Equal(lastName, result.Value?.LastName); + Assert.Equal(creationDate, result.Value?.CreationDate); + } + + [Fact] + public async Task AboutMe_WithMissingEmailClaim_ShouldReturnBadRequest() + { + // Arrange + var customerServiceMock = new Mock(); + var googleAuthServiceMock = new Mock(); + var jwtServiceMock = new Mock(); + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + + + var sut = new CustomerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + customerServiceMock.Object, + claimsServiceMock.Object); + + var claims = new List(); + var identity = new ClaimsIdentity(claims); + var claimsPrincipal = new ClaimsPrincipal(identity); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = claimsPrincipal + } + }; + + // Act + var result = await sut.AboutMe(); + + // Assert + 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 diff --git a/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs new file mode 100644 index 0000000..07acc53 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Customers/Services/CustomerServiceTests.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Http; +using Moq; +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.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_WhenLastNameIsNull_ShouldReturnNewCustomer() + { + const string email = "new@customer.com"; + const string firstName = "First"; + const string lastName = null; + 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 diff --git a/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs new file mode 100644 index 0000000..8c788b1 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Controllers/EventControllerTests.cs @@ -0,0 +1,557 @@ +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; +using System.Security.Claims; +using Moq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Events.Controllers; +using TickAPI.Events.Abstractions; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Response; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; + +namespace TickAPI.Tests.Events.Controllers; + +public class EventControllerTests +{ + [Fact] + public async Task CreateEvent_WhenDataIsValid_ShouldReturnSuccess() + { + // Arrange + const string name = "Concert"; + const string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + const string email = "123@mail.com"; + const EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + CreateEventDto eventDto = new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, email)) + .ReturnsAsync(Result.Success(new Event())); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var organizerServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + sut.ControllerContext = controllerContext; + + // Act + var res = await sut.CreateEvent(eventDto); + + // Assert + var result = Assert.IsType>(res); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(200, objectResult.StatusCode); + Assert.Equal("Event created succesfully", objectResult.Value); + } + + [Fact] + public async Task CreateEvent_WhenMissingEmailClaims_ShouldReturnBadRequest() + { + // Arrange + const string name = "Concert"; + const string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + const EventStatus eventStatus = EventStatus.TicketsAvailable; + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + + var organizerServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + } + }; + + // Act + var res = await sut.CreateEvent(new CreateEventDto(name, description, startDate, endDate, minimumAge, eventStatus, createAddress)); + + // Assert + var result = Assert.IsType>(res); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } + + [Fact] + public async Task GetOrganizerEvents_WhenAllOperationsSucceed_ShouldReturnOkWithPaginatedData() + { + // Arrange + const string email = "organizer@example.com"; + const int page = 0; + const int pageSize = 10; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var paginatedData = new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEventsAsync(organizer, page, pageSize)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginatedData = Assert.IsType>(okResult.Value); + Assert.Equal(2, returnedPaginatedData.Data.Count); + Assert.Equal(paginatedData.Data[0], returnedPaginatedData.Data[0]); + Assert.Equal(paginatedData.Data[1], returnedPaginatedData.Data[1]); + Assert.Equal(page, returnedPaginatedData.PageNumber); + Assert.Equal(pageSize, returnedPaginatedData.PageSize); + Assert.False(returnedPaginatedData.HasNextPage); + Assert.False(returnedPaginatedData.HasPreviousPage); + } + + [Fact] + public async Task GetOrganizerEvents_WhenEmailClaimIsMissing_ShouldReturnBadRequest() + { + // Arrange + const int page = 0; + const int pageSize = 10; + const string errorMessage = "Missing email claim"; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(It.IsAny>())) + .Returns(Result.Failure(StatusCodes.Status400BadRequest, errorMessage)); + + var eventServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity()) + } + }; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetOrganizerEvents_WhenOrganizerIsNotFound_ShouldReturnNotFound() + { + // Arrange + const string email = "organizer@example.com"; + const int page = 0; + const int pageSize = 10; + const string errorMessage = "Organizer not found"; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, errorMessage)); + + var eventServiceMock = new Mock(); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status404NotFound, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetOrganizerEvents_WhenPaginationFails_ShouldReturnBadRequest() + { + // Arrange + const string email = "organizer@example.com"; + const int page = -1; // Invalid page + const int pageSize = 10; + const string errorMessage = "Invalid page number"; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEventsAsync(organizer, page, pageSize)) + .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetOrganizerEventsPaginationDetails_WhenAllOperationsSucceed_ShouldReturnOkWithPaginationDetails() + { + // Arrange + const string email = "organizer@example.com"; + const int pageSize = 10; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var paginationDetails = new PaginationDetails(2, 25); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize)) + .ReturnsAsync(Result.Success(paginationDetails)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginationDetails = Assert.IsType(okResult.Value); + Assert.Equal(2, returnedPaginationDetails.MaxPageNumber); + Assert.Equal(25, returnedPaginationDetails.AllElementsCount); + } + + [Fact] + public async Task GetOrganizerEventsPaginationDetails_WhenPaginationDetailsFails_ShouldReturnBadRequest() + { + // Arrange + const string email = "organizer@example.com"; + const int pageSize = -1; + const string errorMessage = "Invalid page size"; + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock + .Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)) + .Returns(Result.Success(email)); + + var organizer = new Organizer { Email = email, IsVerified = true }; + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var eventServiceMock = new Mock(); + eventServiceMock + .Setup(m => m.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize)) + .ReturnsAsync(Result.Failure(StatusCodes.Status400BadRequest, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + sut.ControllerContext = controllerContext; + + // Act + var response = await sut.GetOrganizerEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetEvents_WhenAllOperationsSucceed_ShouldReturnOkWithPaginatedData() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + var paginatedData = new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + false, + false, + new PaginationDetails(0, 2) + ); + + eventServiceMock + .Setup(m => m.GetEventsAsync(page, pageSize)) + .ReturnsAsync(Result>.Success(paginatedData)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginatedData = Assert.IsType>(okResult.Value); + Assert.Equal(2, returnedPaginatedData.Data.Count); + Assert.Equal(paginatedData.Data[0], returnedPaginatedData.Data[0]); + Assert.Equal(paginatedData.Data[1], returnedPaginatedData.Data[1]); + Assert.Equal(page, returnedPaginatedData.PageNumber); + Assert.Equal(pageSize, returnedPaginatedData.PageSize); + Assert.False(returnedPaginatedData.HasNextPage); + Assert.False(returnedPaginatedData.HasPreviousPage); + } + + [Fact] + public async Task GetEvents_WhenOperationFails_ShouldReturnErrorWithCorrectStatusCode() + { + // Arrange + const int page = 0; + const int pageSize = 10; + const string errorMessage = "Failed to retrieve events"; + const int statusCode = StatusCodes.Status500InternalServerError; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + eventServiceMock + .Setup(m => m.GetEventsAsync(page, pageSize)) + .ReturnsAsync(Result>.Failure(statusCode, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEvents(pageSize, page); + + // Assert + var result = Assert.IsType>>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(statusCode, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); + } + + [Fact] + public async Task GetEventsPaginationDetails_WhenAllOperationsSucceed_ShouldReturnOkWithPaginationDetails() + { + // Arrange + const int pageSize = 10; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + var paginationDetails = new PaginationDetails(0, 20); + + eventServiceMock + .Setup(m => m.GetEventsPaginationDetailsAsync(pageSize)) + .ReturnsAsync(Result.Success(paginationDetails)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var okResult = Assert.IsType(result.Result); + Assert.Equal(StatusCodes.Status200OK, okResult.StatusCode); + + var returnedPaginationDetails = Assert.IsType(okResult.Value); + Assert.Equal(paginationDetails.AllElementsCount, returnedPaginationDetails.AllElementsCount); + Assert.Equal(paginationDetails.MaxPageNumber, returnedPaginationDetails.MaxPageNumber); + } + + [Fact] + public async Task GetEventsPaginationDetails_WhenOperationFails_ShouldReturnErrorWithCorrectStatusCode() + { + // Arrange + const int pageSize = 10; + const string errorMessage = "Failed to retrieve pagination details"; + const int statusCode = StatusCodes.Status500InternalServerError; + + var eventServiceMock = new Mock(); + var claimsServiceMock = new Mock(); + var organizerServiceMock = new Mock(); + + eventServiceMock + .Setup(m => m.GetEventsPaginationDetailsAsync(pageSize)) + .ReturnsAsync(Result.Failure(statusCode, errorMessage)); + + var sut = new EventController(eventServiceMock.Object, claimsServiceMock.Object, organizerServiceMock.Object); + + // Act + var response = await sut.GetEventsPaginationDetails(pageSize); + + // Assert + var result = Assert.IsType>(response); + var objectResult = Assert.IsType(result.Result); + Assert.Equal(statusCode, objectResult.StatusCode); + Assert.Equal(errorMessage, objectResult.Value); +} +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs new file mode 100644 index 0000000..59bdd8a --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Services/EventServiceTests.cs @@ -0,0 +1,450 @@ +using Microsoft.AspNetCore.Http; +using TickAPI.Events.Abstractions; +using Moq; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Events.Models; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.DTOs.Response; +using TickAPI.Events.Services; + +namespace TickAPI.Tests.Events.Services; + +public class EventServiceTests +{ + [Fact] + + public async Task CreateNewEventAsync_WhenEventDataIsValid_ShouldReturnNewEvent() + { + // Arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventRepositoryMock = new Mock(); + eventRepositoryMock.Setup(e => e.AddNewEventAsync(It.IsAny())).Callback(e => e.Id = id) + .Returns(Task.CompletedTask); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + addressServiceMock.Setup(m => m.GetOrCreateAddressAsync(createAddress)).ReturnsAsync( + Result
.Success(new Address + { + City = createAddress.City, + Country = createAddress.Country, + FlatNumber = createAddress.FlatNumber, + HouseNumber = createAddress.HouseNumber, + PostalCode = createAddress.PostalCode, + Street = createAddress.Street, + }) + ); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(new DateTime(2003, 7, 11)); + + var paginationServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(new DateTime(2025, 5, 1), result.Value!.StartDate); + Assert.Equal(new DateTime(2025, 6, 1), result.Value!.EndDate); + Assert.Equal(name, result.Value!.Name); + Assert.Equal(description, result.Value!.Description); + Assert.Equal(eventStatus, result.Value!.EventStatus); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(organizerEmail, result.Value!.Organizer.Email); + } + + [Fact] + public async Task CreateNewEventAsync_WhenEndDateIsBeforeStartDate_ShouldReturnBadRequest() + { + // Arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 8, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + Guid id = Guid.NewGuid(); + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventRepositoryMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); + + // Assert + Assert.False(res.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); + Assert.Equal("End date should be after start date", res.ErrorMsg); + } + + [Fact] + public async Task CreateNewEventAsync_WhenStartDateIsBeforeNow_ShouldReturnBadRequest() + { + // Arrange + string name = "Concert"; + string description = "Description of a concert"; + DateTime startDate = new DateTime(2025, 5, 1); + DateTime endDate = new DateTime(2025, 6, 1); + uint? minimumAge = 18; + string organizerEmail = "123@mail.com"; + EventStatus eventStatus = EventStatus.TicketsAvailable; + CreateAddressDto createAddress = new CreateAddressDto("United States", "New York", "Main st", 20, null, "00-000"); + + var eventRepositoryMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(organizerEmail)) + .ReturnsAsync(Result.Success(new Organizer { Email = organizerEmail, IsVerified = true })); + + var addressServiceMock = new Mock(); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock.Setup(m => m.GetCurrentDateTime()).Returns(new DateTime(2025, 5, 11)); + + var paginationServiceMock = new Mock(); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var res = await sut.CreateNewEventAsync(name, description, startDate, endDate, minimumAge, createAddress, eventStatus, organizerEmail); + + // Assert + Assert.False(res.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, res.StatusCode); + Assert.Equal("Start date is in the past", res.ErrorMsg); + } + + [Fact] + public async Task GetOrganizerEvents_WhenPaginationSucceeds_ShouldReturnPaginatedEvents() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true, + Events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2"), + Utils.CreateSampleEvent("Event 3") + } + }; + int page = 0; + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var paginatedEvents = new PaginatedData( + organizer.Events.Take(pageSize).ToList(), + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + ); + + var organizerEvents = organizer.Events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEventsByOranizer(organizer)).Returns(organizerEvents); + + paginationServiceMock + .Setup(p => p.PaginateAsync(organizerEvents, pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedEvents)); + + paginationServiceMock + .Setup(p => p.MapData(paginatedEvents, It.IsAny>())) + .Returns(new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + )); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetOrganizerEventsAsync(organizer, page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal("Event 1", result.Value!.Data[0].Name); + Assert.Equal("Event 2", result.Value!.Data[1].Name); + Assert.Equal(0, result.Value!.PageNumber); + Assert.Equal(2, result.Value!.PageSize); + Assert.True(result.Value!.HasNextPage); + Assert.False(result.Value!.HasPreviousPage); + Assert.Equal(1, result.Value!.PaginationDetails.MaxPageNumber); + Assert.Equal(3, result.Value!.PaginationDetails.AllElementsCount); + } + + [Fact] + public async Task GetOrganizerEvents_WhenPaginationFails_ShouldPropagateError() + { + // Arrange + var organizer = new Organizer + { + Email = "organizer@example.com", + IsVerified = true, + Events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2") + } + }; + int page = 2; // Invalid page + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var organizerEvents = organizer.Events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEventsByOranizer(organizer)).Returns(organizerEvents); + + paginationServiceMock + .Setup(p => p.PaginateAsync(organizerEvents, pageSize, page)) + .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetOrganizerEventsAsync(organizer, page, pageSize); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid page number", result.ErrorMsg); + } + + [Fact] + public async Task GetEventsAsync_WhenPaginationSucceeds_ShouldReturnPaginatedEvents() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2"), + Utils.CreateSampleEvent("Event 3") + }; + int page = 0; + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var paginatedEvents = new PaginatedData( + events.Take(pageSize).ToList(), + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + ); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + paginationServiceMock + .Setup(p => p.PaginateAsync(eventsQueryable, pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedEvents)); + + paginationServiceMock + .Setup(p => p.MapData(paginatedEvents, It.IsAny>())) + .Returns(new PaginatedData( + new List + { + Utils.CreateSampleEventResponseDto("Event 1"), + Utils.CreateSampleEventResponseDto("Event 2") + }, + page, + pageSize, + true, + false, + new PaginationDetails(1, 3) + )); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal("Event 1", result.Value!.Data[0].Name); + Assert.Equal("Event 2", result.Value!.Data[1].Name); + Assert.Equal(0, result.Value!.PageNumber); + Assert.Equal(2, result.Value!.PageSize); + Assert.True(result.Value!.HasNextPage); + Assert.False(result.Value!.HasPreviousPage); + Assert.Equal(1, result.Value!.PaginationDetails.MaxPageNumber); + Assert.Equal(3, result.Value!.PaginationDetails.AllElementsCount); + } + + [Fact] + public async Task GetEventsAsync_WhenPaginationFails_ShouldPropagateError() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2") + }; + int page = 2; // Invalid page + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + paginationServiceMock + .Setup(p => p.PaginateAsync(eventsQueryable, pageSize, page)) + .ReturnsAsync(Result>.Failure(StatusCodes.Status400BadRequest, "Invalid page number")); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsAsync(page, pageSize); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid page number", result.ErrorMsg); + } + + [Fact] + public async Task GetEventsPaginationDetailsAsync_WhenSuccessful_ShouldReturnPaginationDetails() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2"), + Utils.CreateSampleEvent("Event 3") + }; + int pageSize = 2; + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + var paginationDetails = new PaginationDetails(1, 3); + paginationServiceMock + .Setup(p => p.GetPaginationDetailsAsync(eventsQueryable, pageSize)) + .ReturnsAsync(Result.Success(paginationDetails)); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsPaginationDetailsAsync(pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(1, result.Value!.MaxPageNumber); + Assert.Equal(3, result.Value!.AllElementsCount); + } + + [Fact] + public async Task GetEventsPaginationDetailsAsync_WhenFails_ShouldReturnError() + { + // Arrange + var events = new List + { + Utils.CreateSampleEvent("Event 1"), + Utils.CreateSampleEvent("Event 2") + }; + int pageSize = -1; // Invalid page size + + var eventRepositoryMock = new Mock(); + var organizerServiceMock = new Mock(); + var addressServiceMock = new Mock(); + var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + + var eventsQueryable = events.AsQueryable(); + eventRepositoryMock.Setup(p => p.GetEvents()).Returns(eventsQueryable); + + paginationServiceMock + .Setup(p => p.GetPaginationDetailsAsync(eventsQueryable, pageSize)) + .ReturnsAsync(Result.Failure(StatusCodes.Status400BadRequest, "Invalid page size")); + + var sut = new EventService(eventRepositoryMock.Object, organizerServiceMock.Object, addressServiceMock.Object, + dateTimeServiceMock.Object, paginationServiceMock.Object); + + // Act + var result = await sut.GetEventsPaginationDetailsAsync(pageSize); + + // Assert + Assert.False(result.IsSuccess); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal("Invalid page size", result.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Events/Utils.cs b/TickAPI/TickAPI.Tests/Events/Utils.cs new file mode 100644 index 0000000..8e5c0c8 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Events/Utils.cs @@ -0,0 +1,47 @@ +using TickAPI.Addresses.Models; +using TickAPI.Categories.Models; +using TickAPI.Events.DTOs.Response; +using TickAPI.Events.Models; + +namespace TickAPI.Tests.Events; + +public static class Utils +{ + public static Event CreateSampleEvent(string name) + { + return new Event + { + Id = Guid.NewGuid(), + Name = name, + Description = $"Description of {name}", + StartDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + EndDate = new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), + MinimumAge = 18, + EventStatus = EventStatus.TicketsAvailable, + Categories = new List { new Category { Name = "Test" } }, + Address = new Address + { + Country = "United States", + City = "New York", + PostalCode = "10001", + Street = "Main St", + HouseNumber = 123, + FlatNumber = null + } + }; + } + + public static GetEventResponseDto CreateSampleEventResponseDto(string name) + { + return new GetEventResponseDto( + name, + $"Description of {name}", + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + new DateTime(1970, 1, 2, 0, 0, 0, DateTimeKind.Utc), + 18, + [new GetEventResponseCategoryDto("Test")], + EventStatus.TicketsAvailable, + new GetEventResponseAddressDto("United States", "New York", "10001", "Main St", 123, null) + ); + } +} diff --git a/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs new file mode 100644 index 0000000..5e26e85 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Organizers/Controllers/OrganizerControllerTests.cs @@ -0,0 +1,423 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Controllers; +using TickAPI.Organizers.DTOs.Request; +using TickAPI.Organizers.DTOs.Response; +using TickAPI.Organizers.Models; + +namespace TickAPI.Tests.Organizers.Controllers; + +public class OrganizerControllerTests +{ + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndVerifiedOrganizerExists_ShouldReturnValidVerifiedLoginDto() + { + // Arrange + const string email = "existing@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock + .Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Organizer { Email = email, IsVerified = true })); + + var jwtServiceMock = new Mock(); + jwtServiceMock + .Setup(m => m.GenerateJwtToken(email, UserRole.Organizer)) + .Returns(Result.Success(jwtToken)); + + var claimsServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object + ); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleOrganizerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + Assert.False(actionResult.Value!.IsNewOrganizer); + Assert.True(actionResult.Value!.IsVerified); + } + + [Fact] + public async Task GoogleLogin_WhenAuthSuccessAndUnverifiedOrganizerExists_ShouldReturnValidUnverifiedLoginDto() + { + // Arrange + const string email = "unverified@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock + .Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Organizer { Email = email, IsVerified = false })); + + var jwtServiceMock = new Mock(); + jwtServiceMock + .Setup(m => m.GenerateJwtToken(email, UserRole.UnverifiedOrganizer)) + .Returns(Result.Success(jwtToken)); + + var claimsServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object + ); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleOrganizerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + Assert.False(actionResult.Value!.IsNewOrganizer); + Assert.False(actionResult.Value!.IsVerified); + } + + [Fact] + public async Task + GoogleLogin_WhenAuthSuccessAndOrganizerDoesNotExist_ShouldCreateValidNewOrganizerLoginDto() + { + // Arrange + const string email = "new@test.com"; + const string accessToken = "valid-google-token"; + const string jwtToken = "valid-jwt-token"; + + var googleAuthServiceMock = new Mock(); + googleAuthServiceMock + .Setup(m => m.GetUserDataFromAccessToken(accessToken)) + .ReturnsAsync(Result.Success(new GoogleUserData(email, "First", "Last"))); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var jwtServiceMock = new Mock(); + jwtServiceMock + .Setup(m => m.GenerateJwtToken(email, UserRole.NewOrganizer)) + .Returns(Result.Success(jwtToken)); + + var claimsServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object + ); + + // Act + var actionResult = await sut.GoogleLogin(new GoogleOrganizerLoginDto(accessToken)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + Assert.True(actionResult.Value!.IsNewOrganizer); + Assert.False(actionResult.Value!.IsVerified); + } + + [Fact] + public async Task CreateOrganizer_WhenCreatingAccountIsSuccessful_ShouldReturnToken() + { + // Arrange + const string email = "new@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + const string jwtToken = "valid-jwt-token"; + + var organizer = new Organizer + { + Id = Guid.NewGuid(), + Email = email, + FirstName = firstName, + LastName = lastName, + DisplayName = displayName, + IsVerified = false + }; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.CreateNewOrganizerAsync(email, firstName, lastName, displayName)) + .ReturnsAsync(Result.Success(organizer)); + + var jwtServiceMock = new Mock(); + jwtServiceMock.Setup(m => m.GenerateJwtToken(email, UserRole.UnverifiedOrganizer)) + .Returns(Result.Success(jwtToken)); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + + sut.ControllerContext = controllerContext; + + // Act + var actionResult = await sut.CreateOrganizer(new CreateOrganizerDto(firstName, lastName, displayName)); + + // Assert + Assert.Equal(jwtToken, actionResult.Value!.Token); + } + + [Fact] + public async Task CreateOrganizer_WhenMissingEmailClaim_ShouldReturnBadRequest() + { + // Arrange + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + + var jwtServiceMock = new Mock(); + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List())) + } + }; + + // Act + var actionResult = await sut.CreateOrganizer(new CreateOrganizerDto("First", "Last", "Display")); + + // Assert + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } + + [Fact] + public async Task VerifyOrganizer_WhenVerificationSuccessful_ShouldReturnOk() + { + // Arrange + const string email = "new@test.com"; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.VerifyOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success()); + + var jwtServiceMock = new Mock(); + + var claimsServiceMock = new Mock(); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + // Act + var actionResult = await sut.VerifyOrganizer(new VerifyOrganizerDto(email)); + + // Assert + var result = Assert.IsType(actionResult); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + } + + [Fact] + public async Task AboutMe_WithValidEmailClaim_ShouldReturnOrganizerDetails() + { + // Arrange + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + const bool isVerified = true; + DateTime creationDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizer = new Organizer + { + Id = Guid.NewGuid(), + Email = email, + FirstName = firstName, + LastName = lastName, + DisplayName = displayName, + IsVerified = isVerified, + CreationDate = creationDate + }; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var jwtServiceMock = new Mock(); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + sut.ControllerContext = controllerContext; + + // Act + var actionResult = await sut.AboutMe(); + + // Assert + Assert.Equal(email, actionResult.Value?.Email); + Assert.Equal(firstName, actionResult.Value?.FirstName); + Assert.Equal(lastName, actionResult.Value?.LastName); + Assert.Equal(displayName, actionResult.Value?.DisplayName); + Assert.Equal(isVerified, actionResult.Value?.IsVerified); + Assert.Equal(creationDate, actionResult.Value?.CreationDate); + } + + [Fact] + public async Task AboutMe_WithMissingEmailClaim_ShouldReturnBadRequest() + { + // Arrange + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + + var jwtServiceMock = new Mock(); + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(It.IsAny>())).Returns(Result.Failure(StatusCodes.Status400BadRequest, "missing email claim")); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + sut.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List())) + } + }; + + // Act + var actionResult = await sut.AboutMe(); + + // Assert + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status400BadRequest, objectResult.StatusCode); + Assert.Equal("missing email claim", objectResult.Value); + } + + [Fact] + public async Task AboutMe_WhenOrganizerNotFound_ShouldReturnInternalServerError() + { + // Arrange + const string email = "example@test.com"; + + var googleAuthServiceMock = new Mock(); + + var organizerServiceMock = new Mock(); + organizerServiceMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var jwtServiceMock = new Mock(); + + var claims = new List + { + new Claim(ClaimTypes.Email, email) + }; + var controllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(claims)) + } + }; + + var claimsServiceMock = new Mock(); + claimsServiceMock.Setup(m => m.GetEmailFromClaims(controllerContext.HttpContext.User.Claims)).Returns(Result.Success(email)); + + var sut = new OrganizerController( + googleAuthServiceMock.Object, + jwtServiceMock.Object, + organizerServiceMock.Object, + claimsServiceMock.Object); + + sut.ControllerContext = controllerContext; + + // Act + var actionResult = await sut.AboutMe(); + + // Assert + var objectResult = Assert.IsType(actionResult.Result); + Assert.Equal(StatusCodes.Status500InternalServerError, objectResult.StatusCode); + Assert.Equal("cannot find organizer in database for authorized organizer request", objectResult.Value); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs new file mode 100644 index 0000000..1e91aa9 --- /dev/null +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -0,0 +1,239 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; +using TickAPI.Organizers.Services; + +namespace TickAPI.Tests.Organizers.Services; + +public class OrganizerServiceTests +{ + [Fact] + public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsReturnedFromRepository_ShouldReturnOrganizer() + { + // Arrange + const string email = "example@test.com"; + + var organizer = new Organizer + { + Email = email + }; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(organizer)); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.GetOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(organizer, result.Value); + } + + [Fact] + public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsNotReturnedFromRepository_ShouldReturnFailure() + { + // Arrange + const string email = "example@test.com"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.GetOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"organizer with email '{email}' not found", result.ErrorMsg); + } + + [Fact] + public async Task CreateNewOrganizerAsync_WhenOrganizerDataIsValid_ShouldReturnNewOrganizer() + { + // Arrange + Guid id = Guid.NewGuid(); + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + DateTime currentDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + organizerRepositoryMock + .Setup(m => m.AddNewOrganizerAsync(It.IsAny())) + .Callback(o => o.Id = id) + .Returns(Task.CompletedTask); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(currentDate); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.CreateNewOrganizerAsync(email, firstName, lastName, displayName); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(displayName, result.Value!.DisplayName); + Assert.False(result.Value!.IsVerified); + Assert.Equal(currentDate, result.Value!.CreationDate); + } + + [Fact] + public async Task CreateNewOrganizerAsync_WhenLastNameIsNull_ShouldReturnNewOrganizer() + { + // Arrange + Guid id = Guid.NewGuid(); + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = null; + const string displayName = "Display"; + DateTime currentDate = new DateTime(1970, 1, 1, 8, 0, 0, DateTimeKind.Utc); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + organizerRepositoryMock + .Setup(m => m.AddNewOrganizerAsync(It.IsAny())) + .Callback(o => o.Id = id) + .Returns(Task.CompletedTask); + + var dateTimeServiceMock = new Mock(); + dateTimeServiceMock + .Setup(m => m.GetCurrentDateTime()) + .Returns(currentDate); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.CreateNewOrganizerAsync(email, firstName, lastName, displayName); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(id, result.Value!.Id); + Assert.Equal(email, result.Value!.Email); + Assert.Equal(firstName, result.Value!.FirstName); + Assert.Equal(lastName, result.Value!.LastName); + Assert.Equal(displayName, result.Value!.DisplayName); + Assert.False(result.Value!.IsVerified); + Assert.Equal(currentDate, result.Value!.CreationDate); + } + + [Fact] + public async Task CreateNewOrganizerAsync_WhenWithNotUniqueEmail_ShouldReturnFailure() + { + // Arrange + const string email = "example@test.com"; + const string firstName = "First"; + const string lastName = "Last"; + const string displayName = "Display"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success(new Organizer { Email = email })); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.CreateNewOrganizerAsync(email, firstName, lastName, displayName); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + Assert.Equal($"organizer with email '{email}' already exists", result.ErrorMsg); + } + + [Fact] + public async Task VerifyOrganizerByEmailAsync_WhenVerificationSuccessful_ShouldReturnSuccess() + { + const string email = "example@test.com"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.VerifyOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Success()); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.VerifyOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsSuccess); + } + + [Fact] + public async Task VerifyOrganizerByEmailAsync_WhenVerificationNotSuccessful_ShouldReturnFailure() + { + const string email = "example@test.com"; + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.VerifyOrganizerByEmailAsync(email)) + .ReturnsAsync(Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{email}' not found")); + + var dateTimeServiceMock = new Mock(); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object + ); + + // Act + var result = await sut.VerifyOrganizerByEmailAsync(email); + + // Assert + Assert.True(result.IsError); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal($"organizer with email '{email}' not found", result.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj index 95d873d..5617d62 100644 --- a/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj +++ b/TickAPI/TickAPI.Tests/TickAPI.Tests.csproj @@ -10,6 +10,7 @@ + @@ -18,4 +19,8 @@ + + + + diff --git a/TickAPI/TickAPI.Tests/UnitTest1.cs b/TickAPI/TickAPI.Tests/UnitTest1.cs deleted file mode 100644 index a06b7b3..0000000 --- a/TickAPI/TickAPI.Tests/UnitTest1.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TickAPI.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs b/TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs new file mode 100644 index 0000000..6867158 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Abstractions/IAddressRepository.cs @@ -0,0 +1,10 @@ +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Results.Generic; +using TickAPI.Addresses.Models; + +namespace TickAPI.Addresses.Abstractions; + +public interface IAddressRepository +{ + public Task> GetAddressAsync(CreateAddressDto createAddress); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs b/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs new file mode 100644 index 0000000..cdac615 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Abstractions/IAddressService.cs @@ -0,0 +1,10 @@ +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Results.Generic; +using TickAPI.Addresses.Models; + +namespace TickAPI.Addresses.Abstractions; + +public interface IAddressService +{ + public Task> GetOrCreateAddressAsync(CreateAddressDto createAddress); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs b/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs new file mode 100644 index 0000000..8aa9f4c --- /dev/null +++ b/TickAPI/TickAPI/Addresses/DTOs/Request/CreateAddressDto.cs @@ -0,0 +1,12 @@ +using TickAPI.Events.Models; + +namespace TickAPI.Addresses.DTOs.Request; + +public record CreateAddressDto( + + string Country, + string City, + string? Street, + uint? HouseNumber, + uint? FlatNumber, + string PostalCode); \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Models/Address.cs b/TickAPI/TickAPI/Addresses/Models/Address.cs new file mode 100644 index 0000000..1aca9d9 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Models/Address.cs @@ -0,0 +1,13 @@ +namespace TickAPI.Addresses.Models; +using TickAPI.Events.DTOs.Request; +public class Address +{ + public Guid Id { get; set; } + public string Country { get; set; } + public string City { get; set; } + public string? Street { get; set; } + public uint? HouseNumber { get; set; } + public uint? FlatNumber { get; set; } + public string PostalCode { get; set; } + +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Repositories/AddressRepository.cs b/TickAPI/TickAPI/Addresses/Repositories/AddressRepository.cs new file mode 100644 index 0000000..e976ab6 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Repositories/AddressRepository.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; + + +namespace TickAPI.Addresses.Repositories; + +public class AddressRepository : IAddressRepository +{ + private readonly TickApiDbContext _tickApiDbContext; + + public AddressRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + + public async Task> GetAddressAsync(CreateAddressDto createAddress) + { + var address = await _tickApiDbContext.Addresses.FirstOrDefaultAsync(x => + x.Street == createAddress.Street && + x.City == createAddress.City && + x.Country == createAddress.Country && + x.HouseNumber == createAddress.HouseNumber && + x.PostalCode == createAddress.PostalCode && + x.FlatNumber == createAddress.FlatNumber + ); + + if (address == null) + { + return Result
.Failure(StatusCodes.Status404NotFound,"Address not found"); + } + + return Result
.Success(address); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Addresses/Services/AddressService.cs b/TickAPI/TickAPI/Addresses/Services/AddressService.cs new file mode 100644 index 0000000..2858712 --- /dev/null +++ b/TickAPI/TickAPI/Addresses/Services/AddressService.cs @@ -0,0 +1,45 @@ +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.Abstractions; +using TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Addresses.Models; + +namespace TickAPI.Addresses.Services; + +public class AddressService : IAddressService +{ + + private readonly IAddressRepository _addressRepository; + + public AddressService(IAddressRepository addressRepository) + { + _addressRepository = addressRepository; + } + public async Task> GetOrCreateAddressAsync(CreateAddressDto createAddress) + { + var result = await _addressRepository.GetAddressAsync(createAddress); + if (result.IsSuccess) + { + return Result
.Success(result.Value!); + } + + return Result
.Success(FromDto(createAddress)); + } + + private static Address FromDto(CreateAddressDto dto) + { + return new Address + { + City = dto.City, + HouseNumber = dto.HouseNumber, + FlatNumber = dto.FlatNumber, + PostalCode = dto.PostalCode, + Street = dto.Street, + Country = dto.Country + }; + } + +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Admins/Models/Admin.cs b/TickAPI/TickAPI/Admins/Models/Admin.cs index e629518..7c2d230 100644 --- a/TickAPI/TickAPI/Admins/Models/Admin.cs +++ b/TickAPI/TickAPI/Admins/Models/Admin.cs @@ -2,5 +2,7 @@ public class Admin { - + public Guid Id { get; set; } + public string Email { get; set; } + public string Login { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs new file mode 100644 index 0000000..c7d1fc3 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryRepository.cs @@ -0,0 +1,11 @@ +using TickAPI.Categories.Models; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Categories.Abstractions; + +public interface ICategoryRepository +{ + public IQueryable GetCategories(); + public Task> GetCategoryByNameAsync(string categoryName); + public Task AddNewCategoryAsync(Category category); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs new file mode 100644 index 0000000..0e33bf0 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Abstractions/ICategoryService.cs @@ -0,0 +1,13 @@ +using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Categories.Abstractions; + +public interface ICategoryService +{ + public Task> GetCategoryByNameAsync(string categoryName); + public Task>> GetCategoriesResponsesAsync(int pageSize, int page); + public Task> CreateNewCategoryAsync(string categoryName); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs new file mode 100644 index 0000000..e7d7469 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Controllers/CategoryController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Pagination.Responses; + +namespace TickAPI.Categories.Controllers; + +[ApiController] +[Route("api/[controller]")] + +public class CategoryController : Controller +{ + private readonly ICategoryService _categoryService; + + public CategoryController(ICategoryService categoryService) + { + _categoryService = categoryService; + } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedUserPolicy)] + [HttpGet("get-categories")] + public async Task>> GetCategories([FromQuery] int pageSize, [FromQuery] int page) + { + var res = await _categoryService.GetCategoriesResponsesAsync(pageSize, page); + if (res.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, res.ErrorMsg); + } + return Ok(res.Value); + } + + // TODO: Add appropriate policy verification (admin, maybe also organizer?) + [HttpPost("create-category")] + public async Task CreateCategory([FromBody] CreateCategoryDto request) + { + var newCategoryResult = await _categoryService.CreateNewCategoryAsync(request.Name); + + if(newCategoryResult.IsError) + return StatusCode(newCategoryResult.StatusCode, newCategoryResult.ErrorMsg); + + return Ok("category created successfully"); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs b/TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs new file mode 100644 index 0000000..620d49f --- /dev/null +++ b/TickAPI/TickAPI/Categories/DTOs/CreateCategoryDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Categories.DTOs; + +public record CreateCategoryDto( + string Name +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs new file mode 100644 index 0000000..c2376db --- /dev/null +++ b/TickAPI/TickAPI/Categories/DTOs/Response/GetCategoryResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Categories.DTOs.Response; + +public record GetCategoryResponseDto( + string CategoryName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Models/Category.cs b/TickAPI/TickAPI/Categories/Models/Category.cs new file mode 100644 index 0000000..5c71ac2 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Models/Category.cs @@ -0,0 +1,10 @@ +using TickAPI.Events.Models; + +namespace TickAPI.Categories.Models; + +public class Category +{ + public Guid Id { get; set; } + public string Name { get; set; } + public ICollection Events { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs new file mode 100644 index 0000000..e1dcdb2 --- /dev/null +++ b/TickAPI/TickAPI/Categories/Respositories/CategoryRepository.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +namespace TickAPI.Categories.Respositories; + +public class CategoryRepository : ICategoryRepository +{ + private readonly TickApiDbContext _tickApiDbContext; + + public CategoryRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + + public IQueryable GetCategories() + { + return _tickApiDbContext.Categories; + } + + public async Task> GetCategoryByNameAsync(string categoryName) + { + var category = await _tickApiDbContext.Categories.FirstOrDefaultAsync(c => c.Name == categoryName); + + if (category == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"category with name '{categoryName}' not found"); + } + + return Result.Success(category); + } + + public async Task AddNewCategoryAsync(Category category) + { + _tickApiDbContext.Categories.Add(category); + await _tickApiDbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Categories/Services/CategoryService.cs b/TickAPI/TickAPI/Categories/Services/CategoryService.cs new file mode 100644 index 0000000..929c99e --- /dev/null +++ b/TickAPI/TickAPI/Categories/Services/CategoryService.cs @@ -0,0 +1,58 @@ +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.DTOs.Response; +using TickAPI.Categories.Models; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Categories.Services; + +public class CategoryService : ICategoryService +{ + private readonly ICategoryRepository _categoryRepository; + private readonly IPaginationService _paginationService; + + public CategoryService(ICategoryRepository categoryRepository, IPaginationService paginationService) + { + _categoryRepository = categoryRepository; + _paginationService = paginationService; + } + + public Task> GetCategoryByNameAsync(string categoryName) + { + return _categoryRepository.GetCategoryByNameAsync(categoryName); + } + + public async Task>> GetCategoriesResponsesAsync(int pageSize, int page) + { + var categoriesAllResponse = _categoryRepository.GetCategories(); + var categoriesPaginated = await _paginationService.PaginateAsync(categoriesAllResponse, pageSize, page); + if (!categoriesPaginated.IsSuccess) + { + return Result>.PropagateError(categoriesPaginated); + } + + var categoriesResponse = _paginationService.MapData(categoriesPaginated.Value!, (c) => new GetCategoryResponseDto(c.Name)); + + return Result>.Success(categoriesResponse); + } + + public async Task> CreateNewCategoryAsync(string categoryName) + { + var alreadyExistingResult = await _categoryRepository.GetCategoryByNameAsync(categoryName); + + if (alreadyExistingResult.IsSuccess) + { + return Result.Failure(StatusCodes.Status400BadRequest, + $"category with name '{categoryName}' already exists"); + } + + var category = new Category() + { + Name = categoryName + }; + + await _categoryRepository.AddNewCategoryAsync(category); + return Result.Success(category); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs deleted file mode 100644 index dab82c6..0000000 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IAuthService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TickAPI.Common.Auth.Abstractions; - -public interface IAuthService -{ - -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs new file mode 100644 index 0000000..351dc4a --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleAuthService.cs @@ -0,0 +1,9 @@ +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Auth.Abstractions; + +public interface IGoogleAuthService +{ + Task> GetUserDataFromAccessToken(string accessToken); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs new file mode 100644 index 0000000..9876cda --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IGoogleDataFetcher.cs @@ -0,0 +1,8 @@ +using Google.Apis.Auth; + +namespace TickAPI.Common.Auth.Abstractions; + +public interface IGoogleDataFetcher +{ + Task FetchUserDataAsync(string accessToken); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs index 2965c65..8dd4720 100644 --- a/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Abstractions/IJwtService.cs @@ -1,6 +1,9 @@ -namespace TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Auth.Abstractions; public interface IJwtService { - + public Result GenerateJwtToken(string? userEmail, UserRole role); } \ No newline at end of file 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 diff --git a/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs new file mode 100644 index 0000000..5c9f52a --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Enums/AuthPolicies.cs @@ -0,0 +1,11 @@ +namespace TickAPI.Common.Auth.Enums; + +public enum AuthPolicies +{ + AdminPolicy, + VerifiedOrganizerPolicy, + CustomerPolicy, + NewOrganizerPolicy, + CreatedOrganizerPolicy, + VerifiedUserPolicy, +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs new file mode 100644 index 0000000..019b009 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Enums/UserRole.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Common.Auth.Enums; + +public enum UserRole +{ + Admin, + Organizer, + Customer, + NewOrganizer, + UnverifiedOrganizer, +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs b/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs new file mode 100644 index 0000000..9512e88 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Responses/GoogleUserData.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace TickAPI.Common.Auth.Responses; + +public record GoogleUserData( + [property :JsonPropertyName("email")] string Email, + [property :JsonPropertyName("given_name")] string GivenName, + [property :JsonPropertyName("family_name")] string FamilyName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs index 147beb3..a1476ec 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleAuthService.cs @@ -1,8 +1,43 @@ -using TickAPI.Common.Auth.Abstractions; +using System.Text.Json; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Responses; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Auth.Services; -public class GoogleAuthService : IAuthService +public class GoogleAuthService : IGoogleAuthService { - + private IGoogleDataFetcher _googleDataFetcher; + + public GoogleAuthService(IGoogleDataFetcher googleDataFetcher) + { + _googleDataFetcher = googleDataFetcher; + } + + public async Task> GetUserDataFromAccessToken(string accessToken) + { + try + { + var response = await _googleDataFetcher.FetchUserDataAsync(accessToken); + + if (!response.IsSuccessStatusCode) + { + return Result.Failure(StatusCodes.Status401Unauthorized, "Invalid Google access token"); + } + + var jsonResponse = await response.Content.ReadAsStringAsync(); + var userInfo = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions()); + + if (userInfo == null) + { + return Result.Failure(StatusCodes.Status500InternalServerError, "Failed to parse Google user info"); + } + + return Result.Success(userInfo); + } + catch (Exception ex) + { + return Result.Failure(StatusCodes.Status500InternalServerError, $"Error fetching user data: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs b/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs new file mode 100644 index 0000000..d3e8282 --- /dev/null +++ b/TickAPI/TickAPI/Common/Auth/Services/GoogleDataFetcher.cs @@ -0,0 +1,26 @@ +using Google.Apis.Auth; +using TickAPI.Common.Auth.Abstractions; + +namespace TickAPI.Common.Auth.Services; + +public class GoogleDataFetcher : IGoogleDataFetcher +{ + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + + public GoogleDataFetcher(IConfiguration configuration, IHttpClientFactory httpClientFactory) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + } + + public async Task FetchUserDataAsync(string accessToken) + { + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + + var response = await client.GetAsync(_configuration["Authentication:Google:UserInfoEndpoint"]); + + return response; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs index 30008b8..0e458de 100644 --- a/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs +++ b/TickAPI/TickAPI/Common/Auth/Services/JwtService.cs @@ -1,8 +1,79 @@ -using TickAPI.Common.Auth.Abstractions; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; namespace TickAPI.Common.Auth.Services; public class JwtService : IJwtService { + private readonly IConfiguration _configuration; + private readonly IDateTimeService _dateTimeService; + + public JwtService(IConfiguration configuration, IDateTimeService dateTimeService) + { + _configuration = configuration; + _dateTimeService = dateTimeService; + } + + public Result GenerateJwtToken(string? userEmail, UserRole role) + { + // TODO: add some sort of userEmail/Role validation after adding new users is implemented + appropriate tests + + var configurationDataResult = ValidateConfigurationData(userEmail); + if (configurationDataResult.IsError) + return Result.PropagateError(configurationDataResult); + + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Email, userEmail), + new Claim(ClaimTypes.Role, role.ToString()) + }; + + var key = configurationDataResult.Value.SecurityKey; + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var addedSeconds = configurationDataResult.Value.ExpirySeconds; + + var token = new JwtSecurityToken( + issuer: configurationDataResult.Value.Issuer, + claims: claims, + expires: _dateTimeService.GetCurrentDateTime().AddSeconds(addedSeconds), + signingCredentials: creds + ); + + return Result.Success(new JwtSecurityTokenHandler().WriteToken(token)); + } + private Result ValidateConfigurationData(string? userEmail) + { + if (string.IsNullOrWhiteSpace(userEmail)) + return Result.Failure(StatusCodes.Status400BadRequest, "'userEmail' parameter cannot be null or empty"); + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Authentication:Jwt:SecurityKey"]!)); + if (key.KeySize < 256) + return Result.Failure(StatusCodes.Status500InternalServerError, "'SecurityKey' must be at least 256 bits"); + + if (!int.TryParse(_configuration["Authentication:Jwt:ExpirySeconds"], out var expirySeconds) || expirySeconds <= 0) + return Result.Failure(StatusCodes.Status500InternalServerError, "'ExpirySeconds' must be a positive integer"); + + var issuer = _configuration["Authentication:Jwt:Issuer"]; + + return Result.Success(new ConfigurationData + { + SecurityKey = key, + ExpirySeconds = expirySeconds, + Issuer = issuer, + }); + } + + private struct ConfigurationData + { + public SymmetricSecurityKey SecurityKey; + public int ExpirySeconds; + public string Issuer; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs b/TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs new file mode 100644 index 0000000..7dad5e6 --- /dev/null +++ b/TickAPI/TickAPI/Common/Claims/Abstractions/IClaimsService.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Claims.Abstractions; + +public interface IClaimsService +{ + Result GetEmailFromClaims(IEnumerable claims); +} diff --git a/TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs b/TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs new file mode 100644 index 0000000..83da865 --- /dev/null +++ b/TickAPI/TickAPI/Common/Claims/Services/ClaimsService.cs @@ -0,0 +1,16 @@ +using System.Security.Claims; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Claims.Services; + +public class ClaimsService : IClaimsService +{ + public Result GetEmailFromClaims(IEnumerable claims) + { + var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; + if (email == null) + return Result.Failure(StatusCodes.Status400BadRequest, "missing email claim"); + return Result.Success(email); + } +} diff --git a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs index 618b966..a6fe99e 100644 --- a/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Abstractions/IPaginationService.cs @@ -1,6 +1,11 @@ -namespace TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Pagination.Abstractions; public interface IPaginationService { - + public Task> GetPaginationDetailsAsync(IQueryable collection, int pageSize); + public Task>> PaginateAsync(IQueryable collection, int pageSize, int page); + public PaginatedData MapData(PaginatedData source, Func mapFunction); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Responses/PaginatedData.cs b/TickAPI/TickAPI/Common/Pagination/Responses/PaginatedData.cs new file mode 100644 index 0000000..75ae745 --- /dev/null +++ b/TickAPI/TickAPI/Common/Pagination/Responses/PaginatedData.cs @@ -0,0 +1,11 @@ +namespace TickAPI.Common.Pagination.Responses; + +public record PaginatedData( + List Data, + // First page should have number '0' + int PageNumber, + int PageSize, + bool HasNextPage, + bool HasPreviousPage, + PaginationDetails PaginationDetails +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs b/TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs new file mode 100644 index 0000000..ed44c09 --- /dev/null +++ b/TickAPI/TickAPI/Common/Pagination/Responses/PaginationDetails.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.Pagination.Responses; + +public record PaginationDetails( + int MaxPageNumber, + int AllElementsCount +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs index f4ba3ef..6b47acf 100644 --- a/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs +++ b/TickAPI/TickAPI/Common/Pagination/Services/PaginationService.cs @@ -1,8 +1,67 @@ -using TickAPI.Common.Pagination.Abstractions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results.Generic; namespace TickAPI.Common.Pagination.Services; public class PaginationService : IPaginationService { - + public async Task> GetPaginationDetailsAsync(IQueryable collection, int pageSize) + { + if (pageSize <= 0) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"'pageSize' param must be > 0, got: {pageSize}"); + } + + var allElementsCount = collection.Provider is IAsyncQueryProvider ? await collection.CountAsync() : collection.Count(); + var maxPageNumber = Math.Max((int)Math.Ceiling(1.0 * allElementsCount / pageSize) - 1, 0); + + var paginationDetails = new PaginationDetails(maxPageNumber, allElementsCount); + + return Result.Success(paginationDetails); + } + + public async Task>> PaginateAsync(IQueryable collection, int pageSize, int page) + { + if (pageSize <= 0) + { + return Result>.Failure(StatusCodes.Status400BadRequest, $"'pageSize' param must be > 0, got: {pageSize}"); + } + + if (page < 0) + { + return Result>.Failure(StatusCodes.Status400BadRequest, $"'page' param must be >= 0, got: {page}"); + } + + var paginationDetailsResult = await GetPaginationDetailsAsync(collection, pageSize); + if (paginationDetailsResult.IsError) + { + return Result>.PropagateError(paginationDetailsResult); + } + + var paginationDetails = paginationDetailsResult.Value!; + + if (page > paginationDetails.MaxPageNumber) + { + return Result>.Failure(StatusCodes.Status400BadRequest, + $"'page' param must be <= {paginationDetails.MaxPageNumber}, got: {page}"); + } + + var paginatedQuery = collection.Skip(page * pageSize).Take(pageSize); + var data = collection.Provider is IAsyncQueryProvider ? await paginatedQuery.ToListAsync() : paginatedQuery.ToList(); + var hasPreviousPage = page > 0; + var hasNextPage = page < paginationDetails.MaxPageNumber; + + var paginatedData = new PaginatedData(data, page, pageSize, hasNextPage, hasPreviousPage, paginationDetails); + + return Result>.Success(paginatedData); + } + + public PaginatedData MapData(PaginatedData source, Func mapFunction) + { + var newData = source.Data.Select(mapFunction).ToList(); + return new PaginatedData(newData, source.PageNumber, source.PageSize, source.HasNextPage, source.HasPreviousPage, source.PaginationDetails); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Result/Result.cs b/TickAPI/TickAPI/Common/Result/Result.cs deleted file mode 100644 index 9e196f8..0000000 --- a/TickAPI/TickAPI/Common/Result/Result.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TickAPI.Common.Result; - -public class Result -{ - -} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Results/Generic/Result.cs b/TickAPI/TickAPI/Common/Results/Generic/Result.cs new file mode 100644 index 0000000..10ccf62 --- /dev/null +++ b/TickAPI/TickAPI/Common/Results/Generic/Result.cs @@ -0,0 +1,48 @@ +namespace TickAPI.Common.Results.Generic; + +public record Result +{ + public bool IsSuccess { get; } + public bool IsError => !IsSuccess; + public T? Value { get; } + public int StatusCode { get; } + public string ErrorMsg { get; } + + private Result(bool isSuccess, T? value = default, int statusCode = StatusCodes.Status200OK, string errorMsg = "") + { + IsSuccess = isSuccess; + Value = value; + StatusCode = statusCode; + ErrorMsg = errorMsg; + } + + public static Result Success(T value) + { + return new Result(true, value); + } + + public static Result Failure(int statusCode, string errorMsg) + { + return new Result(false, default, statusCode, errorMsg); + } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Results/Result.cs b/TickAPI/TickAPI/Common/Results/Result.cs new file mode 100644 index 0000000..d339858 --- /dev/null +++ b/TickAPI/TickAPI/Common/Results/Result.cs @@ -0,0 +1,38 @@ +using TickAPI.Common.Results.Generic; + +namespace TickAPI.Common.Results; + +public record Result +{ + public bool IsSuccess { get; } + public bool IsError => !IsSuccess; + public int StatusCode { get; } + public string ErrorMsg { get; } + + private Result(bool isSuccess, int statusCode = StatusCodes.Status200OK, string errorMsg = "") + { + IsSuccess = isSuccess; + StatusCode = statusCode; + ErrorMsg = errorMsg; + } + + public static Result Success() + { + return new Result(true); + } + + public static Result Failure(int statusCode, string errorMsg) + { + return new Result(false, statusCode, errorMsg); + } + + public static Result PropagateError(Result other) + { + if (other.IsSuccess) + { + throw new ArgumentException("Trying to propagate error from successful value"); + } + + return Failure(other.StatusCode, other.ErrorMsg); + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs index 10d1a6c..6a13d40 100644 --- a/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs +++ b/TickAPI/TickAPI/Common/TickApiDbContext/TickApiDbContext.cs @@ -1,19 +1,65 @@ using Microsoft.EntityFrameworkCore; +using TickAPI.Addresses.Models; using TickAPI.Admins.Models; +using TickAPI.Categories.Models; using TickAPI.Customers.Models; using TickAPI.Events.Models; using TickAPI.Organizers.Models; using TickAPI.Tickets.Models; +using TickAPI.TicketTypes.Models; namespace TickAPI.Common.TickApiDbContext; public class TickApiDbContext : DbContext { - public TickApiDbContext(DbContextOptions options) : base(options) { } + public TickApiDbContext(DbContextOptions options) : base(options) + { } + public DbSet Admins { get; set; } public DbSet Customers { get; set; } public DbSet Events { get; set; } public DbSet Organizers { get; set; } public DbSet Tickets { get; set; } + public DbSet TicketTypes { get; set; } + public DbSet
Addresses { get; set; } + public DbSet Categories { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().HasData( + new Category + { + Id = Guid.Parse("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new Category + { + Id = Guid.Parse("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new Category + { + Id = Guid.Parse("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new Category + { + Id = Guid.Parse("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new Category + { + Id = Guid.Parse("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new Category + { + Id = Guid.Parse("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + } + ); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs b/TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs new file mode 100644 index 0000000..7ee11dc --- /dev/null +++ b/TickAPI/TickAPI/Common/Time/Abstractions/IDateTimeService.cs @@ -0,0 +1,6 @@ +namespace TickAPI.Common.Time.Abstractions; + +public interface IDateTimeService +{ + public DateTime GetCurrentDateTime(); +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs b/TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs new file mode 100644 index 0000000..a286224 --- /dev/null +++ b/TickAPI/TickAPI/Common/Time/Services/DateTimeService.cs @@ -0,0 +1,11 @@ +using TickAPI.Common.Time.Abstractions; + +namespace TickAPI.Common.Time.Services; + +public class DateTimeService : IDateTimeService +{ + public DateTime GetCurrentDateTime() + { + return DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs index 8dc6310..4cd7bab 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerRepository.cs @@ -1,6 +1,10 @@ -namespace TickAPI.Customers.Abstractions; +using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; + +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 8371ac2..a27002b 100644 --- a/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs +++ b/TickAPI/TickAPI/Customers/Abstractions/ICustomerService.cs @@ -1,6 +1,10 @@ -namespace TickAPI.Customers.Abstractions; +using TickAPI.Common.Results.Generic; +using TickAPI.Customers.Models; + +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/Controllers/CustomerController.cs b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs index 44599d3..6b5caaf 100644 --- a/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs +++ b/TickAPI/TickAPI/Customers/Controllers/CustomerController.cs @@ -1,4 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Customers.Abstractions; +using TickAPI.Customers.DTOs.Request; +using TickAPI.Customers.DTOs.Response; namespace TickAPI.Customers.Controllers; @@ -6,5 +13,63 @@ namespace TickAPI.Customers.Controllers; [Route("api/[controller]")] public class CustomerController : ControllerBase { + private readonly IGoogleAuthService _googleAuthService; + private readonly IJwtService _jwtService; + private readonly ICustomerService _customerService; + private readonly IClaimsService _claimsService; + public CustomerController(IGoogleAuthService googleAuthService, IJwtService jwtService, ICustomerService customerService, IClaimsService claimsService) + { + _googleAuthService = googleAuthService; + _jwtService = jwtService; + _customerService = customerService; + _claimsService = claimsService; + } + + [HttpPost("google-login")] + public async Task> GoogleLogin([FromBody] GoogleCustomerLoginDto request) + { + var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); + if(userDataResult.IsError) + return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + + var userData = userDataResult.Value!; + + var existingCustomerResult = await _customerService.GetCustomerByEmailAsync(userData.Email); + if (existingCustomerResult.IsError) + { + var newCustomerResult = await _customerService.CreateNewCustomerAsync(userData.Email, userData.GivenName, userData.FamilyName); + if (newCustomerResult.IsError) + return StatusCode(newCustomerResult.StatusCode, newCustomerResult.ErrorMsg); + } + + var jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.Customer); + if (jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleCustomerLoginResponseDto(jwtTokenResult.Value!)); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [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 customerResult = await _customerService.GetCustomerByEmailAsync(email); + if (customerResult.IsError) + return StatusCode(StatusCodes.Status500InternalServerError, + "cannot find customer in database for authorized customer request"); + + var customer = customerResult.Value!; + + var aboutMeResponse = + new AboutMeCustomerResponseDto(customer.Email, customer.FirstName, customer.LastName, customer.CreationDate); + return new ActionResult(aboutMeResponse); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCustomerLoginDto.cs b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCustomerLoginDto.cs new file mode 100644 index 0000000..8979a85 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Request/GoogleCustomerLoginDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Customers.DTOs.Request; + +public record GoogleCustomerLoginDto( + string AccessToken +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeCustomerResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeCustomerResponseDto.cs new file mode 100644 index 0000000..cb4bdb3 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/AboutMeCustomerResponseDto.cs @@ -0,0 +1,8 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record AboutMeCustomerResponseDto( + string Email, + string FirstName, + string LastName, + DateTime CreationDate +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCustomerLoginResponseDto.cs b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCustomerLoginResponseDto.cs new file mode 100644 index 0000000..6b5c7c8 --- /dev/null +++ b/TickAPI/TickAPI/Customers/DTOs/Response/GoogleCustomerLoginResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Customers.DTOs.Response; + +public record GoogleCustomerLoginResponseDto( + string Token +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Models/Customer.cs b/TickAPI/TickAPI/Customers/Models/Customer.cs index 7b19737..5a3c194 100644 --- a/TickAPI/TickAPI/Customers/Models/Customer.cs +++ b/TickAPI/TickAPI/Customers/Models/Customer.cs @@ -1,6 +1,13 @@ -namespace TickAPI.Customers.Models; +using TickAPI.Tickets.Models; + +namespace TickAPI.Customers.Models; public class Customer { - + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string? LastName { get; set; } + public DateTime CreationDate { get; set; } + public ICollection Tickets { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs index 8b21f7e..ed131d5 100644 --- a/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs +++ b/TickAPI/TickAPI/Customers/Repositories/CustomerRepository.cs @@ -1,8 +1,35 @@ -using TickAPI.Customers.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results.Generic; +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); + } + + 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 4b75299..e0a9f80 100644 --- a/TickAPI/TickAPI/Customers/Services/CustomerService.cs +++ b/TickAPI/TickAPI/Customers/Services/CustomerService.cs @@ -1,8 +1,42 @@ -using TickAPI.Customers.Abstractions; +using TickAPI.Common.Results.Generic; +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, 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 diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs index b3873c1..45c8e00 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventRepository.cs @@ -1,6 +1,11 @@ -namespace TickAPI.Events.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Organizers.Models; + +namespace TickAPI.Events.Abstractions; public interface IEventRepository { - + public Task AddNewEventAsync(Event @event); + public IQueryable GetEvents(); + public IQueryable GetEventsByOranizer(Organizer organizer); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs index 0a32eb2..8afbaad 100644 --- a/TickAPI/TickAPI/Events/Abstractions/IEventService.cs +++ b/TickAPI/TickAPI/Events/Abstractions/IEventService.cs @@ -1,6 +1,18 @@ -namespace TickAPI.Events.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Events.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Response; +using TickAPI.Organizers.Models; + +namespace TickAPI.Events.Abstractions; public interface IEventService { - + public Task> CreateNewEventAsync(string name, string description, DateTime startDate, + DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail); + public Task>> GetOrganizerEventsAsync(Organizer organizer, int page, int pageSize); + public Task> GetOrganizerEventsPaginationDetailsAsync(Organizer organizer, int pageSize); + public Task>> GetEventsAsync(int page, int pageSize); + public Task> GetEventsPaginationDetailsAsync(int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Controllers/EventController.cs b/TickAPI/TickAPI/Events/Controllers/EventController.cs index 3349977..5ea6f4e 100644 --- a/TickAPI/TickAPI/Events/Controllers/EventController.cs +++ b/TickAPI/TickAPI/Events/Controllers/EventController.cs @@ -1,10 +1,126 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Events.DTOs.Response; +using TickAPI.Events.DTOs.Request; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Events.Abstractions; +using TickAPI.Organizers.Abstractions; namespace TickAPI.Events.Controllers; [ApiController] [Route("api/[controller]")] + +// TODO: Add lists of categories and tickettypes public class EventController : ControllerBase { + private readonly IEventService _eventService; + private readonly IClaimsService _claimsService; + private readonly IOrganizerService _organizerService; + + public EventController(IEventService eventService, IClaimsService claimsService, IOrganizerService organizerService) + { + _eventService = eventService; + _claimsService = claimsService; + _organizerService = organizerService; + } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpPost("create-event")] + public async Task> CreateEvent([FromBody] CreateEventDto request) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var newEventResult = await _eventService.CreateNewEventAsync(request.Name, request.Description, request.StartDate, request.EndDate, request.MinimumAge, request.CreateAddress, request.EventStatus, email); + + if (newEventResult.IsError) + return StatusCode(newEventResult.StatusCode, newEventResult.ErrorMsg); + + return Ok("Event created succesfully"); + } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpGet("get-organizer-events")] + public async Task>> GetOrganizerEvents([FromQuery] int pageSize, [FromQuery] int page) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + { + return StatusCode(organizerResult.StatusCode, organizerResult.ErrorMsg); + } + var organizer = organizerResult.Value!; + + var paginatedDataResult = await _eventService.GetOrganizerEventsAsync(organizer, page, pageSize); + if (paginatedDataResult.IsError) + { + return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); + } + + return Ok(paginatedDataResult.Value!); + } + + [AuthorizeWithPolicy(AuthPolicies.VerifiedOrganizerPolicy)] + [HttpGet("get-organizer-events-pagination-details")] + public async Task> GetOrganizerEventsPaginationDetails([FromQuery] int pageSize) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(emailResult.StatusCode, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + { + return StatusCode(organizerResult.StatusCode, organizerResult.ErrorMsg); + } + var organizer = organizerResult.Value!; + + var paginationDetailsResult = await _eventService.GetOrganizerEventsPaginationDetailsAsync(organizer, pageSize); + if (paginationDetailsResult.IsError) + { + return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); + } + + return Ok(paginationDetailsResult.Value!); + } + + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("get-events")] + public async Task>> GetEvents([FromQuery] int pageSize, [FromQuery] int page) + { + var paginatedDataResult = await _eventService.GetEventsAsync(page, pageSize); + if (paginatedDataResult.IsError) + { + return StatusCode(paginatedDataResult.StatusCode, paginatedDataResult.ErrorMsg); + } + return Ok(paginatedDataResult.Value!); + } + [AuthorizeWithPolicy(AuthPolicies.CustomerPolicy)] + [HttpGet("get-events-pagination-details")] + public async Task> GetEventsPaginationDetails([FromQuery] int pageSize) + { + var paginationDetailsResult = await _eventService.GetEventsPaginationDetailsAsync(pageSize); + if (paginationDetailsResult.IsError) + { + return StatusCode(paginationDetailsResult.StatusCode, paginationDetailsResult.ErrorMsg); + } + return Ok(paginationDetailsResult.Value!); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs index f06c961..1078587 100644 --- a/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs +++ b/TickAPI/TickAPI/Events/DTOs/Request/CreateEventDto.cs @@ -1,6 +1,15 @@ -namespace TickAPI.Events.DTOs.Request; +using TickAPI.Events.Models; +using TickAPI.Addresses.DTOs.Request; -public class CreateEventDto -{ - -} \ No newline at end of file +namespace TickAPI.Events.DTOs.Request; + +public record CreateEventDto +( + string Name, + string Description, + DateTime StartDate, + DateTime EndDate, + uint? MinimumAge, + EventStatus EventStatus, + CreateAddressDto CreateAddress +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs new file mode 100644 index 0000000..8ade67d --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseAddressDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponseAddressDto( + string Country, + string City, + string PostalCode, + string? Street, + uint? HouseNumber, + uint? FlatNumber +); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs new file mode 100644 index 0000000..4ba5d7a --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseCategoryDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponseCategoryDto( + string Name +); diff --git a/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs new file mode 100644 index 0000000..41d1698 --- /dev/null +++ b/TickAPI/TickAPI/Events/DTOs/Response/GetEventResponseDto.cs @@ -0,0 +1,14 @@ +using TickAPI.Events.Models; + +namespace TickAPI.Events.DTOs.Response; + +public record GetEventResponseDto( + string Name, + string Description, + DateTime StartDate, + DateTime EndDate, + uint? MinimumAge, + List Categories, + EventStatus Status, + GetEventResponseAddressDto Addres +); diff --git a/TickAPI/TickAPI/Events/Models/Event.cs b/TickAPI/TickAPI/Events/Models/Event.cs index 2008893..50ca434 100644 --- a/TickAPI/TickAPI/Events/Models/Event.cs +++ b/TickAPI/TickAPI/Events/Models/Event.cs @@ -1,6 +1,29 @@ -namespace TickAPI.Events.Models; +using TickAPI.Addresses.Models; +using TickAPI.Organizers.Models; +using TickAPI.Categories.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.Events.Models; public class Event { - + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public uint? MinimumAge { get; set; } + public Organizer Organizer { get; set; } + public ICollection Categories { get; set; } + public ICollection TicketTypes { get; set; } + public EventStatus EventStatus { get; set; } + public Address Address { get; set; } +} + +public enum EventStatus +{ + TicketsAvailable, + SoldOut, + InProgress, + Finished } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs index ac0fab2..40c05d7 100644 --- a/TickAPI/TickAPI/Events/Repositories/EventRepository.cs +++ b/TickAPI/TickAPI/Events/Repositories/EventRepository.cs @@ -1,8 +1,32 @@ -using TickAPI.Events.Abstractions; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Events.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Organizers.Models; namespace TickAPI.Events.Repositories; public class EventRepository : IEventRepository { + private readonly TickApiDbContext _tickApiDbContext; + + public EventRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + public async Task AddNewEventAsync(Event @event) + { + _tickApiDbContext.Events.Add(@event); + await _tickApiDbContext.SaveChangesAsync(); + } + + public IQueryable GetEvents() + { + return _tickApiDbContext.Events; + } + + public IQueryable GetEventsByOranizer(Organizer organizer) + { + return _tickApiDbContext.Events.Where(e => e.Organizer.Id == organizer.Id); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Events/Services/EventService.cs b/TickAPI/TickAPI/Events/Services/EventService.cs index 95304bc..6ac130c 100644 --- a/TickAPI/TickAPI/Events/Services/EventService.cs +++ b/TickAPI/TickAPI/Events/Services/EventService.cs @@ -1,8 +1,106 @@ -using TickAPI.Events.Abstractions; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.DTOs.Request; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Common.Results.Generic; +using TickAPI.Events.DTOs.Response; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; namespace TickAPI.Events.Services; public class EventService : IEventService { + private readonly IOrganizerService _organizerService; + private readonly IEventRepository _eventRepository; + private readonly IAddressService _addressService; + private readonly IDateTimeService _dateTimeService; + private readonly IPaginationService _paginationService; + + public EventService(IEventRepository eventRepository, IOrganizerService organizerService, IAddressService addressService, IDateTimeService dateTimeService, IPaginationService paginationService) + { + _eventRepository = eventRepository; + _organizerService = organizerService; + _addressService = addressService; + _dateTimeService = dateTimeService; + _paginationService = paginationService; + } + + public async Task> CreateNewEventAsync(string name, string description, DateTime startDate, DateTime endDate, uint? minimumAge, CreateAddressDto createAddress, EventStatus eventStatus, string organizerEmail) + { + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(organizerEmail); + if (!organizerResult.IsSuccess) + return Result.PropagateError(organizerResult); + + + if (endDate < startDate) + return Result.Failure(StatusCodes.Status400BadRequest, "End date should be after start date"); + + if (startDate < _dateTimeService.GetCurrentDateTime()) + return Result.Failure(StatusCodes.Status400BadRequest, "Start date is in the past"); + + + var address = await _addressService.GetOrCreateAddressAsync(createAddress); + + var @event = new Event + { + Name = name, + Description = description, + StartDate = startDate, + EndDate = endDate, + MinimumAge = minimumAge, + Address = address.Value!, + Organizer = organizerResult.Value!, + EventStatus = eventStatus + }; + await _eventRepository.AddNewEventAsync(@event); + return Result.Success(@event); + } + + public async Task>> GetOrganizerEventsAsync(Organizer organizer, int page, int pageSize) + { + var organizerEvents = _eventRepository.GetEventsByOranizer(organizer); + return await GetPaginatedEventsAsync(organizerEvents, page, pageSize); + } + + public async Task> GetOrganizerEventsPaginationDetailsAsync(Organizer organizer, int pageSize) + { + var organizerEvents = _eventRepository.GetEventsByOranizer(organizer); + return await _paginationService.GetPaginationDetailsAsync(organizerEvents, pageSize); + } + + public async Task>> GetEventsAsync(int page, int pageSize) + { + var events = _eventRepository.GetEvents(); + return await GetPaginatedEventsAsync(events, page, pageSize); + } + + public async Task> GetEventsPaginationDetailsAsync(int pageSize) + { + var events = _eventRepository.GetEvents(); + return await _paginationService.GetPaginationDetailsAsync(events, pageSize); + } + + private async Task>> GetPaginatedEventsAsync(IQueryable events, int page, int pageSize) + { + var paginatedEventsResult = await _paginationService.PaginateAsync(events, pageSize, page); + if (paginatedEventsResult.IsError) + { + return Result>.PropagateError(paginatedEventsResult); + } + + var paginatedData = _paginationService.MapData(paginatedEventsResult.Value!, MapEventToGetEventResponseDto); + + return Result>.Success(paginatedData); + } + private static GetEventResponseDto MapEventToGetEventResponseDto(Event ev) + { + var categories = ev.Categories.Select((c) => new GetEventResponseCategoryDto(c.Name)).ToList(); + var address = new GetEventResponseAddressDto(ev.Address.Country, ev.Address.City, ev.Address.PostalCode, ev.Address.Street, ev.Address.HouseNumber, ev.Address.FlatNumber); + return new GetEventResponseDto(ev.Name, ev.Description, ev.StartDate, ev.EndDate, ev.MinimumAge, categories, ev.EventStatus, address); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.Designer.cs b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.Designer.cs new file mode 100644 index 0000000..319de56 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.Designer.cs @@ -0,0 +1,363 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250317073945_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Events.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.cs b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.cs new file mode 100644 index 0000000..542f32d --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250317073945_InitialMigration.cs @@ -0,0 +1,257 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Country = table.Column(type: "nvarchar(max)", nullable: false), + City = table.Column(type: "nvarchar(max)", nullable: false), + Street = table.Column(type: "nvarchar(max)", nullable: true), + HouseNumber = table.Column(type: "bigint", nullable: true), + FlatNumber = table.Column(type: "bigint", nullable: true), + PostalCode = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Admins", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + Login = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Admins", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + CategoryName = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + CreationDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Organizers", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(max)", nullable: false), + Login = table.Column(type: "nvarchar(max)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + CreationDate = table.Column(type: "datetime2", nullable: false), + OrganizerName = table.Column(type: "nvarchar(max)", nullable: false), + IsVerified = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Organizers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Events", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + StartDate = table.Column(type: "datetime2", nullable: false), + EndDate = table.Column(type: "datetime2", nullable: false), + MinimumAge = table.Column(type: "bigint", nullable: true), + OrganizerId = table.Column(type: "uniqueidentifier", nullable: false), + EventStatus = table.Column(type: "int", nullable: false), + AddressId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Events", x => x.Id); + table.ForeignKey( + name: "FK_Events_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Events_Organizers_OrganizerId", + column: x => x.OrganizerId, + principalTable: "Organizers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CategoryEvent", + columns: table => new + { + CategoriesId = table.Column(type: "uniqueidentifier", nullable: false), + EventsId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoryEvent", x => new { x.CategoriesId, x.EventsId }); + table.ForeignKey( + name: "FK_CategoryEvent_Categories_CategoriesId", + column: x => x.CategoriesId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CategoryEvent_Events_EventsId", + column: x => x.EventsId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TicketTypes", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EventId = table.Column(type: "uniqueidentifier", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + MaxCount = table.Column(type: "bigint", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false), + Currency = table.Column(type: "nvarchar(max)", nullable: false), + AvailableFrom = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TicketTypes", x => x.Id); + table.ForeignKey( + name: "FK_TicketTypes_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TypeId = table.Column(type: "uniqueidentifier", nullable: false), + OwnerId = table.Column(type: "uniqueidentifier", nullable: false), + NameOnTicket = table.Column(type: "nvarchar(max)", nullable: false), + Seats = table.Column(type: "nvarchar(max)", nullable: true), + ForResell = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Customers_OwnerId", + column: x => x.OwnerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tickets_TicketTypes_TypeId", + column: x => x.TypeId, + principalTable: "TicketTypes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CategoryEvent_EventsId", + table: "CategoryEvent", + column: "EventsId"); + + migrationBuilder.CreateIndex( + name: "IX_Events_AddressId", + table: "Events", + column: "AddressId"); + + migrationBuilder.CreateIndex( + name: "IX_Events_OrganizerId", + table: "Events", + column: "OrganizerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_OwnerId", + table: "Tickets", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_TypeId", + table: "Tickets", + column: "TypeId"); + + migrationBuilder.CreateIndex( + name: "IX_TicketTypes_EventId", + table: "TicketTypes", + column: "EventId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Admins"); + + migrationBuilder.DropTable( + name: "CategoryEvent"); + + migrationBuilder.DropTable( + name: "Tickets"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "TicketTypes"); + + migrationBuilder.DropTable( + name: "Events"); + + migrationBuilder.DropTable( + name: "Addresses"); + + migrationBuilder.DropTable( + name: "Organizers"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs new file mode 100644 index 0000000..d2a288b --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.Designer.cs @@ -0,0 +1,359 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250320114817_UpdateOrganizer")] + partial class UpdateOrganizer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Events.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs new file mode 100644 index 0000000..9998296 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250320114817_UpdateOrganizer.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class UpdateOrganizer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Login", + table: "Organizers"); + + migrationBuilder.RenameColumn( + name: "OrganizerName", + table: "Organizers", + newName: "DisplayName"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "DisplayName", + table: "Organizers", + newName: "OrganizerName"); + + migrationBuilder.AddColumn( + name: "Login", + table: "Organizers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs new file mode 100644 index 0000000..f5bf758 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.Designer.cs @@ -0,0 +1,357 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250401224843_NullableLastNames")] + partial class NullableLastNames + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs new file mode 100644 index 0000000..e7741a4 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250401224843_NullableLastNames.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TickAPI.Migrations +{ + /// + public partial class NullableLastNames : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastName", + table: "Organizers", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customers", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastName", + table: "Organizers", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs new file mode 100644 index 0000000..476547a --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.Designer.cs @@ -0,0 +1,389 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + [Migration("20250405225108_SeedCategories")] + partial class SeedCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs new file mode 100644 index 0000000..7e4a868 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/20250405225108_SeedCategories.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace TickAPI.Migrations +{ + /// + public partial class SeedCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "CategoryName", + table: "Categories", + newName: "Name"); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), "Workshops" }, + { new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), "Theatre" }, + { new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), "Comedy" }, + { new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), "Sports" }, + { new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), "Conferences" }, + { new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), "Music" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("ea58370b-2a17-4770-abea-66399ad69fb8")); + + migrationBuilder.DeleteData( + table: "Categories", + keyColumn: "Id", + keyValue: new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272")); + + migrationBuilder.RenameColumn( + name: "Name", + table: "Categories", + newName: "CategoryName"); + } + } +} diff --git a/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs new file mode 100644 index 0000000..8866990 --- /dev/null +++ b/TickAPI/TickAPI/Migrations/TickApiDbContextModelSnapshot.cs @@ -0,0 +1,386 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TickAPI.Common.TickApiDbContext; + +#nullable disable + +namespace TickAPI.Migrations +{ + [DbContext(typeof(TickApiDbContext))] + partial class TickApiDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryEvent", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "EventsId"); + + b.HasIndex("EventsId"); + + b.ToTable("CategoryEvent"); + }); + + modelBuilder.Entity("TickAPI.Addresses.Models.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("City") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FlatNumber") + .HasColumnType("bigint"); + + b.Property("HouseNumber") + .HasColumnType("bigint"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Street") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("TickAPI.Admins.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Login") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Admins"); + }); + + modelBuilder.Entity("TickAPI.Categories.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("ec3daf69-baa9-4fcd-a674-c09884a57272"), + Name = "Music" + }, + new + { + Id = new Guid("de89dd76-3b29-43e1-8f4b-5278b1b8bde2"), + Name = "Sports" + }, + new + { + Id = new Guid("ea58370b-2a17-4770-abea-66399ad69fb8"), + Name = "Conferences" + }, + new + { + Id = new Guid("4a086d9e-59de-4fd1-a1b2-bd9b5eec797c"), + Name = "Theatre" + }, + new + { + Id = new Guid("5f8dbe65-30be-453f-8f22-191a11b2977b"), + Name = "Comedy" + }, + new + { + Id = new Guid("4421327a-4bc8-4706-bec0-666f78ed0c69"), + Name = "Workshops" + }); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddressId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EventStatus") + .HasColumnType("int"); + + b.Property("MinimumAge") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizerId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("AddressId"); + + b.HasIndex("OrganizerId"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsVerified") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organizers"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AvailableFrom") + .HasColumnType("datetime2"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaxCount") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("EventId"); + + b.ToTable("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ForResell") + .HasColumnType("bit"); + + b.Property("NameOnTicket") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Seats") + .HasColumnType("nvarchar(max)"); + + b.Property("TypeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("CategoryEvent", b => + { + b.HasOne("TickAPI.Categories.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Events.Models.Event", null) + .WithMany() + .HasForeignKey("EventsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.HasOne("TickAPI.Addresses.Models.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.Organizers.Models.Organizer", "Organizer") + .WithMany("Events") + .HasForeignKey("OrganizerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + + b.Navigation("Organizer"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.HasOne("TickAPI.Events.Models.Event", "Event") + .WithMany("TicketTypes") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("TickAPI.Tickets.Models.Ticket", b => + { + b.HasOne("TickAPI.Customers.Models.Customer", "Owner") + .WithMany("Tickets") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TickAPI.TicketTypes.Models.TicketType", "Type") + .WithMany("Tickets") + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("TickAPI.Customers.Models.Customer", b => + { + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("TickAPI.Events.Models.Event", b => + { + b.Navigation("TicketTypes"); + }); + + modelBuilder.Entity("TickAPI.Organizers.Models.Organizer", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("TickAPI.TicketTypes.Models.TicketType", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs index 42e14a1..31b1c22 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs @@ -1,6 +1,12 @@ -namespace TickAPI.Organizers.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Models; + +namespace TickAPI.Organizers.Abstractions; public interface IOrganizerRepository { - + Task> GetOrganizerByEmailAsync(string organizerEmail); + Task AddNewOrganizerAsync(Organizer organizer); + Task VerifyOrganizerByEmailAsync(string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs index 572caad..0fb0bc6 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs @@ -1,6 +1,14 @@ -namespace TickAPI.Organizers.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Models; + +namespace TickAPI.Organizers.Abstractions; public interface IOrganizerService { + public Task> GetOrganizerByEmailAsync(string organizerEmail); + public Task> CreateNewOrganizerAsync(string email, string firstName, string lastName, string displayName); + + public Task VerifyOrganizerByEmailAsync(string organizerEmail); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs index f6c89ff..4643556 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizerController.cs @@ -1,4 +1,12 @@ using Microsoft.AspNetCore.Mvc; +using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Attributes; +using TickAPI.Common.Auth.Enums; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.DTOs.Request; +using TickAPI.Organizers.DTOs.Response; namespace TickAPI.Organizers.Controllers; @@ -6,5 +14,107 @@ namespace TickAPI.Organizers.Controllers; [Route("api/[controller]")] public class OrganizerController : ControllerBase { + private readonly IGoogleAuthService _googleAuthService; + private readonly IJwtService _jwtService; + private readonly IOrganizerService _organizerService; + private readonly IClaimsService _claimsService; + + public OrganizerController(IGoogleAuthService googleAuthService, IJwtService jwtService, + IOrganizerService organizerService, IClaimsService claimsService) + { + _googleAuthService = googleAuthService; + _jwtService = jwtService; + _organizerService = organizerService; + _claimsService = claimsService; + } + + [HttpPost("google-login")] + public async Task> GoogleLogin([FromBody] GoogleOrganizerLoginDto request) + { + var userDataResult = await _googleAuthService.GetUserDataFromAccessToken(request.AccessToken); + if(userDataResult.IsError) + return StatusCode(userDataResult.StatusCode, userDataResult.ErrorMsg); + + var userData = userDataResult.Value!; + + Result jwtTokenResult; + var existingOrganizerResult = await _organizerService.GetOrganizerByEmailAsync(userData.Email); + if (existingOrganizerResult.IsError) + { + jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, UserRole.NewOrganizer); + + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, true, false)); + } + + var isVerified = existingOrganizerResult.Value!.IsVerified; + + var role = isVerified ? UserRole.Organizer : UserRole.UnverifiedOrganizer; + + jwtTokenResult = _jwtService.GenerateJwtToken(userData.Email, role); + + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new GoogleOrganizerLoginResponseDto(jwtTokenResult.Value!, false, isVerified)); + } + + [AuthorizeWithPolicy(AuthPolicies.NewOrganizerPolicy)] + [HttpPost("create-organizer")] + public async Task> CreateOrganizer([FromBody] CreateOrganizerDto request) + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(StatusCodes.Status400BadRequest, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var newOrganizerResult = await _organizerService.CreateNewOrganizerAsync(email, request.FirstName, request.LastName, request.DisplayName); + if(newOrganizerResult.IsError) + return StatusCode(newOrganizerResult.StatusCode, newOrganizerResult.ErrorMsg); + + var jwtTokenResult = _jwtService.GenerateJwtToken(newOrganizerResult.Value!.Email, newOrganizerResult.Value!.IsVerified ? UserRole.Organizer : UserRole.UnverifiedOrganizer); + if(jwtTokenResult.IsError) + return StatusCode(jwtTokenResult.StatusCode, jwtTokenResult.ErrorMsg); + + return new ActionResult(new CreateOrganizerResponseDto(jwtTokenResult.Value!)); + } + // TODO: Add authorization with admin policy here + [HttpPost("verify-organizer")] + public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto request) + { + var verifyOrganizerResult = await _organizerService.VerifyOrganizerByEmailAsync(request.Email); + + if(verifyOrganizerResult.IsError) + return StatusCode(verifyOrganizerResult.StatusCode, verifyOrganizerResult.ErrorMsg); + + return Ok(); + } + + [AuthorizeWithPolicy(AuthPolicies.CreatedOrganizerPolicy)] + [HttpGet("about-me")] + public async Task> AboutMe() + { + var emailResult = _claimsService.GetEmailFromClaims(User.Claims); + if (emailResult.IsError) + { + return StatusCode(StatusCodes.Status400BadRequest, emailResult.ErrorMsg); + } + var email = emailResult.Value!; + + var organizerResult = await _organizerService.GetOrganizerByEmailAsync(email); + if (organizerResult.IsError) + return StatusCode(StatusCodes.Status500InternalServerError, + "cannot find organizer in database for authorized organizer request"); + + var organizer = organizerResult.Value!; + + var aboutMeResponse = + new AboutMeOrganizerResponseDto(organizer.Email, organizer.FirstName, organizer.LastName, organizer.DisplayName, organizer.IsVerified, organizer.CreationDate); + return new ActionResult(aboutMeResponse); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs new file mode 100644 index 0000000..cb985f1 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Request/CreateOrganizerDto.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Organizers.DTOs.Request; + +public record CreateOrganizerDto( + string FirstName, + string LastName, + string DisplayName +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs new file mode 100644 index 0000000..52ddda3 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Request/GoogleOrganizerLoginDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Organizers.DTOs.Request; + +public record GoogleOrganizerLoginDto( + string AccessToken +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs new file mode 100644 index 0000000..80c9464 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Request/VerifyOrganizerDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Organizers.DTOs.Request; + +public record VerifyOrganizerDto( + string Email +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs new file mode 100644 index 0000000..5928d16 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs @@ -0,0 +1,10 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record AboutMeOrganizerResponseDto( + string Email, + string FirstName, + string LastName, + string DisplayName, + bool IsVerified, + DateTime CreationDate +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs new file mode 100644 index 0000000..3c5c2b5 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/CreateOrganizerResponseDto.cs @@ -0,0 +1,5 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record CreateOrganizerResponseDto( + string Token +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs new file mode 100644 index 0000000..eaae43b --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/GoogleOrganizerLoginResponseDto.cs @@ -0,0 +1,7 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record GoogleOrganizerLoginResponseDto( + string Token, + bool IsNewOrganizer, + bool IsVerified +); \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Models/Organizer.cs b/TickAPI/TickAPI/Organizers/Models/Organizer.cs index 56df9ab..1d74909 100644 --- a/TickAPI/TickAPI/Organizers/Models/Organizer.cs +++ b/TickAPI/TickAPI/Organizers/Models/Organizer.cs @@ -1,6 +1,15 @@ -namespace TickAPI.Organizers.Models; +using TickAPI.Events.Models; + +namespace TickAPI.Organizers.Models; public class Organizer { - + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string? LastName { get; set; } + public DateTime CreationDate { get; set; } + public string DisplayName { get; set; } + public bool IsVerified { get; set; } + public ICollection Events { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs index f99ebb9..1a01bce 100644 --- a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs @@ -1,8 +1,56 @@ -using TickAPI.Organizers.Abstractions; +using Microsoft.EntityFrameworkCore; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Repositories; public class OrganizerRepository : IOrganizerRepository { + private readonly TickApiDbContext _tickApiDbContext; + + public OrganizerRepository(TickApiDbContext tickApiDbContext) + { + _tickApiDbContext = tickApiDbContext; + } + public async Task> GetOrganizerByEmailAsync(string organizerEmail) + { + var organizer = await _tickApiDbContext.Organizers.FirstOrDefaultAsync(organizer => organizer.Email == organizerEmail); + + if (organizer == null) + { + return Result.Failure(StatusCodes.Status404NotFound, $"organizer with email '{organizerEmail}' not found"); + } + + return Result.Success(organizer); + } + + public async Task AddNewOrganizerAsync(Organizer organizer) + { + _tickApiDbContext.Organizers.Add(organizer); + await _tickApiDbContext.SaveChangesAsync(); + } + + public async Task VerifyOrganizerByEmailAsync(string organizerEmail) + { + var organizerResult = await GetOrganizerByEmailAsync(organizerEmail); + + if(organizerResult.IsError) + return Result.PropagateError(organizerResult); + + var organizer = organizerResult.Value!; + + if (organizer.IsVerified) + { + return Result.Failure(StatusCodes.Status400BadRequest, $"organizer with email '{organizerEmail}' is already verified"); + } + + organizer.IsVerified = true; + await _tickApiDbContext.SaveChangesAsync(); + + return Result.Success(); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs index 22e0c39..9ff72ce 100644 --- a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs @@ -1,8 +1,51 @@ -using TickAPI.Organizers.Abstractions; +using TickAPI.Common.Results; +using TickAPI.Common.Results.Generic; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Events.Models; +using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Services; public class OrganizerService : IOrganizerService { + private readonly IOrganizerRepository _organizerRepository; + private readonly IDateTimeService _dateTimeService; + + public OrganizerService(IOrganizerRepository organizerRepository, IDateTimeService dateTimeService) + { + _organizerRepository = organizerRepository; + _dateTimeService = dateTimeService; + } + public async Task> GetOrganizerByEmailAsync(string organizerEmail) + { + return await _organizerRepository.GetOrganizerByEmailAsync(organizerEmail); + } + + public async Task> CreateNewOrganizerAsync(string email, string firstName, string lastName, string displayName) + { + var alreadyExistingResult = await GetOrganizerByEmailAsync(email); + if (alreadyExistingResult.IsSuccess) + return Result.Failure(StatusCodes.Status400BadRequest, + $"organizer with email '{email}' already exists"); + + var organizer = new Organizer + { + Email = email, + FirstName = firstName, + LastName = lastName, + DisplayName = displayName, + IsVerified = false, + CreationDate = _dateTimeService.GetCurrentDateTime(), + Events = new List() + }; + await _organizerRepository.AddNewOrganizerAsync(organizer); + return Result.Success(organizer); + } + + public async Task VerifyOrganizerByEmailAsync(string organizerEmail) + { + return await _organizerRepository.VerifyOrganizerByEmailAsync(organizerEmail); + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Program.cs b/TickAPI/TickAPI/Program.cs index 3b4c236..08779cd 100644 --- a/TickAPI/TickAPI/Program.cs +++ b/TickAPI/TickAPI/Program.cs @@ -1,11 +1,20 @@ -using TickAPI; +using System.Text; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using TickAPI.Admins.Abstractions; using TickAPI.Admins.Repositories; using TickAPI.Admins.Services; using TickAPI.Common.Auth.Abstractions; +using TickAPI.Common.Auth.Enums; using TickAPI.Common.Auth.Services; using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Pagination.Services; +using TickAPI.Common.TickApiDbContext; +using TickAPI.Common.Time.Abstractions; +using TickAPI.Common.Time.Services; using TickAPI.Customers.Abstractions; using TickAPI.Customers.Repositories; using TickAPI.Customers.Services; @@ -18,18 +27,60 @@ using TickAPI.Tickets.Abstractions; using TickAPI.Tickets.Repositories; using TickAPI.Tickets.Services; +using TickAPI.Addresses.Abstractions; +using TickAPI.Addresses.Repositories; +using TickAPI.Addresses.Services; +using TickAPI.Categories.Abstractions; +using TickAPI.Categories.Respositories; +using TickAPI.Categories.Services; +using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Claims.Services; // Builder constants const string allowClientPolicyName = "AllowClient"; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -builder.Services.AddAuthorization(); - // Add controllers to the container. builder.Services.AddControllers(); +// Add authentication. +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; + }) + .AddGoogle(options => + { + options.ClientId = builder.Configuration["Authentication:Google:ClientId"]; + options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Authentication:Jwt:Issuer"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Authentication:Jwt:SecurityKey"])) + }; + }); + +// Add authorization. +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(AuthPolicies.AdminPolicy.ToString(), policy => policy.RequireRole(UserRole.Admin.ToString())); + options.AddPolicy(AuthPolicies.VerifiedOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.Organizer.ToString())); + options.AddPolicy(AuthPolicies.CustomerPolicy.ToString(), policy => policy.RequireRole(UserRole.Customer.ToString())); + + options.AddPolicy(AuthPolicies.NewOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.NewOrganizer.ToString())); + options.AddPolicy(AuthPolicies.CreatedOrganizerPolicy.ToString(), policy => policy.RequireRole(UserRole.UnverifiedOrganizer.ToString(), UserRole.Organizer.ToString())); + options.AddPolicy(AuthPolicies.VerifiedUserPolicy.ToString(), policy => policy.RequireRole(UserRole.Admin.ToString(), UserRole.Organizer.ToString(), UserRole.Customer.ToString())); +}); + // Add admin services. builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -42,6 +93,10 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add address services. +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Add organizer services. builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -50,14 +105,49 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +// Add category services. +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Add common services. -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme", + Name = "Authorization", + Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey + }); + c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); + +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("ResellioDatabase")); +}); // Create CORS policy builder.Services.AddCors(options => @@ -73,6 +163,13 @@ }); }); +// TODO: when we start using redis we should probably also check here if we can connect to it +// Setup healtcheck +builder.Services.AddHealthChecks().AddSqlServer(connectionString: builder.Configuration.GetConnectionString("ResellioDatabase") ?? ""); + +// Add http client +builder.Services.AddHttpClient(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -88,4 +185,8 @@ app.UseCors(allowClientPolicyName); +app.MapHealthChecks("/health"); + +app.MapControllers(); + app.Run(); \ No newline at end of file diff --git a/TickAPI/TickAPI/TickAPI.csproj b/TickAPI/TickAPI/TickAPI.csproj index 66af13d..1e131f6 100644 --- a/TickAPI/TickAPI/TickAPI.csproj +++ b/TickAPI/TickAPI/TickAPI.csproj @@ -7,8 +7,19 @@ + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs new file mode 100644 index 0000000..b861fb5 --- /dev/null +++ b/TickAPI/TickAPI/TicketTypes/Models/TicketType.cs @@ -0,0 +1,16 @@ +using TickAPI.Events.Models; +using TickAPI.Tickets.Models; + +namespace TickAPI.TicketTypes.Models; + +public class TicketType +{ + public Guid Id { get; set; } + public Event Event { get; set; } + public string Description { get; set; } + public uint MaxCount { get; set; } + public decimal Price { get; set; } + public string Currency { get; set; } + public DateTime AvailableFrom { get; set; } + public ICollection Tickets { get; set; } +} \ No newline at end of file diff --git a/TickAPI/TickAPI/Tickets/Models/Ticket.cs b/TickAPI/TickAPI/Tickets/Models/Ticket.cs index a72e26b..21f765f 100644 --- a/TickAPI/TickAPI/Tickets/Models/Ticket.cs +++ b/TickAPI/TickAPI/Tickets/Models/Ticket.cs @@ -1,6 +1,14 @@ -namespace TickAPI.Tickets.Models; +using TickAPI.Customers.Models; +using TickAPI.TicketTypes.Models; + +namespace TickAPI.Tickets.Models; public class Ticket { - + public Guid Id { get; set; } + public TicketType Type { get; set; } + public Customer Owner { get; set; } + public string NameOnTicket { get; set; } + public string? Seats { get; set; } + public bool ForResell { get; set; } } \ No newline at end of file diff --git a/TickAPI/TickAPI/appsettings.example.json b/TickAPI/TickAPI/appsettings.example.json index 0602d10..9070fd3 100644 --- a/TickAPI/TickAPI/appsettings.example.json +++ b/TickAPI/TickAPI/appsettings.example.json @@ -9,5 +9,20 @@ "AllowedOrigins": [ "https://example.com", "https://another-site.com" - ] + ], + "ConnectionStrings": { + "ResellioDatabase": "Server=tcp:localhost,1433;Initial Catalog=resellioDB;Persist Security Info=False;User ID=sa;Password=Rese11io;TrustServerCertificate=True;Encrypt=False" + }, + "Authentication": { + "Google": { + "ClientId": "your-google-client-id-here", + "ClientSecret": "your-google-client-secret-here", + "UserInfoEndpoint" : "https://www.googleapis.com/oauth2/v3/userinfo" + }, + "Jwt": { + "Issuer": "your-api-issuer-here", + "SecurityKey": "IH4xhBUKl3z51Gig5MFfg4kl0yLOulGk", + "ExpirySeconds" : "3600" + } + } } diff --git a/docker-compose.yml b/docker-compose.yml index 728575d..cf8ef14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,29 @@ version: '3.8' services: - postgres: - image: postgres:latest - container_name: resellio_postgres - restart: always + azuresqledge: + image: mcr.microsoft.com/azure-sql-edge:latest + container_name: resellio_azuresqledge environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: resellioDB + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Rese11io + MSSQL_PID: Developer + MSSQL_DATABASE: resellioDB volumes: - - pg_data:/var/lib/postgresql/data + - azuresqledge_data:/var/opt/mssql ports: - - 5432:5432 + - 1433:1433 redis: image: redis:latest container_name: resellio_redis - restart: always volumes: - redis_data:/data ports: - 6379:6379 volumes: - pg_data: - driver: local redis_data: + driver: local + azuresqledge_data: driver: local \ No newline at end of file