using FluentAssertions; using InboxIntel.Application.Abstractions; using InboxIntel.Application.DTOs; using InboxIntel.Domain.Entities; using InboxIntel.Infrastructure.Persistence; using InboxIntel.Infrastructure.Search; using Microsoft.EntityFrameworkCore; using Xunit; namespace InboxIntel.IntegrationTests; /// /// RECOMMENDATIONS #8: keyset (cursor) pagination for the browse path — the window after a /// (SentAtUtc, Id) cursor returns the next rows with no duplicates/skips and no OFFSET scan. /// public class KeysetPaginationTests { private sealed class FakeCurrentUser : ICurrentUser { public Guid UserId { get; set; } public bool IsAuthenticated => UserId != Guid.Empty; } [Fact] public async Task Cursor_window_continues_exactly_after_the_previous_page() { var user = Guid.NewGuid(); var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Cursor_window_continues_exactly_after_the_previous_page)).Options; var baseline = DateTimeOffset.UtcNow; using (var seed = new AppDbContext(opts, new FakeCurrentUser())) { var sender = new Sender { UserId = user, Address = "s@x.x" }; seed.Senders.Add(sender); for (var i = 0; i < 5; i++) seed.Emails.Add(new Email { UserId = user, GmailMessageId = $"m{i}", Sender = sender, SentAtUtc = baseline.AddMinutes(-i) }); await seed.SaveChangesAsync(); } using var ctx = new AppDbContext(opts, new FakeCurrentUser { UserId = user }); var svc = new SearchService(ctx); // First window via offset (page 1, size 2): m0, m1 (newest first). var page1 = await svc.SearchAsync(user, new SearchRequestDto(null, null, null, null, null, null, null, false, 1, 2)); page1.Items.Select(i => i.GmailMessageId).Should().Equal("m0", "m1"); // Next window via cursor from the last row of page 1. var last = page1.Items[^1]; var page2 = await svc.SearchAsync(user, new SearchRequestDto( null, null, null, null, null, null, null, false, 1, 2, AfterSentAtUtc: last.SentAtUtc, AfterId: last.Id)); page2.Items.Select(i => i.GmailMessageId).Should().Equal("m2", "m3"); // no dupes, no skips page2.TotalCount.Should().Be(-1, "cursor windows skip the COUNT — that's the perf win"); } }