|
3 | 3 |
|
4 | 4 | using System; |
5 | 5 | using System.Collections.Generic; |
| 6 | +using System.Collections.Specialized; |
6 | 7 | using System.IdentityModel.Tokens.Jwt; |
7 | 8 | using System.IO; |
8 | 9 | using System.IO.Abstractions; |
|
16 | 17 | using System.Text.Json; |
17 | 18 | using System.Threading; |
18 | 19 | using System.Threading.Tasks; |
| 20 | +using System.Web; |
19 | 21 | using Azure.DataApiBuilder.Config; |
20 | 22 | using Azure.DataApiBuilder.Config.ObjectModel; |
21 | 23 | using Azure.DataApiBuilder.Core.AuthenticationHelpers; |
@@ -2717,6 +2719,95 @@ public async Task OpenApi_EntityLevelRestEndpoint() |
2717 | 2719 | Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher", out _)); |
2718 | 2720 | } |
2719 | 2721 |
|
| 2722 | + /// <summary> |
| 2723 | + /// This test validates that DAB properly creates and returns a nextLink with a single $after |
| 2724 | + /// query parameter when sending paging requests. |
| 2725 | + /// The first request initiates a paging workload, meaning the response is expected to have a nextLink. |
| 2726 | + /// The validation occurs after the second request which uses the previously acquired nextLink |
| 2727 | + /// This test ensures that the second request's response body contains the expected nextLink which: |
| 2728 | + /// - is base64 encoded and NOT URI escaped e.g. the trailing "==" are not URI escaped to "%3D%3D" |
| 2729 | + /// - is not the same as the first response's nextLink -> DAB is properly injecting a new $after query param |
| 2730 | + /// and updating the new nextLink |
| 2731 | + /// - does not contain a comma (,) indicating that the URI namevaluecollection tracking the query parameters |
| 2732 | + /// did not come across two $after query parameters. This addresses a customer raised issue where two $after |
| 2733 | + /// query parameters were returned by DAB. |
| 2734 | + /// </summary> |
| 2735 | + [TestMethod] |
| 2736 | + [TestCategory(TestCategory.MSSQL)] |
| 2737 | + public async Task ValidateNextLinkUsage() |
| 2738 | + { |
| 2739 | + // Arrange - Setup test server with entity that has >1 record so that results can be paged. |
| 2740 | + // A short cut to using an entity with >100 records is to just include the $first=1 filter |
| 2741 | + // as done in this test, so that paging behavior can be invoked. |
| 2742 | + |
| 2743 | + const string ENTITY_NAME = "Bookmark"; |
| 2744 | + |
| 2745 | + // At least one entity is required in the runtime config for the engine to start. |
| 2746 | + // Even though this entity is not under test, it must be supplied to the config |
| 2747 | + // file creation function. |
| 2748 | + Entity requiredEntity = new( |
| 2749 | + Source: new("bookmarks", EntitySourceType.Table, null, null), |
| 2750 | + Rest: new(Enabled: true), |
| 2751 | + GraphQL: new(Singular: "", Plural: "", Enabled: false), |
| 2752 | + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, |
| 2753 | + Relationships: null, |
| 2754 | + Mappings: null); |
| 2755 | + |
| 2756 | + Dictionary<string, Entity> entityMap = new() |
| 2757 | + { |
| 2758 | + { ENTITY_NAME, requiredEntity } |
| 2759 | + }; |
| 2760 | + |
| 2761 | + CreateCustomConfigFile(globalRestEnabled: true, entityMap); |
| 2762 | + |
| 2763 | + string[] args = new[] |
| 2764 | + { |
| 2765 | + $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" |
| 2766 | + }; |
| 2767 | + |
| 2768 | + using TestServer server = new(Program.CreateWebHostBuilder(args)); |
| 2769 | + using HttpClient client = server.CreateClient(); |
| 2770 | + |
| 2771 | + // Setup and send GET request |
| 2772 | + HttpRequestMessage initialPaginationRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{ENTITY_NAME}?$first=1"); |
| 2773 | + HttpResponseMessage initialPaginationResponse = await client.SendAsync(initialPaginationRequest); |
| 2774 | + |
| 2775 | + // Process response body for first request and get the nextLink to use on subsequent request |
| 2776 | + // which represents what this test is validating. |
| 2777 | + string responseBody = await initialPaginationResponse.Content.ReadAsStringAsync(); |
| 2778 | + Dictionary<string, JsonElement> responseProperties = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(responseBody); |
| 2779 | + string nextLinkUri = responseProperties["nextLink"].ToString(); |
| 2780 | + |
| 2781 | + // Act - Submit request with nextLink uri as target and capture response |
| 2782 | + |
| 2783 | + HttpRequestMessage followNextLinkRequest = new(HttpMethod.Get, nextLinkUri); |
| 2784 | + HttpResponseMessage followNextLinkResponse = await client.SendAsync(followNextLinkRequest); |
| 2785 | + |
| 2786 | + // Assert |
| 2787 | + |
| 2788 | + Assert.AreEqual(HttpStatusCode.OK, followNextLinkResponse.StatusCode, message: "Expected request to succeed."); |
| 2789 | + |
| 2790 | + // Process the response body and inspect the "nextLink" property for expected contents. |
| 2791 | + string followNextLinkResponseBody = await followNextLinkResponse.Content.ReadAsStringAsync(); |
| 2792 | + Dictionary<string, JsonElement> followNextLinkResponseProperties = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(followNextLinkResponseBody); |
| 2793 | + |
| 2794 | + string followUpResponseNextLink = followNextLinkResponseProperties["nextLink"].ToString(); |
| 2795 | + Uri nextLink = new(uriString: followUpResponseNextLink); |
| 2796 | + NameValueCollection parsedQueryParameters = HttpUtility.ParseQueryString(query: nextLink.Query); |
| 2797 | + Assert.AreEqual(expected: false, actual: parsedQueryParameters["$after"].Contains(','), message: "nextLink erroneously contained two $after query parameters that were joined by HttpUtility.ParseQueryString(queryString)."); |
| 2798 | + Assert.AreNotEqual(notExpected: nextLinkUri, actual: followUpResponseNextLink, message: "The follow up request erroneously returned the same nextLink value."); |
| 2799 | + |
| 2800 | + // Do not use SqlPaginationUtils.Base64Encode()/Decode() here to eliminate test dependency on engine code to perform an assert. |
| 2801 | + try |
| 2802 | + { |
| 2803 | + Convert.FromBase64String(parsedQueryParameters["$after"]); |
| 2804 | + } |
| 2805 | + catch (FormatException) |
| 2806 | + { |
| 2807 | + Assert.Fail(message: "$after query parameter was not a valid base64 encoded value."); |
| 2808 | + } |
| 2809 | + } |
| 2810 | + |
2720 | 2811 | /// <summary> |
2721 | 2812 | /// Helper function to write custom configuration file. with minimal REST/GraphQL global settings |
2722 | 2813 | /// using the supplied entities. |
|
0 commit comments