From 51f54e6550281515e16d7b8a9141c82532b7cf2a Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 18:51:53 +0200 Subject: [PATCH 1/4] Make `LastName` nullable in `AboutMeOrganizerResponseDto` --- .../Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs index 5928d16..b7a983c 100644 --- a/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/AboutMeOrganizerResponseDto.cs @@ -3,7 +3,7 @@ public record AboutMeOrganizerResponseDto( string Email, string FirstName, - string LastName, + string? LastName, string DisplayName, bool IsVerified, DateTime CreationDate From cf2fadab59e618e406208eba3c418b66419c7230 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 18:53:25 +0200 Subject: [PATCH 2/4] Add endpoint for getting unverified organizers --- .../Abstractions/IOrganizerRepository.cs | 1 + .../Abstractions/IOrganizerService.cs | 6 ++++- .../Controllers/OrganizersController.cs | 9 ++++++++ .../GetUnverifiedOrganizerResponseDto.cs | 8 +++++++ .../Repositories/OrganizerRepository.cs | 5 +++++ .../Organizers/Services/OrganizerService.cs | 22 +++++++++++++++++-- 6 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs index 31b1c22..29513c6 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerRepository.cs @@ -9,4 +9,5 @@ public interface IOrganizerRepository Task> GetOrganizerByEmailAsync(string organizerEmail); Task AddNewOrganizerAsync(Organizer organizer); Task VerifyOrganizerByEmailAsync(string organizerEmail); + IQueryable GetOrganizers(); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs index 0fb0bc6..226028f 100644 --- a/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Abstractions/IOrganizerService.cs @@ -1,5 +1,7 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Pagination.Responses; +using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; +using TickAPI.Organizers.DTOs.Response; using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Abstractions; @@ -11,4 +13,6 @@ public interface IOrganizerService public Task> CreateNewOrganizerAsync(string email, string firstName, string lastName, string displayName); public Task VerifyOrganizerByEmailAsync(string organizerEmail); + + public Task>> GetUnverifiedOrganizersAsync(int page, int pageSize); } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs index efecb40..604ef1a 100644 --- a/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs +++ b/TickAPI/TickAPI/Organizers/Controllers/OrganizersController.cs @@ -3,6 +3,7 @@ using TickAPI.Common.Auth.Attributes; using TickAPI.Common.Auth.Enums; using TickAPI.Common.Claims.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results.Generic; using TickAPI.Organizers.Abstractions; using TickAPI.Organizers.DTOs.Request; @@ -91,6 +92,14 @@ public async Task VerifyOrganizer([FromBody] VerifyOrganizerDto re return verifyOrganizerResult.ToObjectResult(); } + [AuthorizeWithPolicy(AuthPolicies.AdminPolicy)] + [HttpGet("unverified")] + public async Task>> GetUnverifiedOrganizers([FromQuery] int page, [FromQuery] int pageSize) + { + var result = await _organizerService.GetUnverifiedOrganizersAsync(page, pageSize); + return result.ToObjectResult(); + } + [AuthorizeWithPolicy(AuthPolicies.CreatedOrganizerPolicy)] [HttpGet("about-me")] public async Task> AboutMe() diff --git a/TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs b/TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs new file mode 100644 index 0000000..397fb29 --- /dev/null +++ b/TickAPI/TickAPI/Organizers/DTOs/Response/GetUnverifiedOrganizerResponseDto.cs @@ -0,0 +1,8 @@ +namespace TickAPI.Organizers.DTOs.Response; + +public record GetUnverifiedOrganizerResponseDto( + string Email, + string FirstName, + string? LastName, + string DisplayName +); diff --git a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs index 1a01bce..6a43c22 100644 --- a/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs +++ b/TickAPI/TickAPI/Organizers/Repositories/OrganizerRepository.cs @@ -53,4 +53,9 @@ public async Task VerifyOrganizerByEmailAsync(string organizerEmail) return Result.Success(); } + + public IQueryable GetOrganizers() + { + return _tickApiDbContext.Organizers; + } } \ No newline at end of file diff --git a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs index 9ff72ce..75bbaf4 100644 --- a/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs +++ b/TickAPI/TickAPI/Organizers/Services/OrganizerService.cs @@ -1,8 +1,11 @@ -using TickAPI.Common.Results; +using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; +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.DTOs.Response; using TickAPI.Organizers.Models; namespace TickAPI.Organizers.Services; @@ -11,11 +14,13 @@ public class OrganizerService : IOrganizerService { private readonly IOrganizerRepository _organizerRepository; private readonly IDateTimeService _dateTimeService; + private readonly IPaginationService _paginationService; - public OrganizerService(IOrganizerRepository organizerRepository, IDateTimeService dateTimeService) + public OrganizerService(IOrganizerRepository organizerRepository, IDateTimeService dateTimeService, IPaginationService paginationService) { _organizerRepository = organizerRepository; _dateTimeService = dateTimeService; + _paginationService = paginationService; } public async Task> GetOrganizerByEmailAsync(string organizerEmail) @@ -48,4 +53,17 @@ public async Task VerifyOrganizerByEmailAsync(string organizerEmail) { return await _organizerRepository.VerifyOrganizerByEmailAsync(organizerEmail); } + + public async Task>> GetUnverifiedOrganizersAsync(int page, int pageSize) + { + var unverifiedOrganizers = _organizerRepository.GetOrganizers().Where(o => !o.IsVerified); + var paginatedResult = await _paginationService.PaginateAsync(unverifiedOrganizers, pageSize, page); + if (paginatedResult.IsError) + { + return Result>.PropagateError(paginatedResult); + } + var paginated = paginatedResult.Value!; + var mapped = _paginationService.MapData(paginated, (o) => new GetUnverifiedOrganizerResponseDto(o.Email, o.FirstName, o.LastName, o.DisplayName)); + return Result>.Success(mapped); + } } \ No newline at end of file From 4a71c77910c431c603ac363f30836c07fd957adb Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 18:53:37 +0200 Subject: [PATCH 3/4] Fix already existing tests --- .../Services/OrganizerServiceTests.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs index 1e91aa9..b220395 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Moq; +using TickAPI.Common.Pagination.Abstractions; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; @@ -29,9 +30,12 @@ public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsReturnedFromR var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -55,9 +59,12 @@ public async Task GetOrganizerByEmailAsync_WhenOrganizerWithEmailIsNotReturnedFr var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -94,9 +101,12 @@ public async Task CreateNewOrganizerAsync_WhenOrganizerDataIsValid_ShouldReturnN .Setup(m => m.GetCurrentDateTime()) .Returns(currentDate); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -138,9 +148,12 @@ public async Task CreateNewOrganizerAsync_WhenLastNameIsNull_ShouldReturnNewOrga .Setup(m => m.GetCurrentDateTime()) .Returns(currentDate); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -173,9 +186,12 @@ public async Task CreateNewOrganizerAsync_WhenWithNotUniqueEmail_ShouldReturnFai var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -199,9 +215,12 @@ public async Task VerifyOrganizerByEmailAsync_WhenVerificationSuccessful_ShouldR var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act @@ -223,9 +242,12 @@ public async Task VerifyOrganizerByEmailAsync_WhenVerificationNotSuccessful_Shou var dateTimeServiceMock = new Mock(); + var paginationServiceMock = new Mock(); + var sut = new OrganizerService( organizerRepositoryMock.Object, - dateTimeServiceMock.Object + dateTimeServiceMock.Object, + paginationServiceMock.Object ); // Act From 10f2884deeb4bb1b97a374d20a87bbb4c414f3a4 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Tue, 13 May 2025 19:13:58 +0200 Subject: [PATCH 4/4] Add tests for `GetUnverifiedOrganizersAsync` --- .../Services/OrganizerServiceTests.cs | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) diff --git a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs index b220395..e517360 100644 --- a/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs +++ b/TickAPI/TickAPI.Tests/Organizers/Services/OrganizerServiceTests.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Http; using Moq; using TickAPI.Common.Pagination.Abstractions; +using TickAPI.Common.Pagination.Responses; using TickAPI.Common.Results; using TickAPI.Common.Results.Generic; using TickAPI.Common.Time.Abstractions; using TickAPI.Organizers.Abstractions; +using TickAPI.Organizers.DTOs.Response; using TickAPI.Organizers.Models; using TickAPI.Organizers.Services; @@ -258,4 +260,303 @@ public async Task VerifyOrganizerByEmailAsync_WhenVerificationNotSuccessful_Shou Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); Assert.Equal($"organizer with email '{email}' not found", result.ErrorMsg); } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WhenPaginationSuccessful_ShouldReturnPaginatedUnverifiedOrganizers() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var unverifiedOrganizers = new List + { + new() { Email = "unverified1@test.com", FirstName = "First1", LastName = "Last1", DisplayName = "Display1", IsVerified = false }, + new() { Email = "unverified2@test.com", FirstName = "First2", LastName = "Last2", DisplayName = "Display2", IsVerified = false }, + new() { Email = "unverified3@test.com", FirstName = "First3", LastName = "Last3", DisplayName = "Display3", IsVerified = false } + }.AsQueryable(); + + var paginationDetails = new PaginationDetails(0, 3); + var paginatedData = new PaginatedData( + unverifiedOrganizers.ToList(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var expectedDtos = new List + { + new("unverified1@test.com", "First1", "Last1", "Display1"), + new("unverified2@test.com", "First2", "Last2", "Display2"), + new("unverified3@test.com", "First3", "Last3", "Display3") + }; + + var mappedData = new PaginatedData( + expectedDtos, + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(unverifiedOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(unverifiedOrganizers, pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + // Capture and verify the mapping function + Func capturedMapFunction = null; + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns, Func>((source, mapFunc) => + { + capturedMapFunction = mapFunc; + return mappedData; + }); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Equal(3, result.Value!.Data.Count); + + // Verify each DTO was correctly mapped + for (int i = 0; i < expectedDtos.Count; i++) + { + Assert.Equal(expectedDtos[i], result.Value.Data[i]); + } + + // Verify the mapping function works correctly + Assert.NotNull(capturedMapFunction); + var testOrganizer = new Organizer { Email = "test@example.com", FirstName = "TestFirst", LastName = "TestLast", DisplayName = "TestDisplay" }; + var mappedDto = capturedMapFunction(testOrganizer); + var expectedDto = new GetUnverifiedOrganizerResponseDto("test@example.com", "TestFirst", "TestLast", "TestDisplay"); + Assert.Equal(expectedDto, mappedDto); + } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WhenFilteringOrganizers_ShouldOnlyReturnUnverifiedOnes() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var mixedOrganizers = new List + { + new() { Email = "unverified1@test.com", FirstName = "First1", LastName = "Last1", DisplayName = "Display1", IsVerified = false }, + new() { Email = "verified1@test.com", FirstName = "First2", LastName = "Last2", DisplayName = "Display2", IsVerified = true }, + new() { Email = "unverified2@test.com", FirstName = "First3", LastName = "Last3", DisplayName = "Display3", IsVerified = false } + }.AsQueryable(); + + var filteredOrganizers = mixedOrganizers.Where(o => !o.IsVerified).ToList(); + + var paginationDetails = new PaginationDetails(0, 2); + var paginatedData = new PaginatedData( + filteredOrganizers, + page, + pageSize, + false, + false, + paginationDetails + ); + + var expectedDtos = new List + { + new("unverified1@test.com", "First1", "Last1", "Display1"), + new("unverified2@test.com", "First3", "Last3", "Display3") + }; + + var mappedData = new PaginatedData( + expectedDtos, + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(mixedOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(It.Is>(q => q.Count() == 2), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns(mappedData); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Equal(2, result.Value!.Data.Count); + Assert.Equal(expectedDtos, result.Value.Data); + Assert.DoesNotContain(result.Value.Data, dto => dto.Email == "verified1@test.com"); + } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WhenNoPaginationResults_ShouldReturnEmptyList() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var emptyOrganizers = new List().AsQueryable(); + + var paginationDetails = new PaginationDetails(0, 0); + var paginatedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var mappedData = new PaginatedData( + new List(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(emptyOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns(mappedData); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Empty(result.Value!.Data); + } + + [Fact] + public async Task GetUnverifiedOrganizersAsync_WithNullLastNames_ShouldMapCorrectly() + { + // Arrange + const int page = 0; + const int pageSize = 10; + + var unverifiedOrganizers = new List + { + new() { Email = "nulllast@test.com", FirstName = "First", LastName = null, DisplayName = "Display", IsVerified = false }, + }.AsQueryable(); + + var paginationDetails = new PaginationDetails(0, 1); + var paginatedData = new PaginatedData( + unverifiedOrganizers.ToList(), + page, + pageSize, + false, + false, + paginationDetails + ); + + var expectedDto = new GetUnverifiedOrganizerResponseDto("nulllast@test.com", "First", null, "Display"); + var mappedData = new PaginatedData( + new List { expectedDto }, + page, + pageSize, + false, + false, + paginationDetails + ); + + var organizerRepositoryMock = new Mock(); + organizerRepositoryMock + .Setup(m => m.GetOrganizers()) + .Returns(unverifiedOrganizers); + + var dateTimeServiceMock = new Mock(); + + var paginationServiceMock = new Mock(); + paginationServiceMock + .Setup(m => m.PaginateAsync(It.IsAny>(), pageSize, page)) + .ReturnsAsync(Result>.Success(paginatedData)); + + // Verify the mapping function handles null LastName correctly + Func capturedMapFunction = null; + paginationServiceMock + .Setup(m => m.MapData(paginatedData, It.IsAny>())) + .Returns, Func>((source, mapFunc) => + { + capturedMapFunction = mapFunc; + return mappedData; + }); + + var sut = new OrganizerService( + organizerRepositoryMock.Object, + dateTimeServiceMock.Object, + paginationServiceMock.Object + ); + + // Act + var result = await sut.GetUnverifiedOrganizersAsync(page, pageSize); + + // Assert + Assert.True(result.IsSuccess); + Assert.Equal(mappedData, result.Value); + Assert.Single(result.Value!.Data); + Assert.Equal(expectedDto, result.Value.Data[0]); + Assert.Null(result.Value.Data[0].LastName); + + // Verify the mapping function works correctly with null LastName + Assert.NotNull(capturedMapFunction); + var testOrganizer = new Organizer { Email = "test@example.com", FirstName = "TestFirst", LastName = null, DisplayName = "TestDisplay" }; + var mappedDto = capturedMapFunction(testOrganizer); + var expectedMappedDto = new GetUnverifiedOrganizerResponseDto("test@example.com", "TestFirst", null, "TestDisplay"); + Assert.Equal(expectedMappedDto, mappedDto); + } } \ No newline at end of file