Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 110 additions & 11 deletions JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,25 +262,124 @@ 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<int> 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);
Assert.Equal(3, meta.TotalPages);
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<TestEntity>().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
Expand Down
30 changes: 25 additions & 5 deletions JsonApiToolkit/Extensions/Querying/PaginationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ public static class PaginationHandler
/// <returns>A new IQueryable with pagination applied (Skip/Take)</returns>
/// <remarks>
/// 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.
/// </remarks>
public static IQueryable<T> ApplyPagination<T>(
this IQueryable<T> 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);
}

Expand All @@ -42,21 +49,34 @@ PaginationParameters pagination
/// <returns>A PaginationMeta object containing total counts and pagination information</returns>
/// <remarks>
/// 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.
/// </remarks>
public static async Task<PaginationMeta> CreatePaginationMetaAsync<T>(
this IQueryable<T> 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,
};
}
Expand Down