diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs index c757191304..d0743968ae 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs @@ -4,7 +4,6 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Resources diff --git a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs index 83f0a3fd28..079115d378 100644 --- a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs @@ -309,6 +309,7 @@ public virtual async Task> PageAsync(IQueryable) { // since EntityFramework does not support IQueryable.Reverse(), we need to know the number of queried entities @@ -317,10 +318,12 @@ public virtual async Task> PageAsync(IQueryable GetResourceHookContainer(Res return (IResourceHookContainer)GetResourceHookContainer(typeof(TResource), hook); } - public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] inclusionChain) + public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationshipsToNextLayer) { var idType = TypeHelper.GetIdentifierType(entityTypeForRepository); var parameterizedGetWhere = GetType() @@ -82,7 +82,7 @@ public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable en .MakeGenericMethod(entityTypeForRepository, idType); var casted = ((IEnumerable)entities).Cast(); var ids = casted.Select(e => e.StringId).Cast(idType); - var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, inclusionChain }); + var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, relationshipsToNextLayer }); if (values == null) return null; return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(entityTypeForRepository), values.Cast(entityTypeForRepository)); } @@ -129,11 +129,15 @@ IHooksDiscovery GetHookDiscovery(Type entityType) return discovery; } - IEnumerable GetWhereAndInclude(IEnumerable ids, RelationshipAttribute[] inclusionChain) where TResource : class, IIdentifiable + IEnumerable GetWhereAndInclude(IEnumerable ids, RelationshipAttribute[] relationshipsToNextLayer) where TResource : class, IIdentifiable { var repo = GetRepository(); var query = repo.Get().Where(e => ids.Contains(e.Id)); - return repo.Include(query, inclusionChain).ToList(); + foreach (var inclusionChainElement in relationshipsToNextLayer) + { + query = repo.Include(query, new RelationshipAttribute[] { inclusionChainElement }); + } + return query.ToList(); } IResourceReadRepository GetRepository() where TResource : class, IIdentifiable @@ -183,12 +187,6 @@ public Dictionary LoadImplicitlyAffected( } return implicitlyAffected.ToDictionary(kvp => kvp.Key, kvp => TypeHelper.CreateHashSetFor(kvp.Key.RightType, kvp.Value)); - - } - - private IEnumerable CreateHashSet(Type type, IList elements) - { - return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(type), new object[] { elements }); } bool IsHasManyThrough(KeyValuePair kvp, @@ -201,3 +199,4 @@ bool IsHasManyThrough(KeyValuePair kvp, } } } + diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs index 017773d3d6..8157ab077f 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs @@ -14,7 +14,7 @@ public interface IPageService : IQueryParameterService /// int PageSize { get; set; } /// - /// What page are we currently on + /// The page requested. Note that the page number is one-based. /// int CurrentPage { get; set; } /// @@ -22,8 +22,12 @@ public interface IPageService : IQueryParameterService /// int TotalPages { get; } /// - /// Checks if pagination is enabled + /// Denotes if pagination is possible for the current request /// - bool ShouldPaginate(); + bool CanPaginate { get; } + /// + /// Denotes if pagination is backwards + /// + bool Backwards { get; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs b/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs index 6f1559246c..2827eaa2d9 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs @@ -10,27 +10,37 @@ namespace JsonApiDotNetCore.Query /// public class PageService : QueryParameterService, IPageService { - private IJsonApiOptions _options; + private readonly IJsonApiOptions _options; public PageService(IJsonApiOptions options) { _options = options; PageSize = _options.DefaultPageSize; } + /// public int? TotalRecords { get; set; } + /// public int PageSize { get; set; } + /// - public int CurrentPage { get; set; } + public int CurrentPage { get; set; } = 1; + + /// + public bool Backwards { get; set; } + /// public int TotalPages => (TotalRecords == null || PageSize == 0) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); + /// + public bool CanPaginate { get { return TotalPages > 1; } } + /// public virtual void Parse(KeyValuePair queryParameter) { - // expected input = page[size]=10 - // page[number]=1 + // expected input = page[size]= + // page[number]= 0> var propertyName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; const string SIZE = "size"; @@ -38,24 +48,41 @@ public virtual void Parse(KeyValuePair queryParameter) if (propertyName == SIZE) { - if (int.TryParse(queryParameter.Value, out var size)) + if (!int.TryParse(queryParameter.Value, out var size)) + { + ThrowBadPagingRequest(queryParameter, "value could not be parsed as an integer"); + } + else if (size < 1) + { + ThrowBadPagingRequest(queryParameter, "value needs to be greater than zero"); + } + else + { PageSize = size; - else - throw new JsonApiException(400, $"Invalid page size '{queryParameter.Value}'"); + } } else if (propertyName == NUMBER) - { - if (int.TryParse(queryParameter.Value, out var size)) - CurrentPage = size; + { + if (!int.TryParse(queryParameter.Value, out var number)) + { + ThrowBadPagingRequest(queryParameter, "value could not be parsed as an integer"); + } + else if (number == 0) + { + ThrowBadPagingRequest(queryParameter, "page index is not zero-based"); + } else - throw new JsonApiException(400, $"Invalid page number '{queryParameter.Value}'"); + { + Backwards = (number < 0); + CurrentPage = Math.Abs(number); + } } } - /// - public bool ShouldPaginate() + private void ThrowBadPagingRequest(KeyValuePair parameter, string message) { - return (PageSize > 0) || ((CurrentPage == 1 || CurrentPage == 0) && TotalPages <= 0); + throw new JsonApiException(400, $"Invalid page query parameter '{parameter.Key}={parameter.Value}': {message}"); } + } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs index 9b00cdc23a..8b195a661a 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -31,10 +31,14 @@ public TopLevelLinks GetTopLevelLinks(ResourceContext primaryResource) { TopLevelLinks topLevelLinks = null; if (ShouldAddTopLevelLink(primaryResource, Link.Self)) + { topLevelLinks = new TopLevelLinks { Self = GetSelfTopLevelLink(primaryResource.ResourceName) }; + } - if (ShouldAddTopLevelLink(primaryResource, Link.Paging)) - SetPageLinks(primaryResource, ref topLevelLinks); + if (ShouldAddTopLevelLink(primaryResource, Link.Paging) && _pageService.CanPaginate) + { + SetPageLinks(primaryResource, topLevelLinks ??= new TopLevelLinks()); + } return topLevelLinks; } @@ -48,29 +52,31 @@ public TopLevelLinks GetTopLevelLinks(ResourceContext primaryResource) private bool ShouldAddTopLevelLink(ResourceContext primaryResource, Link link) { if (primaryResource.TopLevelLinks != Link.NotConfigured) + { return primaryResource.TopLevelLinks.HasFlag(link); + } + return _options.TopLevelLinks.HasFlag(link); } - private void SetPageLinks(ResourceContext primaryResource, ref TopLevelLinks links) + private void SetPageLinks(ResourceContext primaryResource, TopLevelLinks links) { - if (!_pageService.ShouldPaginate()) - return; - - links = links ?? new TopLevelLinks(); - if (_pageService.CurrentPage > 1) { - links.First = GetPageLink(primaryResource, 1, _pageService.PageSize); links.Prev = GetPageLink(primaryResource, _pageService.CurrentPage - 1, _pageService.PageSize); } if (_pageService.CurrentPage < _pageService.TotalPages) + { links.Next = GetPageLink(primaryResource, _pageService.CurrentPage + 1, _pageService.PageSize); - + } if (_pageService.TotalPages > 0) + { + links.Self = GetPageLink(primaryResource, _pageService.CurrentPage, _pageService.PageSize); + links.First = GetPageLink(primaryResource, 1, _pageService.PageSize); links.Last = GetPageLink(primaryResource, _pageService.TotalPages, _pageService.PageSize); + } } private string GetSelfTopLevelLink(string resourceName) @@ -80,6 +86,11 @@ private string GetSelfTopLevelLink(string resourceName) private string GetPageLink(ResourceContext primaryResource, int pageOffset, int pageSize) { + if (_pageService.Backwards) + { + pageOffset = -pageOffset; + } + return $"{GetBasePath()}/{primaryResource.ResourceName}?page[size]={pageSize}&page[number]={pageOffset}"; } @@ -89,7 +100,9 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) { var resourceContext = _provider.GetResourceContext(resourceName); if (ShouldAddResourceLink(resourceContext, Link.Self)) + { return new ResourceLinks { Self = GetSelfResourceLink(resourceName, id) }; + } return null; } @@ -101,7 +114,9 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship var childNavigation = relationship.PublicRelationshipName; RelationshipLinks links = null; if (ShouldAddRelationshipLink(parentResourceContext, relationship, Link.Related)) + { links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation) }; + } if (ShouldAddRelationshipLink(parentResourceContext, relationship, Link.Self)) { @@ -137,7 +152,9 @@ private string GetRelatedRelationshipLink(string parent, string parentId, string private bool ShouldAddResourceLink(ResourceContext resourceContext, Link link) { if (resourceContext.ResourceLinks != Link.NotConfigured) + { return resourceContext.ResourceLinks.HasFlag(link); + } return _options.ResourceLinks.HasFlag(link); } @@ -151,16 +168,24 @@ private bool ShouldAddResourceLink(ResourceContext resourceContext, Link link) private bool ShouldAddRelationshipLink(ResourceContext resourceContext, RelationshipAttribute relationship, Link link) { if (relationship.RelationshipLinks != Link.NotConfigured) + { return relationship.RelationshipLinks.HasFlag(link); + } if (resourceContext.RelationshipLinks != Link.NotConfigured) + { return resourceContext.RelationshipLinks.HasFlag(link); + } + return _options.RelationshipLinks.HasFlag(link); } protected string GetBasePath() { if (_options.RelativeLinks) + { return string.Empty; + } + return _currentRequest.BasePath; } } diff --git a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs index ea6f4c7a87..39efc5a72c 100644 --- a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs +++ b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs @@ -23,7 +23,7 @@ public class DefaultResourceService : IResourceService where TResource : class, IIdentifiable { - private readonly IPageService _pageManager; + private readonly IPageService _pageService; private readonly IJsonApiOptions _options; private readonly IFilterService _filterService; private readonly ISortService _sortService; @@ -44,7 +44,7 @@ public DefaultResourceService( { _includeService = queryParameters.FirstOrDefault(); _sparseFieldsService = queryParameters.FirstOrDefault(); - _pageManager = queryParameters.FirstOrDefault(); + _pageService = queryParameters.FirstOrDefault(); _sortService = queryParameters.FirstOrDefault(); _filterService = queryParameters.FirstOrDefault(); _options = options; @@ -99,7 +99,7 @@ public virtual async Task> GetAsync() } if (_options.IncludeTotalRecordCount) - _pageManager.TotalRecords = await _repository.CountAsync(entityQuery); + _pageService.TotalRecords = await _repository.CountAsync(entityQuery); // pagination should be done last since it will execute the query var pagedEntities = await ApplyPageQueryAsync(entityQuery); @@ -195,19 +195,24 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) { - if (!(_pageManager.PageSize > 0)) + if (!(_pageService.PageSize > 0)) { var allEntities = await _repository.ToListAsync(entities); return allEntities as IEnumerable; } + int pageOffset = _pageService.CurrentPage; + if (_pageService.Backwards) + { + pageOffset = -pageOffset; + } if (_logger?.IsEnabled(LogLevel.Information) == true) { - _logger?.LogInformation($"Applying paging query. Fetching page {_pageManager.CurrentPage} " + - $"with {_pageManager.PageSize} entities"); + _logger?.LogInformation($"Applying paging query. Fetching page {pageOffset} " + + $"with {_pageService.PageSize} entities"); } - return await _repository.PageAsync(entities, _pageManager.PageSize, _pageManager.CurrentPage); + return await _repository.PageAsync(entities, _pageService.PageSize, pageOffset); } /// diff --git a/test/IntegrationTests/Data/EntityRepositoryTests.cs b/test/IntegrationTests/Data/EntityRepositoryTests.cs index e0699ceb2b..39f414bf39 100644 --- a/test/IntegrationTests/Data/EntityRepositoryTests.cs +++ b/test/IntegrationTests/Data/EntityRepositoryTests.cs @@ -9,22 +9,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; using System.Threading.Tasks; using Xunit; - namespace JADNC.IntegrationTests.Data { public class EntityRepositoryTests { - - - public EntityRepositoryTests() - { - } - [Fact] public async Task UpdateAsync_AttributesUpdated_ShouldHaveSpecificallyThoseAttributesUpdated() { @@ -138,9 +129,9 @@ public async Task Paging_PageNumberIsZero_PretendsItsOne() } [Theory] - [InlineData(6, -1, new[] { 4, 5, 6, 7, 8, 9 })] - [InlineData(6, -2, new[] { 1, 2, 3 })] - [InlineData(20, -1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })] + [InlineData(6, -1, new[] { 9, 8, 7, 6, 5, 4 })] + [InlineData(6, -2, new[] { 3, 2, 1 })] + [InlineData(20, -1, new[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 })] public async Task Paging_PageNumberIsNegative_GiveBackReverseAmountOfIds(int pageSize, int pageNumber, int[] expectedIds) { // Arrange diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs deleted file mode 100644 index e34e63179e..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using JsonApiDotNetCoreExample; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; -using Xunit; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCoreExample.Data; -using Bogus; -using JsonApiDotNetCoreExample.Models; -using System; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests -{ - [Collection("WebHostCollection")] - public class PagingTests - { - private readonly AppDbContext _context; - private readonly Faker _todoItemFaker; - - public PagingTests(TestFixture fixture) - { - _context = fixture.GetService(); - _todoItemFaker = new Faker() - .RuleFor(t => t.Description, f => f.Lorem.Sentence()) - .RuleFor(t => t.Ordinal, f => f.Random.Number()) - .RuleFor(t => t.CreatedDate, f => f.Date.Past()); - } - - [Fact] - public async Task Server_IncludesPagination_Links() - { - // Arrange - var pageSize = 5; - const int minimumNumberOfRecords = 11; - _context.TodoItems.RemoveRange(_context.TodoItems); - - for(var i=0; i < minimumNumberOfRecords; i++) - _context.TodoItems.Add(_todoItemFaker.Generate()); - - await _context.SaveChangesAsync(); - - var numberOfPages = (int)Math.Ceiling(decimal.Divide(minimumNumberOfRecords, pageSize)); - var startPageNumber = 2; - - var builder = new WebHostBuilder() - .UseStartup(); - - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todoItems?page[number]=2"; - - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); - - // Act - var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var links = documents.Links; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotEmpty(links.First); - Assert.NotEmpty(links.Next); - Assert.NotEmpty(links.Last); - - Assert.Equal($"http://localhost/api/v1/todoItems?page[size]={pageSize}&page[number]={startPageNumber+1}", links.Next); - Assert.Equal($"http://localhost/api/v1/todoItems?page[size]={pageSize}&page[number]={startPageNumber-1}", links.Prev); - Assert.Equal($"http://localhost/api/v1/todoItems?page[size]={pageSize}&page[number]={numberOfPages}", links.Last); - Assert.Equal($"http://localhost/api/v1/todoItems?page[size]={pageSize}&page[number]=1", links.First); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs index ea50bf8201..3ff55bf427 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Models; +using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; @@ -27,24 +28,26 @@ public PagingTests(TestFixture fixture) .RuleFor(t => t.CreatedDate, f => f.Date.Past()); } - [Fact] - public async Task Can_Paginate_TodoItems() + [Theory] + [InlineData(1)] + [InlineData(-1)] + public async Task Pagination_WithPageSizeAndPageNumber_ReturnsCorrectSubsetOfResources(int pageNum) { // Arrange const int expectedEntitiesPerPage = 2; var totalCount = expectedEntitiesPerPage * 2; var person = new Person(); - var todoItems = _todoItemFaker.Generate(totalCount); - + var todoItems = _todoItemFaker.Generate(totalCount).ToList(); foreach (var todoItem in todoItems) + { todoItem.Owner = person; - + } + Context.TodoItems.RemoveRange(Context.TodoItems); Context.TodoItems.AddRange(todoItems); Context.SaveChanges(); - var route = $"/api/v1/todoItems?page[size]={expectedEntitiesPerPage}"; - // Act + var route = $"/api/v1/todoItems?page[size]={expectedEntitiesPerPage}&page[number]={pageNum}"; var response = await Client.GetAsync(route); // Assert @@ -53,16 +56,28 @@ public async Task Can_Paginate_TodoItems() var body = await response.Content.ReadAsStringAsync(); var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; - Assert.NotEmpty(deserializedBody); - Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count); + if (pageNum < 0) + { + todoItems.Reverse(); + } + var expectedTodoItems = todoItems.Take(expectedEntitiesPerPage).ToList(); + Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer()); + } - [Fact] - public async Task Can_Paginate_TodoItems_From_Start() + [Theory] + [InlineData(1, 1, 1, null, 2, 4)] + [InlineData(2, 2, 1, 1, 3, 4)] + [InlineData(3, 3, 1, 2, 4, 4)] + [InlineData(4, 4, 1, 3, null, 4)] + [InlineData(-1, -1, -1, null, -2, -4)] + [InlineData(-2, -2, -1, -1, -3, -4)] + [InlineData(-3, -3, -1, -2, -4, -4)] + [InlineData(-4, -4, -1, -3, null, -4)] + public async Task Pagination_OnGivenPage_DisplaysCorrectTopLevelLinks(int pageNum, int selfLink, int? firstLink, int? prevLink, int? nextLink, int? lastLink) { // Arrange - const int expectedEntitiesPerPage = 2; - var totalCount = expectedEntitiesPerPage * 2; + var totalCount = 20; var person = new Person(); var todoItems = _todoItemFaker.Generate(totalCount).ToList(); @@ -73,49 +88,55 @@ public async Task Can_Paginate_TodoItems_From_Start() Context.TodoItems.AddRange(todoItems); Context.SaveChanges(); - var route = $"/api/v1/todoItems?page[size]={expectedEntitiesPerPage}&page[number]=1"; - + string route = $"/api/v1/todoItems"; + if (pageNum != 1) + { + route += $"?page[size]=5&page[number]={pageNum}"; + } // Act var response = await Client.GetAsync(route); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; - - var expectedTodoItems = new[] { todoItems[0], todoItems[1] }; - Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer()); - } - - [Fact] - public async Task Can_Paginate_TodoItems_From_End() - { - // Arrange - const int expectedEntitiesPerPage = 2; - var totalCount = expectedEntitiesPerPage * 2; - var person = new Person(); - var todoItems = _todoItemFaker.Generate(totalCount).ToList(); - foreach (var ti in todoItems) - ti.Owner = person; - - Context.TodoItems.RemoveRange(Context.TodoItems); - Context.TodoItems.AddRange(todoItems); - Context.SaveChanges(); - var route = $"/api/v1/todoItems?page[size]={expectedEntitiesPerPage}&page[number]=-1"; - - // Act - var response = await Client.GetAsync(route); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data.Select(ti => ti.Id).ToArray(); - - var expectedTodoItems = new[] { todoItems[totalCount - 2].Id, todoItems[totalCount - 1].Id }; - for (int i = 0; i < expectedEntitiesPerPage-1 ; i++) - Assert.Contains(expectedTodoItems[i], deserializedBody); + var links = JsonConvert.DeserializeObject(body).Links; + + Assert.EndsWith($"/api/v1/todoItems?page[size]=5&page[number]={selfLink}", links.Self); + if (firstLink.HasValue) + { + Assert.EndsWith($"/api/v1/todoItems?page[size]=5&page[number]={firstLink.Value}", links.First); + } + else + { + Assert.Null(links.First); + } + + if (prevLink.HasValue) + { + Assert.EndsWith($"/api/v1/todoItems?page[size]=5&page[number]={prevLink}", links.Prev); + } + else + { + Assert.Null(links.Prev); + } + + if (nextLink.HasValue) + { + Assert.EndsWith($"/api/v1/todoItems?page[size]=5&page[number]={nextLink}", links.Next); + } + else + { + Assert.Null(links.Next); + } + + if (lastLink.HasValue) + { + Assert.EndsWith($"/api/v1/todoItems?page[size]=5&page[number]={lastLink}", links.Last); + } + else + { + Assert.Null(links.Last); + } } private class IdComparer : IEqualityComparer diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs index 0282f52527..32742560cf 100644 --- a/test/UnitTests/Builders/LinkBuilderTests.cs +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -153,7 +153,6 @@ public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks(Link } else { - Assert.Equal(expectedSelfLink, links.Self); Assert.True(CheckPages(links, pages)); } } @@ -162,7 +161,8 @@ private bool CheckPages(TopLevelLinks links, bool pages) { if (pages) { - return links.First == $"{_host}/articles?page[size]=10&page[number]=1" + return links.Self == $"{_host}/articles?page[size]=10&page[number]=2" + && links.First == $"{_host}/articles?page[size]=10&page[number]=1" && links.Prev == $"{_host}/articles?page[size]=10&page[number]=1" && links.Next == $"{_host}/articles?page[size]=10&page[number]=3" && links.Last == $"{_host}/articles?page[size]=10&page[number]=3"; @@ -192,7 +192,7 @@ private ILinksConfiguration GetConfiguration(Link resourceLinks = Link.All, private IPageService GetPageManager() { var mock = new Mock(); - mock.Setup(m => m.ShouldPaginate()).Returns(true); + mock.Setup(m => m.CanPaginate).Returns(true); mock.Setup(m => m.CurrentPage).Returns(2); mock.Setup(m => m.TotalPages).Returns(3); mock.Setup(m => m.PageSize).Returns(10); @@ -200,8 +200,6 @@ private IPageService GetPageManager() } - - private ResourceContext GetResourceContext(Link resourceLinks = Link.NotConfigured, Link topLevelLinks = Link.NotConfigured, Link relationshipLinks = Link.NotConfigured) where TResource : class, IIdentifiable diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 9834c2477c..f18a88772c 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -6,13 +6,10 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Query; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; using Moq; using Xunit; @@ -21,17 +18,10 @@ namespace UnitTests.Services public class EntityResourceService_Tests { private readonly Mock> _repositoryMock = new Mock>(); - private readonly ILoggerFactory _loggerFactory = new Mock().Object; - private readonly Mock _crMock; - private readonly Mock _pgsMock; - private readonly Mock _ufMock; private readonly IResourceGraph _resourceGraph; public EntityResourceService_Tests() { - _crMock = new Mock(); - _pgsMock = new Mock(); - _ufMock = new Mock(); _resourceGraph = new ResourceGraphBuilder() .AddResource() .AddResource()