diff --git a/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs b/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs index 594a4a1..ab5db31 100644 --- a/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs +++ b/JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs @@ -262,18 +262,8 @@ public async Task CreatePaginationMetaAsync_CreatesCorrectMetadata() var query = GetTestData(); var pagination = new PaginationParameters { Number = 2, Size = 2 }; - // Mock async behavior for in-memory testing - // Note: This is a simplification; for EF Core you'd need proper async testing - Task CountAsync() => Task.FromResult(query.Count()); - // Act - var meta = await new - { - TotalResources = await CountAsync(), - TotalPages = (int)Math.Ceiling(await CountAsync() / (double)pagination.Size), - CurrentPage = pagination.Number, - PageSize = pagination.Size, - }.ToTaskResult(); + var meta = await query.CreatePaginationMetaAsync(pagination); // Assert Assert.Equal(5, meta.TotalResources); @@ -281,6 +271,115 @@ public async Task CreatePaginationMetaAsync_CreatesCorrectMetadata() Assert.Equal(2, meta.CurrentPage); Assert.Equal(2, meta.PageSize); } + + [Fact] + public void ApplyPagination_WithInvalidPageNumber_ReturnsLastPage() + { + // Arrange + var query = GetTestData(); // 5 items + var pagination = new PaginationParameters { Number = 10, Size = 2 }; // Request page 10, but only 3 pages exist + + // Act + var result = query.ApplyPagination(pagination).ToList(); + + // Assert - Should return the last page (page 3) which has 1 item (item 5) + Assert.Single(result); + Assert.Equal(5, result[0].Id); + Assert.Equal("Epsilon", result[0].Name); + } + + [Fact] + public void ApplyPagination_WithPageZero_ReturnsFirstPage() + { + // Arrange + var query = GetTestData(); + var pagination = new PaginationParameters { Number = 0, Size = 2 }; // Invalid page 0 + + // Act + var result = query.ApplyPagination(pagination).ToList(); + + // Assert - Should return first page + Assert.Equal(2, result.Count); + Assert.Equal(1, result[0].Id); + Assert.Equal(2, result[1].Id); + } + + [Fact] + public async Task CreatePaginationMetaAsync_WithInvalidPageNumber_ReturnsLastPageInMetadata() + { + // Arrange + var query = GetTestData(); // 5 items + var pagination = new PaginationParameters { Number = 10, Size = 2 }; // Request page 10, but only 3 pages exist + + // Create a simplified test scenario by manually implementing the meta logic + var totalCount = query.Count(); + var totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size); + var expectedCurrentPage = Math.Min(Math.Max(pagination.Number, 1), Math.Max(totalPages, 1)); + + // Act - for now we'll test the current behavior + var meta = await query.CreatePaginationMetaAsync(pagination); + + // Assert + Assert.Equal(5, meta.TotalResources); + Assert.Equal(3, meta.TotalPages); + Assert.Equal(3, meta.CurrentPage); // Should be clamped to last page (3) + Assert.Equal(2, meta.PageSize); + } + + [Fact] + public async Task CreatePaginationMetaAsync_WithPageZero_ReturnsFirstPageInMetadata() + { + // Arrange + var query = GetTestData(); + var pagination = new PaginationParameters { Number = 0, Size = 2 }; + + // Act - for now we'll test the current behavior + var meta = await query.CreatePaginationMetaAsync(pagination); + + // Assert + Assert.Equal(5, meta.TotalResources); + Assert.Equal(3, meta.TotalPages); + Assert.Equal(1, meta.CurrentPage); // Should be clamped to first page (1) + Assert.Equal(2, meta.PageSize); + } + + [Fact] + public void ApplyPagination_WithEmptyDataset_ReturnsEmptyResult() + { + // Arrange + var emptyQuery = new List().AsQueryable(); + var pagination = new PaginationParameters { Number = 2, Size = 10 }; + + // Act + var result = emptyQuery.ApplyPagination(pagination).ToList(); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task Issue_Scenario_PageTwoOfOneTotal_ReturnsLastPageData() + { + // Arrange - exact scenario from the issue: 6 total resources, page size 10, requesting page 2 + var query = GetTestData(); // 5 items + var largePageQuery = query.Take(6).AsQueryable(); // Take 6 to match issue example + var pagination = new PaginationParameters { Number = 2, Size = 10 }; // page 2, size 10 + + // Act + var result = largePageQuery.ApplyPagination(pagination).ToList(); + var meta = await largePageQuery.CreatePaginationMetaAsync(pagination); + + // Assert - Should return the first page (which is also the last page) with data + Assert.Equal(5, result.Count); // All 5 items should be returned (first page = last page) + Assert.Equal(5, meta.TotalResources); + Assert.Equal(1, meta.TotalPages); // Only 1 page with size 10 for 5 items + Assert.Equal(1, meta.CurrentPage); // Should be clamped to page 1 (the last available page) + Assert.Equal(10, meta.PageSize); + + // Verify we got actual data, not empty results + Assert.True(result.Any()); + Assert.Equal(1, result.First().Id); + } } // Helper extension to simulate async for in-memory testing diff --git a/JsonApiToolkit/Extensions/Querying/PaginationHandler.cs b/JsonApiToolkit/Extensions/Querying/PaginationHandler.cs index 7277dc5..c8d888f 100644 --- a/JsonApiToolkit/Extensions/Querying/PaginationHandler.cs +++ b/JsonApiToolkit/Extensions/Querying/PaginationHandler.cs @@ -22,14 +22,21 @@ public static class PaginationHandler /// A new IQueryable with pagination applied (Skip/Take) /// /// Translates the page-based pagination model (page number and size) into the offset-based - /// pagination used by LINQ (Skip and Take). + /// pagination used by LINQ (Skip and Take). Invalid page numbers are clamped to valid ranges. /// public static IQueryable ApplyPagination( this IQueryable query, PaginationParameters pagination ) { - int skip = (pagination.Number - 1) * pagination.Size; + // Calculate total count and pages to determine valid page range + int totalCount = query.Count(); + int totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size); + + // Clamp page number to valid range (1 to totalPages, default to 1 if empty) + int effectivePage = Math.Max(1, Math.Min(pagination.Number, Math.Max(totalPages, 1))); + + int skip = (effectivePage - 1) * pagination.Size; return query.Skip(skip).Take(pagination.Size); } @@ -42,21 +49,34 @@ PaginationParameters pagination /// A PaginationMeta object containing total counts and pagination information /// /// This method executes a COUNT query on the database to determine the total number of resources - /// and calculates total pages based on the page size. + /// and calculates total pages based on the page size. Invalid page numbers are clamped to valid ranges. /// public static async Task CreatePaginationMetaAsync( this IQueryable query, PaginationParameters pagination ) { - int totalCount = await query.CountAsync(); + int totalCount; + try + { + totalCount = await query.CountAsync(); + } + catch (InvalidOperationException) + { + // Fallback for in-memory queryables that don't support async operations + totalCount = query.Count(); + } + int totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size); + // Clamp page number to valid range (1 to totalPages, default to 1 if empty) + int effectivePage = Math.Max(1, Math.Min(pagination.Number, Math.Max(totalPages, 1))); + return new PaginationMeta { TotalResources = totalCount, TotalPages = totalPages, - CurrentPage = pagination.Number, + CurrentPage = effectivePage, PageSize = pagination.Size, }; }