Skip to content

Commit 215440a

Browse files
Merge pull request #14 from intility/copilot/fix-3
2 parents a239c31 + 9af3ccb commit 215440a

2 files changed

Lines changed: 135 additions & 16 deletions

File tree

JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -262,25 +262,124 @@ public async Task CreatePaginationMetaAsync_CreatesCorrectMetadata()
262262
var query = GetTestData();
263263
var pagination = new PaginationParameters { Number = 2, Size = 2 };
264264

265-
// Mock async behavior for in-memory testing
266-
// Note: This is a simplification; for EF Core you'd need proper async testing
267-
Task<int> CountAsync() => Task.FromResult(query.Count());
268-
269265
// Act
270-
var meta = await new
271-
{
272-
TotalResources = await CountAsync(),
273-
TotalPages = (int)Math.Ceiling(await CountAsync() / (double)pagination.Size),
274-
CurrentPage = pagination.Number,
275-
PageSize = pagination.Size,
276-
}.ToTaskResult();
266+
var meta = await query.CreatePaginationMetaAsync(pagination);
277267

278268
// Assert
279269
Assert.Equal(5, meta.TotalResources);
280270
Assert.Equal(3, meta.TotalPages);
281271
Assert.Equal(2, meta.CurrentPage);
282272
Assert.Equal(2, meta.PageSize);
283273
}
274+
275+
[Fact]
276+
public void ApplyPagination_WithInvalidPageNumber_ReturnsLastPage()
277+
{
278+
// Arrange
279+
var query = GetTestData(); // 5 items
280+
var pagination = new PaginationParameters { Number = 10, Size = 2 }; // Request page 10, but only 3 pages exist
281+
282+
// Act
283+
var result = query.ApplyPagination(pagination).ToList();
284+
285+
// Assert - Should return the last page (page 3) which has 1 item (item 5)
286+
Assert.Single(result);
287+
Assert.Equal(5, result[0].Id);
288+
Assert.Equal("Epsilon", result[0].Name);
289+
}
290+
291+
[Fact]
292+
public void ApplyPagination_WithPageZero_ReturnsFirstPage()
293+
{
294+
// Arrange
295+
var query = GetTestData();
296+
var pagination = new PaginationParameters { Number = 0, Size = 2 }; // Invalid page 0
297+
298+
// Act
299+
var result = query.ApplyPagination(pagination).ToList();
300+
301+
// Assert - Should return first page
302+
Assert.Equal(2, result.Count);
303+
Assert.Equal(1, result[0].Id);
304+
Assert.Equal(2, result[1].Id);
305+
}
306+
307+
[Fact]
308+
public async Task CreatePaginationMetaAsync_WithInvalidPageNumber_ReturnsLastPageInMetadata()
309+
{
310+
// Arrange
311+
var query = GetTestData(); // 5 items
312+
var pagination = new PaginationParameters { Number = 10, Size = 2 }; // Request page 10, but only 3 pages exist
313+
314+
// Create a simplified test scenario by manually implementing the meta logic
315+
var totalCount = query.Count();
316+
var totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size);
317+
var expectedCurrentPage = Math.Min(Math.Max(pagination.Number, 1), Math.Max(totalPages, 1));
318+
319+
// Act - for now we'll test the current behavior
320+
var meta = await query.CreatePaginationMetaAsync(pagination);
321+
322+
// Assert
323+
Assert.Equal(5, meta.TotalResources);
324+
Assert.Equal(3, meta.TotalPages);
325+
Assert.Equal(3, meta.CurrentPage); // Should be clamped to last page (3)
326+
Assert.Equal(2, meta.PageSize);
327+
}
328+
329+
[Fact]
330+
public async Task CreatePaginationMetaAsync_WithPageZero_ReturnsFirstPageInMetadata()
331+
{
332+
// Arrange
333+
var query = GetTestData();
334+
var pagination = new PaginationParameters { Number = 0, Size = 2 };
335+
336+
// Act - for now we'll test the current behavior
337+
var meta = await query.CreatePaginationMetaAsync(pagination);
338+
339+
// Assert
340+
Assert.Equal(5, meta.TotalResources);
341+
Assert.Equal(3, meta.TotalPages);
342+
Assert.Equal(1, meta.CurrentPage); // Should be clamped to first page (1)
343+
Assert.Equal(2, meta.PageSize);
344+
}
345+
346+
[Fact]
347+
public void ApplyPagination_WithEmptyDataset_ReturnsEmptyResult()
348+
{
349+
// Arrange
350+
var emptyQuery = new List<TestEntity>().AsQueryable();
351+
var pagination = new PaginationParameters { Number = 2, Size = 10 };
352+
353+
// Act
354+
var result = emptyQuery.ApplyPagination(pagination).ToList();
355+
356+
// Assert
357+
Assert.Empty(result);
358+
}
359+
360+
[Fact]
361+
public async Task Issue_Scenario_PageTwoOfOneTotal_ReturnsLastPageData()
362+
{
363+
// Arrange - exact scenario from the issue: 6 total resources, page size 10, requesting page 2
364+
var query = GetTestData(); // 5 items
365+
var largePageQuery = query.Take(6).AsQueryable(); // Take 6 to match issue example
366+
var pagination = new PaginationParameters { Number = 2, Size = 10 }; // page 2, size 10
367+
368+
// Act
369+
var result = largePageQuery.ApplyPagination(pagination).ToList();
370+
var meta = await largePageQuery.CreatePaginationMetaAsync(pagination);
371+
372+
// Assert - Should return the first page (which is also the last page) with data
373+
Assert.Equal(5, result.Count); // All 5 items should be returned (first page = last page)
374+
Assert.Equal(5, meta.TotalResources);
375+
Assert.Equal(1, meta.TotalPages); // Only 1 page with size 10 for 5 items
376+
Assert.Equal(1, meta.CurrentPage); // Should be clamped to page 1 (the last available page)
377+
Assert.Equal(10, meta.PageSize);
378+
379+
// Verify we got actual data, not empty results
380+
Assert.True(result.Any());
381+
Assert.Equal(1, result.First().Id);
382+
}
284383
}
285384

286385
// Helper extension to simulate async for in-memory testing

JsonApiToolkit/Extensions/Querying/PaginationHandler.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@ public static class PaginationHandler
2222
/// <returns>A new IQueryable with pagination applied (Skip/Take)</returns>
2323
/// <remarks>
2424
/// Translates the page-based pagination model (page number and size) into the offset-based
25-
/// pagination used by LINQ (Skip and Take).
25+
/// pagination used by LINQ (Skip and Take). Invalid page numbers are clamped to valid ranges.
2626
/// </remarks>
2727
public static IQueryable<T> ApplyPagination<T>(
2828
this IQueryable<T> query,
2929
PaginationParameters pagination
3030
)
3131
{
32-
int skip = (pagination.Number - 1) * pagination.Size;
32+
// Calculate total count and pages to determine valid page range
33+
int totalCount = query.Count();
34+
int totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size);
35+
36+
// Clamp page number to valid range (1 to totalPages, default to 1 if empty)
37+
int effectivePage = Math.Max(1, Math.Min(pagination.Number, Math.Max(totalPages, 1)));
38+
39+
int skip = (effectivePage - 1) * pagination.Size;
3340
return query.Skip(skip).Take(pagination.Size);
3441
}
3542

@@ -42,21 +49,34 @@ PaginationParameters pagination
4249
/// <returns>A PaginationMeta object containing total counts and pagination information</returns>
4350
/// <remarks>
4451
/// This method executes a COUNT query on the database to determine the total number of resources
45-
/// and calculates total pages based on the page size.
52+
/// and calculates total pages based on the page size. Invalid page numbers are clamped to valid ranges.
4653
/// </remarks>
4754
public static async Task<PaginationMeta> CreatePaginationMetaAsync<T>(
4855
this IQueryable<T> query,
4956
PaginationParameters pagination
5057
)
5158
{
52-
int totalCount = await query.CountAsync();
59+
int totalCount;
60+
try
61+
{
62+
totalCount = await query.CountAsync();
63+
}
64+
catch (InvalidOperationException)
65+
{
66+
// Fallback for in-memory queryables that don't support async operations
67+
totalCount = query.Count();
68+
}
69+
5370
int totalPages = (int)Math.Ceiling(totalCount / (double)pagination.Size);
5471

72+
// Clamp page number to valid range (1 to totalPages, default to 1 if empty)
73+
int effectivePage = Math.Max(1, Math.Min(pagination.Number, Math.Max(totalPages, 1)));
74+
5575
return new PaginationMeta
5676
{
5777
TotalResources = totalCount,
5878
TotalPages = totalPages,
59-
CurrentPage = pagination.Number,
79+
CurrentPage = effectivePage,
6080
PageSize = pagination.Size,
6181
};
6282
}

0 commit comments

Comments
 (0)