Skip to content

Commit 5a645c9

Browse files
authored
Fix event cursor pagination links (#2322)
1 parent 12bce17 commit 5a645c9

3 files changed

Lines changed: 95 additions & 17 deletions

File tree

src/Exceptionless.Web/Utility/Results/OkWithHeadersContentResult.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public override void OnFormatting(ActionContext context)
4141
{
4242
var links = Page.HasValue
4343
? GetPagedLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Page.Value, HasMore)
44-
: GetBeforeAndAfterLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Before, After);
44+
: GetBeforeAndAfterLinks(new Uri(context.HttpContext.Request.GetDisplayUrl()), Before, After, HasMore);
4545
if (links.Count > 0)
4646
Headers[HeaderNames.Link] = links.ToArray();
4747

@@ -74,7 +74,7 @@ public static List<string> GetPagedLinks(Uri url, int page, bool hasMore)
7474
return links;
7575
}
7676

77-
public static List<string> GetBeforeAndAfterLinks(Uri url, string? before, string? after)
77+
public static List<string> GetBeforeAndAfterLinks(Uri url, string? before, string? after, bool hasMore)
7878
{
7979
var previousParameters = HttpUtility.ParseQueryString(url.Query);
8080
previousParameters.Remove("before");
@@ -88,7 +88,7 @@ public static List<string> GetBeforeAndAfterLinks(Uri url, string? before, strin
8888
var links = new List<string>(2);
8989
if (!String.IsNullOrEmpty(before))
9090
links.Add($"<{baseUrl}?{previousParameters.ToQueryString()}>; rel=\"previous\"");
91-
if (!String.IsNullOrEmpty(after))
91+
if (hasMore && !String.IsNullOrEmpty(after))
9292
links.Add($"<{baseUrl}?{nextParameters.ToQueryString()}>; rel=\"next\"");
9393

9494
return links;

tests/Exceptionless.Tests/Controllers/EventControllerTests.cs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,8 +1610,62 @@ await CreateDataAsync(d =>
16101610
}
16111611

16121612
[Fact]
1613-
public async Task CanEventsWithStablePagingAsync()
1613+
public async Task GetEvents_WithPartialLastCursorPage_DoesNotReturnNextLinkAsync()
16141614
{
1615+
// Arrange
1616+
await CreateDataAsync(d =>
1617+
{
1618+
d.Event().TestProject().Type(Event.KnownTypes.Log);
1619+
d.Event().TestProject().Type(Event.KnownTypes.Log);
1620+
d.Event().TestProject().Type(Event.KnownTypes.Log);
1621+
});
1622+
1623+
// Act
1624+
var response = await SendRequestAsync(r => r
1625+
.AsGlobalAdminUser()
1626+
.AppendPath("events")
1627+
.QueryString("limit", "2")
1628+
.QueryString("include", "total")
1629+
.StatusCodeShouldBeOk()
1630+
);
1631+
1632+
// Assert
1633+
Assert.Equal("3", response.Headers.GetValues(Headers.ResultCount).Single());
1634+
1635+
var links = ParseLinkHeaderValue(response.Headers.GetValues(HeaderNames.Link).ToArray());
1636+
Assert.True(links.TryGetValue("next", out var nextLink));
1637+
1638+
string? after = GetQueryStringValue(nextLink, "after");
1639+
Assert.NotNull(after);
1640+
1641+
var result = await response.Content.ReadFromJsonAsync<IReadOnlyCollection<PersistentEvent>>(TestCancellationToken);
1642+
Assert.NotNull(result);
1643+
Assert.Equal(2, result.Count);
1644+
1645+
// Act
1646+
response = await SendRequestAsync(r => r
1647+
.AsGlobalAdminUser()
1648+
.AppendPath("events")
1649+
.QueryString("limit", "2")
1650+
.QueryString("after", after)
1651+
.StatusCodeShouldBeOk()
1652+
);
1653+
1654+
// Assert
1655+
links = ParseLinkHeaderValue(response.Headers.GetValues(HeaderNames.Link).ToArray());
1656+
Assert.Single(links);
1657+
Assert.True(links.ContainsKey("previous"));
1658+
Assert.False(links.ContainsKey("next"));
1659+
1660+
result = await response.Content.ReadFromJsonAsync<IReadOnlyCollection<PersistentEvent>>(TestCancellationToken);
1661+
Assert.NotNull(result);
1662+
Assert.Single(result);
1663+
}
1664+
1665+
[Fact]
1666+
public async Task GetEvents_WithStableCursorPaging_ReturnsExpectedDirectionalLinksAsync()
1667+
{
1668+
// Arrange
16151669
await CreateDataAsync(d =>
16161670
{
16171671
d.Event().TestProject().Type(Event.KnownTypes.Log);
@@ -1623,6 +1677,7 @@ await CreateDataAsync(d =>
16231677
Log.SetLogLevel<StackRepository>(LogLevel.Trace);
16241678
Log.SetLogLevel<EventStackFilterQueryBuilder>(LogLevel.Trace);
16251679

1680+
// Act
16261681
var response = await SendRequestAsync(r => r
16271682
.AsGlobalAdminUser()
16281683
.AppendPath("events")
@@ -1631,6 +1686,7 @@ await CreateDataAsync(d =>
16311686
.StatusCodeShouldBeOk()
16321687
);
16331688

1689+
// Assert
16341690
Assert.Equal("3", response.Headers.GetValues(Headers.ResultCount).Single());
16351691

16361692
var links = ParseLinkHeaderValue(response.Headers.GetValues(HeaderNames.Link).ToArray());
@@ -1647,7 +1703,7 @@ await CreateDataAsync(d =>
16471703
Assert.NotNull(result);
16481704
string firstEventId = result.Single().Id;
16491705

1650-
// Go to second page
1706+
// Act
16511707
response = await SendRequestAsync(r => r
16521708
.AsGlobalAdminUser()
16531709
.AppendPath("events")
@@ -1657,6 +1713,7 @@ await CreateDataAsync(d =>
16571713
.StatusCodeShouldBeOk()
16581714
);
16591715

1716+
// Assert
16601717
Assert.Equal("3", response.Headers.GetValues(Headers.ResultCount).Single());
16611718
links = ParseLinkHeaderValue(response.Headers.GetValues(HeaderNames.Link).ToArray());
16621719
Assert.Equal(2, links.Count);
@@ -1673,7 +1730,7 @@ await CreateDataAsync(d =>
16731730
string secondEventId = result.Single().Id;
16741731
Assert.NotEqual(firstEventId, secondEventId);
16751732

1676-
// Go to last page
1733+
// Act
16771734
response = await SendRequestAsync(r => r
16781735
.AsGlobalAdminUser()
16791736
.AppendPath("events")
@@ -1683,23 +1740,21 @@ await CreateDataAsync(d =>
16831740
.StatusCodeShouldBeOk()
16841741
);
16851742

1743+
// Assert
16861744
Assert.Equal("3", response.Headers.GetValues(Headers.ResultCount).Single());
16871745
links = ParseLinkHeaderValue(response.Headers.GetValues(HeaderNames.Link).ToArray());
1688-
Assert.Equal(2, links.Count);
1746+
Assert.Single(links);
16891747

16901748
before = GetQueryStringValue(links["previous"], "before");
16911749
Assert.NotNull(before);
1692-
1693-
after = GetQueryStringValue(links["next"], "after");
1694-
Assert.NotNull(after);
1695-
Assert.Equal(before, after);
1750+
Assert.False(links.ContainsKey("next"));
16961751

16971752
result = await response.Content.ReadFromJsonAsync<IReadOnlyCollection<PersistentEvent>>(TestCancellationToken);
16981753
Assert.NotNull(result);
16991754
string thirdEventId = result.Single().Id;
17001755
Assert.NotEqual(secondEventId, thirdEventId);
17011756

1702-
// go to previous page
1757+
// Act
17031758
response = await SendRequestAsync(r => r
17041759
.AsGlobalAdminUser()
17051760
.AppendPath("events")
@@ -1709,6 +1764,7 @@ await CreateDataAsync(d =>
17091764
.StatusCodeShouldBeOk()
17101765
);
17111766

1767+
// Assert
17121768
Assert.Equal("3", response.Headers.GetValues(Headers.ResultCount).Single());
17131769
links = ParseLinkHeaderValue(response.Headers.GetValues(HeaderNames.Link).ToArray());
17141770
Assert.Equal(2, links.Count);

tests/Exceptionless.Tests/Miscellaneous/EfficientPagingTests.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ public EfficientPagingTests(ITestOutputHelper output) : base(output) { }
1212
[InlineData("http://localhost?after=1", "1", null, true, false)]
1313
[InlineData("http://localhost?after=1", "1", "2", true, true)]
1414
[InlineData("http://localhost?before=11", null, "1", false, true)]
15-
public void CanBeforeAndAfterLinks(string url, string? before, string? after, bool expectPrevious, bool expectNext)
15+
public void GetBeforeAndAfterLinks_WithCursorTokens_ReturnsExpectedDirectionalLinks(string url, string? before, string? after, bool expectPrevious, bool expectNext)
1616
{
17-
var links = OkWithResourceLinks<string>.GetBeforeAndAfterLinks(new Uri(url), before, after); ;
17+
// Arrange
1818
byte expectedLinkCount = 0;
1919
if (expectPrevious)
2020
expectedLinkCount++;
2121
if (expectNext)
2222
expectedLinkCount++;
2323

24+
// Act
25+
var links = OkWithResourceLinks<string>.GetBeforeAndAfterLinks(new Uri(url), before, after, true);
26+
27+
// Assert
2428
foreach (string link in links)
2529
_logger.LogInformation(link);
2630

@@ -31,21 +35,39 @@ public void CanBeforeAndAfterLinks(string url, string? before, string? after, bo
3135
Assert.Contains(links, l => l.Contains("next") && l.Contains("after"));
3236
}
3337

38+
[Fact]
39+
public void GetBeforeAndAfterLinks_WithoutMoreResults_DoesNotIncludeNextLink()
40+
{
41+
// Arrange
42+
var url = new Uri("http://localhost?after=1");
43+
44+
// Act
45+
var links = OkWithResourceLinks<string>.GetBeforeAndAfterLinks(url, "1", "2", false);
46+
47+
// Assert
48+
Assert.Single(links);
49+
Assert.Contains(links, l => l.Contains("previous") && l.Contains("before"));
50+
Assert.DoesNotContain(links, l => l.Contains("next"));
51+
}
52+
3453
[Theory]
3554
[InlineData("http://localhost", 0, false, false, false)]
3655
[InlineData("http://localhost", 1, false, false, false)]
3756
[InlineData("http://localhost", 2, false, true, false)]
3857
[InlineData("http://localhost", 2, true, true, true)]
39-
public void CanPageLinks(string url, int pageNumber, bool hasMore, bool expectPrevious, bool expectNext)
58+
public void GetPagedLinks_WithPageNumber_ReturnsExpectedDirectionalLinks(string url, int pageNumber, bool hasMore, bool expectPrevious, bool expectNext)
4059
{
41-
var links = OkWithResourceLinks<string>.GetPagedLinks(new Uri(url), pageNumber, hasMore);
42-
60+
// Arrange
4361
int expectedLinkCount = 0;
4462
if (expectPrevious)
4563
expectedLinkCount++;
4664
if (expectNext)
4765
expectedLinkCount++;
4866

67+
// Act
68+
var links = OkWithResourceLinks<string>.GetPagedLinks(new Uri(url), pageNumber, hasMore);
69+
70+
// Assert
4971
foreach (string link in links)
5072
_logger.LogInformation(link);
5173

0 commit comments

Comments
 (0)