102 lines
4.1 KiB
C#
102 lines
4.1 KiB
C#
using System.Diagnostics;
|
|
using System.Net.Http.Json;
|
|
using ItWorx.KnowledgeHub.Contracts.Knowledge;
|
|
using ItWorx.KnowledgeHub.Domain.Knowledge;
|
|
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
|
|
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace ItWorx.KnowledgeHub.IntegrationTests.Knowledge;
|
|
|
|
/// <summary>A 10,000-node synthetic hierarchy remains navigable without loading the
|
|
/// entire tree into the browser (docs/27-acceptance-matrix.md, M2-A07): the API must
|
|
/// serve one bounded page of children quickly, regardless of how many siblings exist
|
|
/// in total.</summary>
|
|
[Collection(PostgresTestGroup.Name)]
|
|
public sealed class PageTreeScaleTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
|
|
{
|
|
private const int ChildCount = 10_000;
|
|
|
|
[Fact]
|
|
public async Task Fetching_one_page_of_children_under_a_10000_child_node_stays_fast_and_bounded()
|
|
{
|
|
using var client = Factory.CreateClient();
|
|
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
|
|
|
|
var createResponse = await SetupHelper.PostWithCsrfAsync(
|
|
client, "/api/v1/pages", new CreatePageRequest(null, "Grote categorie", "WikiPage", "Root", null));
|
|
var parent = await createResponse.Content.ReadFromJsonAsync<PageDetail>();
|
|
Assert.NotNull(parent);
|
|
|
|
await SeedChildrenDirectlyAsync(parent.Id);
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var firstPage = await client.GetFromJsonAsync<PageChildrenResponse>(
|
|
$"/api/v1/pages/{parent.Id}/children?limit=50");
|
|
stopwatch.Stop();
|
|
|
|
Assert.NotNull(firstPage);
|
|
Assert.Equal(50, firstPage.Items.Count);
|
|
Assert.False(string.IsNullOrEmpty(firstPage.NextCursor));
|
|
|
|
// Generous bound for a CI/dev machine: the point is "bounded and fast", not a
|
|
// precise performance benchmark (that belongs to the M8 release scale suite).
|
|
Assert.True(
|
|
stopwatch.Elapsed < TimeSpan.FromSeconds(5),
|
|
$"Fetching one bounded page of children took {stopwatch.Elapsed}, expected it to stay well under 5s regardless of the {ChildCount} total children.");
|
|
|
|
var secondPage = await client.GetFromJsonAsync<PageChildrenResponse>(
|
|
$"/api/v1/pages/{parent.Id}/children?limit=50&cursor={Uri.EscapeDataString(firstPage.NextCursor!)}");
|
|
Assert.NotNull(secondPage);
|
|
Assert.Equal(50, secondPage.Items.Count);
|
|
Assert.Empty(firstPage.Items.Select(item => item.Id).Intersect(secondPage.Items.Select(item => item.Id)));
|
|
}
|
|
|
|
private async Task SeedChildrenDirectlyAsync(Guid parentId)
|
|
{
|
|
// Seeded directly through the DbContext rather than 10,000 HTTP calls — this
|
|
// test is about proving the *read* path stays bounded at scale, not about
|
|
// exercising page creation (already covered by PageCrudTests).
|
|
using var scope = Factory.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
|
|
|
var parent = await db.Pages.FindAsync(parentId);
|
|
Assert.NotNull(parent);
|
|
var workspaceId = parent.WorkspaceId;
|
|
var actorId = parent.CreatedBy ?? Guid.NewGuid();
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
var sortKey = SortKey.First();
|
|
var batch = new List<Page>(1000);
|
|
|
|
for (var i = 0; i < ChildCount; i++)
|
|
{
|
|
var page = Page.Create(
|
|
workspaceId,
|
|
parentId,
|
|
sortKey,
|
|
PageTitle.Create($"Kindpagina {i:D5}"),
|
|
DocumentType.WikiPage,
|
|
"Inhoud",
|
|
actorId,
|
|
now);
|
|
batch.Add(page);
|
|
sortKey = SortKey.Next(sortKey);
|
|
|
|
if (batch.Count == 1000)
|
|
{
|
|
db.Pages.AddRange(batch);
|
|
await db.SaveChangesAsync();
|
|
db.ChangeTracker.Clear();
|
|
batch.Clear();
|
|
}
|
|
}
|
|
|
|
if (batch.Count > 0)
|
|
{
|
|
db.Pages.AddRange(batch);
|
|
await db.SaveChangesAsync();
|
|
}
|
|
}
|
|
}
|