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