From 6eaf2f94993ded88421cd9744dc23a2946688dd0 Mon Sep 17 00:00:00 2001 From: Geoff McElhanon Date: Thu, 14 Nov 2024 13:25:33 -0600 Subject: [PATCH] [ODS-6545] Align API's paging behavior with recent changes to Open API metadata (#1178) --- .../DataManagementControllerBase.cs | 2 +- .../GetEntitiesBySpecification.cs | 22 +- .../Queries/QueryParametersValidator.cs | 85 ++++--- .../Queries/Paging/PagingParameters.cs | 2 +- .../Queries/QueryParameterValidatorTests.cs | 214 ++++++++++++++++++ ...uite ResponseTests.postman_collection.json | 42 ++-- ...gration Test Suite.postman_collection.json | 150 +++++++++--- 7 files changed, 421 insertions(+), 96 deletions(-) create mode 100644 Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Queries/QueryParameterValidatorTests.cs diff --git a/Application/EdFi.Ods.Api/Controllers/DataManagementControllerBase.cs b/Application/EdFi.Ods.Api/Controllers/DataManagementControllerBase.cs index 6b7d3f645e..71e214c833 100644 --- a/Application/EdFi.Ods.Api/Controllers/DataManagementControllerBase.cs +++ b/Application/EdFi.Ods.Api/Controllers/DataManagementControllerBase.cs @@ -196,7 +196,7 @@ public virtual async Task GetAll( Response.Headers.Append(HeaderConstants.TotalCount, result.ResultMetadata.TotalCount.ToString()); } - if (queryParameters.MinAggregateId != null && result.Resources.Count > 0) + if (result.Resources.Count > 0) { Response.Headers.Append(HeaderConstants.NextPageToken, result.ResultMetadata.NextPageToken); } diff --git a/Application/EdFi.Ods.Common/Infrastructure/Repositories/GetEntitiesBySpecification.cs b/Application/EdFi.Ods.Common/Infrastructure/Repositories/GetEntitiesBySpecification.cs index 30cb24cbe5..7de1c37091 100644 --- a/Application/EdFi.Ods.Common/Infrastructure/Repositories/GetEntitiesBySpecification.cs +++ b/Application/EdFi.Ods.Common/Infrastructure/Repositories/GetEntitiesBySpecification.cs @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 // Licensed to the Ed-Fi Alliance under one or more agreements. // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. @@ -25,8 +25,7 @@ public class GetEntitiesBySpecification : NHibernateRepositoryOperationBase, IGetEntitiesBySpecification where TEntity : AggregateRootWithCompositeKey { - public const string _Id = "Id"; - private static IList EmptyList = new List(); + private static readonly IList _emptyList = Array.Empty(); private readonly IAggregateRootQueryBuilderProvider _pagedAggregateIdsCriteriaProvider; private readonly IDomainModelProvider _domainModelProvider; @@ -52,7 +51,7 @@ public async Task> GetBySpecificationAsync( CancellationToken cancellationToken) { var entityFullName = specification.GetApiModelFullName(); - + if (!_domainModelProvider.GetDomainModel().EntityByFullName.TryGetValue(entityFullName, out var aggregateRootEntity)) { throw new Exception($"Unable to find API model entity for '{entityFullName}'."); @@ -69,7 +68,7 @@ public async Task> GetBySpecificationAsync( { return new GetBySpecificationResult { - Results = EmptyList, + Results = _emptyList, ResultMetadata = new ResultMetadata { TotalCount = specificationResult.TotalCount, @@ -83,14 +82,9 @@ public async Task> GetBySpecificationAsync( var result = await _getEntitiesByAggregateIds.GetByAggregateIdsAsync(aggregateIds, cancellationToken); - string nextPageToken = null; - - if (queryParameters.MinAggregateId != null) - { - nextPageToken = PagingHelpers.GetPageToken( - specificationResult.Ids[^1].AggregateId + 1, - queryParameters.MaxAggregateId!.Value); - } + string nextPageToken = PagingHelpers.GetPageToken( + specificationResult.Ids[^1].AggregateId + 1, + queryParameters.MaxAggregateId ?? int.MaxValue); return new GetBySpecificationResult { @@ -133,7 +127,7 @@ async Task GetPagedAggregateIdsAsync() { if (countTemplateParameters.ParameterNames.Contains("MinAggregateId")) { - throw new BadRequestParameterException(BadRequestException.DefaultDetail, ["Total count cannot be determined while using key set paging."]); + throw new BadRequestParameterException(BadRequestException.DefaultDetail, ["Total count cannot be determined while using cursor paging (when pageToken is specified)."]); } } } diff --git a/Application/EdFi.Ods.Common/Models/Queries/QueryParametersValidator.cs b/Application/EdFi.Ods.Common/Models/Queries/QueryParametersValidator.cs index f6dbb18e6b..076e01b680 100644 --- a/Application/EdFi.Ods.Common/Models/Queries/QueryParametersValidator.cs +++ b/Application/EdFi.Ods.Common/Models/Queries/QueryParametersValidator.cs @@ -3,56 +3,85 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.Common.Providers.Queries.Paging; + namespace EdFi.Ods.Common.Models.Queries; -public class QueryParametersValidator +public static class QueryParametersValidator { - public static bool IsValid(QueryParameters queryParameters, int defaultPageLimitSize, out string errorMessage) + public static bool IsValid(IQueryParameters queryParameters, int defaultPageLimitSize, out string errorMessage) { - errorMessage = string.Empty; - - // Check if offset/limit paging is being used - bool isOffsetLimitPagingProvided = queryParameters.Offset.HasValue || queryParameters.Limit.HasValue; - - // Check if key set paging is being used - bool isKeySetPagingProvided = queryParameters.PageSize.HasValue - || queryParameters.MinAggregateId.HasValue - || queryParameters.MaxAggregateId.HasValue; + errorMessage = null; - // Ensure that only one type of paging is provided - if (isOffsetLimitPagingProvided && isKeySetPagingProvided) + // Look for multiple paging approaches indicated by parameters + if (queryParameters.Offset.HasValue + && (queryParameters.MinAggregateId.HasValue || queryParameters.MaxAggregateId.HasValue)) { - errorMessage = - "Both offset/limit and key set paging parameters are provided, but only one type of paging can be used."; + errorMessage = "Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together."; return false; } - // // Validate offset/limit paging: if one parameter is provided, both must be provided - if (isOffsetLimitPagingProvided) + // Determine which paging strategy is in use + var pagingStrategy = queryParameters.MinAggregateId.HasValue || queryParameters.MaxAggregateId.HasValue + ? PagingStrategy.KeySet + : PagingStrategy.LimitOffset; + + if (pagingStrategy == PagingStrategy.LimitOffset) { - if ((queryParameters.Offset ?? 0) < 0) + // Validate basic parameter usage + if (queryParameters.PageSize.HasValue && !queryParameters.Limit.HasValue) + { + if (queryParameters.Offset.HasValue) + { + errorMessage = "Use limit instead of pageSize when using limit/offset paging."; + } + else + { + errorMessage = "PageToken is required when pageSize is specified."; + } + + return false; + } + + // Validate the values provided + if (queryParameters.Offset is < 0) { errorMessage = $"Offset cannot be a negative value."; return false; } - - if ((queryParameters.Limit ?? 0) < 0 || (queryParameters.Limit ?? defaultPageLimitSize) > defaultPageLimitSize) + + if (queryParameters.Limit is < 0) { - errorMessage = $"Limit must be omitted or set to a value between 0 and {defaultPageLimitSize}."; + errorMessage = $"Limit must be a value between 0 and {defaultPageLimitSize}."; return false; } - } - // Validate key set paging: if one parameter is provided, all must be provided - if (isKeySetPagingProvided) + if (queryParameters.Limit.HasValue && queryParameters.Limit > defaultPageLimitSize) + { + errorMessage = $"Limit must be a value between 0 and {defaultPageLimitSize}."; + return false; + } + } + else { - if (!queryParameters.PageSize.HasValue - || !queryParameters.MinAggregateId.HasValue - || !queryParameters.MaxAggregateId.HasValue) + // Cursor paging + if (queryParameters.Limit.HasValue && !queryParameters.PageSize.HasValue) { - errorMessage = "Page token and page size must both be provided for key set paging."; + errorMessage = "Use pageSize instead of limit when using cursor paging with pageToken."; + return false; + } + // Validate the values provided + if (queryParameters.PageSize is < 0) + { + errorMessage = $"PageSize must be a value between 0 and {defaultPageLimitSize}."; + return false; + } + + if (queryParameters.PageSize.HasValue && queryParameters.PageSize > defaultPageLimitSize) + { + errorMessage = $"PageSize must be a value between 0 and {defaultPageLimitSize}."; return false; } } diff --git a/Application/EdFi.Ods.Common/Providers/Queries/Paging/PagingParameters.cs b/Application/EdFi.Ods.Common/Providers/Queries/Paging/PagingParameters.cs index fbbe535a9b..3ae979a905 100644 --- a/Application/EdFi.Ods.Common/Providers/Queries/Paging/PagingParameters.cs +++ b/Application/EdFi.Ods.Common/Providers/Queries/Paging/PagingParameters.cs @@ -8,7 +8,7 @@ public PagingParameters() { } public PagingParameters(IQueryParameters queryParameters) { - if (queryParameters.MinAggregateId != null && queryParameters.MaxAggregateId != null && queryParameters.PageSize != null) + if (queryParameters.MinAggregateId != null && queryParameters.MaxAggregateId != null) { PageSize = queryParameters.PageSize; MinAggregateId = queryParameters.MinAggregateId; diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Queries/QueryParameterValidatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Queries/QueryParameterValidatorTests.cs new file mode 100644 index 0000000000..14fbbd2b7e --- /dev/null +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/Queries/QueryParameterValidatorTests.cs @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using EdFi.Ods.Common; +using EdFi.Ods.Common.Models.Queries; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.Tests.EdFi.Ods.Common.Models.Queries; + +[TestFixture] +public class QueryParametersValidatorTests +{ + private const int DefaultPageLimitSize = 100; + + [Flags] + public enum Parameters + { + None = 0, + PageSize = 1, + PageToken = 2, + Limit = 4, + Offset = 8, + } + + [TestCase(true, Parameters.None)] + [TestCase(false, Parameters.PageSize, "PageToken is required when pageSize is specified.")] + [TestCase(true, Parameters.PageToken)] + [TestCase(true, Parameters.PageToken | Parameters.PageSize)] + [TestCase(true, Parameters.Limit)] + [TestCase(true, Parameters.Limit | Parameters.PageSize)] + [TestCase(false, Parameters.Limit | Parameters.PageToken, "Use pageSize instead of limit when using cursor paging with pageToken.")] + [TestCase(true, Parameters.Limit | Parameters.PageToken | Parameters.PageSize)] + [TestCase(true, Parameters.Offset)] + [TestCase(false, Parameters.Offset | Parameters.PageSize, "Use limit instead of pageSize when using limit/offset paging.")] + [TestCase(false, Parameters.Offset | Parameters.PageToken, "Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together.")] + [TestCase(false, Parameters.Offset | Parameters.PageToken | Parameters.PageSize, "Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together.")] + [TestCase(true, Parameters.Offset | Parameters.Limit)] + [TestCase(true, Parameters.Offset | Parameters.Limit | Parameters.PageSize)] + [TestCase(false, Parameters.Offset | Parameters.Limit | Parameters.PageToken, "Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together.")] + [TestCase(false, Parameters.Offset | Parameters.Limit | Parameters.PageToken | Parameters.PageSize, "Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together.")] + public void When_valid_parameters_are_provided_in_various_combinations(bool isValid, Parameters parameters, string validationMessage = null) + { + // Arrange + var queryParameters = A.Fake(); + + if ((parameters & Parameters.Limit) != Parameters.None) + { + queryParameters.Limit = 50; + } + + if ((parameters & Parameters.Offset) != Parameters.None) + { + queryParameters.Offset = 50; + } + + if ((parameters & Parameters.PageToken) != Parameters.None) + { + queryParameters.MinAggregateId = 1; + queryParameters.MaxAggregateId = 100; + } + + if ((parameters & Parameters.PageSize) != Parameters.None) + { + queryParameters.PageSize = 50; + } + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + if (isValid) + { + result.ShouldBeTrue(); + errorMessage.ShouldBeNull(); + } + else + { + result.ShouldBeFalse(); + errorMessage.ShouldBe(validationMessage); + } + } + + [Test] + public void IsValid_WithNegativeOffset_ShouldReturnFalseWithErrorMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.Offset = -1; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeFalse(); + errorMessage.ShouldBe("Offset cannot be a negative value."); + } + + [Test] + public void IsValid_WithNegativeLimit_ShouldReturnFalseWithErrorMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.Limit = -1; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeFalse(); + errorMessage.ShouldBe($"Limit must be a value between 0 and {DefaultPageLimitSize}."); + } + + [Test] + public void IsValid_WithLimitGreaterThanDefaultPageLimit_ShouldReturnFalseWithErrorMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.Limit = DefaultPageLimitSize + 1; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeFalse(); + errorMessage.ShouldBe($"Limit must be a value between 0 and {DefaultPageLimitSize}."); + } + + [Test] + public void IsValid_WithLimitEqualToTheDefaultPageLimit_ShouldReturnTrueWithNullMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.Limit = DefaultPageLimitSize; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeTrue(); + errorMessage.ShouldBeNull(); + } + + [Test] + public void IsValid_WithNegativePageSize_ShouldReturnFalseWithErrorMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.MinAggregateId = 1; + queryParameters.MaxAggregateId = 100; + queryParameters.PageSize = -1; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeFalse(); + errorMessage.ShouldBe($"PageSize must be a value between 0 and {DefaultPageLimitSize}."); + } + + [Test] + public void IsValid_WithPageSizeGreaterThanDefaultLimit_ShouldReturnFalseWithErrorMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.MinAggregateId = 1; + queryParameters.MaxAggregateId = 100; + queryParameters.PageSize = DefaultPageLimitSize + 1; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeFalse(); + errorMessage.ShouldBe($"PageSize must be a value between 0 and {DefaultPageLimitSize}."); + } + + [Test] + public void IsValid_WithPageSizeEqualToDefaultLimit_ShouldReturnTrueWithNullMessage() + { + // Arrange + var queryParameters = A.Fake(); + queryParameters.MinAggregateId = 1; + queryParameters.MaxAggregateId = 100; + queryParameters.PageSize = DefaultPageLimitSize; + + string errorMessage; + + // Act + var result = QueryParametersValidator.IsValid(queryParameters, DefaultPageLimitSize, out errorMessage); + + // Assert + result.ShouldBeTrue(); + errorMessage.ShouldBeNull(); + } +} diff --git a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json index 8481fdde3c..b645f10fbe 100644 --- a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "d3d5da8b-42ff-4fcb-bb76-d215b5cebd98", + "_postman_id": "5e983aaa-3e8f-45dd-af38-2aa59f79ceda", "name": "Ed-Fi ODS/API Integration Test Suite ResponseTests", "description": "Localhost integration testing using Postman Scripts", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -863,17 +863,18 @@ " pm.expect(pm.response.code).to.equal(400);", "});", "", - "pm.test(\"Should return a message indicating that the limit must be omitted or set to a value between 0 and 500.\", () => {", + "pm.test(\"Should return a message indicating that the limit must be between 0 and 500.\", () => {", " const problemDetails = pm.response.json();", "", " pm.expect(pm.response.code).equal(problemDetails.status);", " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:bad-request:parameter-validation-failed\");", " pm.expect(problemDetails.detail).to.equal(\"The request construction was invalid.\");", "", - " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be omitted or set to a value between 0 and 500.\");", + " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be a value between 0 and 500.\");", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -882,7 +883,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -927,17 +929,18 @@ " pm.expect(pm.response.code).to.equal(400);", "});", "", - "pm.test(\"Should return a message indicating that the limit must be omitted or set to a value between 0 and 500.\", () => {", + "pm.test(\"Should return a message indicating that the limit must be a value between 0 and 500.\", () => {", " const problemDetails = pm.response.json();", "", " pm.expect(pm.response.code).equal(problemDetails.status);", " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:bad-request:parameter-validation-failed\");", " pm.expect(problemDetails.detail).to.equal(\"The request construction was invalid.\");", "", - " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be omitted or set to a value between 0 and 500.\");", + " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be a value between 0 and 500.\");", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -946,7 +949,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2520,7 +2524,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2531,17 +2536,18 @@ " pm.expect(pm.response.code).to.equal(400);", "});", "", - "pm.test(\"Should return a message indicating that the limit must be omitted or set to a value between 0 and 500.\", () => {", + "pm.test(\"Should return a message indicating that the limit must be a value between 0 and 500.\", () => {", " const problemDetails = pm.response.json();", "", " pm.expect(pm.response.code).equal(problemDetails.status);", " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:bad-request:parameter-validation-failed\");", " pm.expect(problemDetails.detail).to.equal(\"The request construction was invalid.\");", "", - " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be omitted or set to a value between 0 and 500.\");", + " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be a value between 0 and 500.\");", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -2588,7 +2594,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -2599,17 +2606,18 @@ " pm.expect(pm.response.code).to.equal(400);", "});", "", - "pm.test(\"Should return a message indicating that the limit must be omitted or set to a value between 0 and 500.\", () => {", + "pm.test(\"Should return a message indicating that the limit must be a value between 0 and 500.\", () => {", " const problemDetails = pm.response.json();", "", " pm.expect(pm.response.code).equal(problemDetails.status);", " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:bad-request:parameter-validation-failed\");", " pm.expect(problemDetails.detail).to.equal(\"The request construction was invalid.\");", "", - " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be omitted or set to a value between 0 and 500.\");", + " pm.expect(problemDetails.errors[0]).to.equal(\"Limit must be a value between 0 and 500.\");", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], diff --git a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite.postman_collection.json index 630477a8e6..a54d3e6e07 100644 --- a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite.postman_collection.json @@ -3387,6 +3387,104 @@ } }, "response": [] + }, + { + "name": "Limit > 0 includes Next-Page-Token header", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "pm.test(\"Next-Page-Token header is present\", () => {\r", + " pm.response.to.have.header(\"Next-Page-Token\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/courses?offset=10&limit=25", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "courses" + ], + "query": [ + { + "key": "offset", + "value": "10" + }, + { + "key": "limit", + "value": "25" + } + ] + } + }, + "response": [] + }, + { + "name": "Limit = 0 excludes Next-Page-Token header", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "pm.test(\"Next-Page-Token header is NOT present\", () => {\r", + " pm.response.to.not.have.header(\"Next-Page-Token\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/courses?offset=10&limit=0", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "courses" + ], + "query": [ + { + "key": "offset", + "value": "10" + }, + { + "key": "limit", + "value": "0" + } + ] + } + }, + "response": [] } ] }, @@ -3957,7 +4055,7 @@ ] }, { - "name": "Keyset Paging", + "name": "Cursor Paging", "item": [ { "name": "Setup", @@ -4488,25 +4586,16 @@ "name": "Paging Validation", "item": [ { - "name": "Key set paging, No page size", + "name": "Cursor paging, No page size", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 400\", () => {\r", - " pm.expect(pm.response.code).to.equal(400);\r", - "});\r", - "\r", - "const problemDetails = pm.response.json();\r", - "\r", - "pm.test(\"Should indicate request was invalid.\", () => {\r", - " pm.expect(problemDetails.detail).to.equal(\"The request construction was invalid.\");\r", + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", "});\r", - "\r", - "pm.test(\"Should indicate both key set paging parameter must be provided.\", () => {\r", - " pm.expect(problemDetails.errors).to.contain(\"Page token and page size must both be provided for key set paging.\")\r", - "});" + "" ], "type": "text/javascript", "packages": {} @@ -4543,7 +4632,7 @@ "response": [] }, { - "name": "Key set paging, No page token", + "name": "Cursor paging, No page token", "event": [ { "listen": "test", @@ -4560,7 +4649,7 @@ "});\r", "\r", "pm.test(\"Should indicate both key set paging parameter must be provided.\", () => {\r", - " pm.expect(problemDetails.errors).to.contain(\"Page token and page size must both be provided for key set paging.\")\r", + " pm.expect(problemDetails.errors).to.contain(\"PageToken is required when pageSize is specified.\")\r", "});" ], "type": "text/javascript", @@ -4598,7 +4687,7 @@ "response": [] }, { - "name": "Key set paging, Invalid page token (not an encoded token)", + "name": "Cursor paging, Invalid page token (not an encoded token)", "event": [ { "listen": "test", @@ -4653,7 +4742,7 @@ "response": [] }, { - "name": "Key set paging, Invalid page token (leading space)", + "name": "Cursor paging, Invalid page token (leading space)", "event": [ { "listen": "test", @@ -4708,7 +4797,7 @@ "response": [] }, { - "name": "Key set paging, Offset also specified", + "name": "Cursor paging, Offset also specified", "event": [ { "listen": "test", @@ -4725,7 +4814,7 @@ "});\r", "\r", "pm.test(\"Should indicate both key set paging parameter must be provided.\", () => {\r", - " pm.expect(problemDetails.errors).to.contain(\"Both offset/limit and key set paging parameters are provided, but only one type of paging can be used.\");\r", + " pm.expect(problemDetails.errors).to.contain(\"Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together.\");\r", "});" ], "type": "text/javascript", @@ -4766,25 +4855,16 @@ "response": [] }, { - "name": "Key set paging, Limit also specified", + "name": "Cursor paging, Limit also specified", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 400\", () => {\r", - " pm.expect(pm.response.code).to.equal(400);\r", - "});\r", - "\r", - "const problemDetails = pm.response.json();\r", - "\r", - "pm.test(\"Should indicate request was invalid.\", () => {\r", - " pm.expect(problemDetails.detail).to.equal(\"The request construction was invalid.\");\r", + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", "});\r", - "\r", - "pm.test(\"Should indicate both key set paging parameter must be provided.\", () => {\r", - " pm.expect(problemDetails.errors).to.contain(\"Both offset/limit and key set paging parameters are provided, but only one type of paging can be used.\");\r", - "});" + "" ], "type": "text/javascript", "packages": {} @@ -4824,7 +4904,7 @@ "response": [] }, { - "name": "Key set paging, Total Count requested", + "name": "Cursor paging, Total Count requested", "event": [ { "listen": "test", @@ -4841,7 +4921,7 @@ "});\r", "\r", "pm.test(\"Should indicate Total Count cannot be determined while using key set paging.\", () => {\r", - " pm.expect(problemDetails.errors).to.contain(\"Total count cannot be determined while using key set paging.\");\r", + " pm.expect(problemDetails.errors).to.contain(\"Total count cannot be determined while using cursor paging (when pageToken is specified).\");\r", "});\r", "" ],